From 1df4405e0f6a6e3ac27497aa965db770c036406f Mon Sep 17 00:00:00 2001 From: amickan Date: Mon, 16 Dec 2024 15:37:46 +0100 Subject: [PATCH] Update tests --- app/grandchallenge/algorithms/forms.py | 1 + app/grandchallenge/algorithms/models.py | 4 +- app/grandchallenge/algorithms/serializers.py | 1 + app/tests/algorithms_tests/test_forms.py | 91 +++-- app/tests/algorithms_tests/test_models.py | 46 ++- .../algorithms_tests/test_permissions.py | 9 +- app/tests/algorithms_tests/test_tasks.py | 27 +- app/tests/algorithms_tests/test_views.py | 351 ++++++++---------- app/tests/conftest.py | 6 +- 9 files changed, 307 insertions(+), 229 deletions(-) diff --git a/app/grandchallenge/algorithms/forms.py b/app/grandchallenge/algorithms/forms.py index d72cf1c47c..8ebd68c8da 100644 --- a/app/grandchallenge/algorithms/forms.py +++ b/app/grandchallenge/algorithms/forms.py @@ -205,6 +205,7 @@ def clean(self): if Job.objects.get_jobs_with_same_inputs( inputs=cleaned_data["inputs"], + interface=cleaned_data["algorithm_interface"], algorithm_image=cleaned_data["algorithm_image"], algorithm_model=cleaned_data["algorithm_model"], ): diff --git a/app/grandchallenge/algorithms/models.py b/app/grandchallenge/algorithms/models.py index 24cf3c09ac..022d9d86f5 100644 --- a/app/grandchallenge/algorithms/models.py +++ b/app/grandchallenge/algorithms/models.py @@ -976,13 +976,13 @@ def retrieve_existing_civs(*, civ_data): return existing_civs def get_jobs_with_same_inputs( - self, *, inputs, algorithm_image, algorithm_model + self, *, inputs, interface, algorithm_image, algorithm_model ): existing_civs = self.retrieve_existing_civs(civ_data=inputs) unique_kwargs = { "algorithm_image": algorithm_image, } - input_interface_count = algorithm_image.algorithm.inputs.count() + input_interface_count = interface.inputs.count() if algorithm_model: unique_kwargs["algorithm_model"] = algorithm_model diff --git a/app/grandchallenge/algorithms/serializers.py b/app/grandchallenge/algorithms/serializers.py index 28ece083a8..c3b2ae6c1a 100644 --- a/app/grandchallenge/algorithms/serializers.py +++ b/app/grandchallenge/algorithms/serializers.py @@ -258,6 +258,7 @@ def validate(self, data): if Job.objects.get_jobs_with_same_inputs( inputs=self.inputs, + interface=None, # TODO fix this in separate PR algorithm_image=data["algorithm_image"], algorithm_model=data["algorithm_model"], ): diff --git a/app/tests/algorithms_tests/test_forms.py b/app/tests/algorithms_tests/test_forms.py index 580ed73e33..43388f5423 100644 --- a/app/tests/algorithms_tests/test_forms.py +++ b/app/tests/algorithms_tests/test_forms.py @@ -321,7 +321,10 @@ def test_create_job_input_fields( response = get_view_for_user( viewname="algorithms:job-create", client=client, - reverse_kwargs={"slug": alg.slug}, + reverse_kwargs={ + "slug": alg.slug, + "interface": alg.default_interface.pk, + }, follow=True, user=creator, ) @@ -353,7 +356,10 @@ def test_create_job_json_input_field_validation( response = get_view_for_user( viewname="algorithms:job-create", client=client, - reverse_kwargs={"slug": alg.slug}, + reverse_kwargs={ + "slug": alg.slug, + "interface": alg.default_interface.pk, + }, method=client.post, follow=True, user=creator, @@ -384,7 +390,10 @@ def test_create_job_simple_input_field_validation( response = get_view_for_user( viewname="algorithms:job-create", client=client, - reverse_kwargs={"slug": alg.slug}, + reverse_kwargs={ + "slug": alg.slug, + "interface": alg.default_interface.pk, + }, method=client.post, follow=True, user=creator, @@ -400,7 +409,11 @@ def create_algorithm_with_input(slug): VerificationFactory(user=creator, is_verified=True) alg = AlgorithmFactory() alg.add_editor(user=creator) - alg.inputs.set([ComponentInterface.objects.get(slug=slug)]) + interface = AlgorithmInterfaceFactory( + inputs=[ComponentInterface.objects.get(slug=slug)], + outputs=[ComponentInterfaceFactory()], + ) + alg.interfaces.add(interface, through_defaults={"is_default": True}) AlgorithmImageFactory( algorithm=alg, is_manifest_valid=True, @@ -500,13 +513,34 @@ def test_only_publish_successful_jobs(): @pytest.mark.django_db class TestJobCreateLimits: + + def create_form(self, algorithm, user, algorithm_image=None): + ci = ComponentInterfaceFactory(kind=ComponentInterface.Kind.STRING) + interface = AlgorithmInterfaceFactory(inputs=[ci]) + algorithm.interfaces.add(interface) + + algorithm_image_kwargs = {} + if algorithm_image: + algorithm_image_kwargs = { + "algorithm_image": str(algorithm_image.pk) + } + + return JobCreateForm( + algorithm=algorithm, + user=user, + interface=interface, + data={ + **algorithm_image_kwargs, + **get_interface_form_data(interface_slug=ci.slug, data="Foo"), + }, + ) + def test_form_invalid_without_enough_credits(self, settings): algorithm = AlgorithmFactory( minimum_credits_per_job=( settings.ALGORITHMS_GENERAL_CREDITS_PER_MONTH_PER_USER + 1 ), ) - algorithm.inputs.clear() user = UserFactory() AlgorithmImageFactory( algorithm=algorithm, @@ -514,13 +548,11 @@ def test_form_invalid_without_enough_credits(self, settings): is_in_registry=True, is_desired_version=True, ) - - form = JobCreateForm(algorithm=algorithm, user=user, data={}) - + form = self.create_form(algorithm=algorithm, user=user) assert not form.is_valid() - assert form.errors == { - "__all__": ["You have run out of algorithm credits"], - } + assert "You have run out of algorithm credits" in str( + form.errors["__all__"] + ) def test_form_valid_for_editor(self, settings): algorithm = AlgorithmFactory( @@ -528,7 +560,6 @@ def test_form_valid_for_editor(self, settings): settings.ALGORITHMS_GENERAL_CREDITS_PER_MONTH_PER_USER + 1 ), ) - algorithm.inputs.clear() algorithm_image = AlgorithmImageFactory( algorithm=algorithm, is_manifest_valid=True, @@ -539,17 +570,15 @@ def test_form_valid_for_editor(self, settings): algorithm.add_editor(user=user) - form = JobCreateForm( + form = self.create_form( algorithm=algorithm, user=user, - data={"algorithm_image": str(algorithm_image.pk)}, + algorithm_image=algorithm_image, ) - assert form.is_valid() def test_form_valid_with_credits(self): algorithm = AlgorithmFactory(minimum_credits_per_job=1) - algorithm.inputs.clear() algorithm_image = AlgorithmImageFactory( algorithm=algorithm, is_manifest_valid=True, @@ -558,12 +587,11 @@ def test_form_valid_with_credits(self): ) user = UserFactory() - form = JobCreateForm( + form = self.create_form( algorithm=algorithm, user=user, - data={"algorithm_image": str(algorithm_image.pk)}, + algorithm_image=algorithm_image, ) - assert form.is_valid() @@ -710,7 +738,12 @@ def test_creator_queryset( ): algorithm = algorithm_with_image_and_model_and_two_inputs.algorithm editor = algorithm.editors_group.user_set.first() - form = JobCreateForm(algorithm=algorithm, user=editor, data={}) + form = JobCreateForm( + algorithm=algorithm, + user=editor, + interface=algorithm.default_interface, + data={}, + ) assert list(form.fields["creator"].queryset.all()) == [editor] assert form.fields["creator"].initial == editor @@ -726,7 +759,12 @@ def test_algorithm_image_queryset( is_manifest_valid=True, is_in_registry=True, ) - form = JobCreateForm(algorithm=algorithm, user=editor, data={}) + form = JobCreateForm( + algorithm=algorithm, + user=editor, + interface=algorithm.default_interface, + data={}, + ) ai_qs = form.fields["algorithm_image"].queryset.all() assert algorithm.active_image in ai_qs assert inactive_image not in ai_qs @@ -745,12 +783,14 @@ def test_cannot_create_job_with_same_inputs_twice( algorithm_model=algorithm.active_model, status=Job.SUCCESS, time_limit=123, + algorithm_interface=algorithm.default_interface, ) job.inputs.set(civs) form = JobCreateForm( algorithm=algorithm, user=editor, + interface=algorithm.default_interface, data={ "algorithm_image": algorithm.active_image, "algorithm_model": algorithm.active_model, @@ -775,13 +815,18 @@ def test_all_inputs_required_on_job_creation(algorithm_with_multiple_inputs): kind=InterfaceKind.InterfaceKindChoices.ANY, store_in_database=True, ) - algorithm_with_multiple_inputs.algorithm.inputs.add( - ci_json_in_db_without_schema + interface = AlgorithmInterfaceFactory( + inputs=[ci_json_in_db_without_schema], + outputs=[ComponentInterfaceFactory()], + ) + algorithm_with_multiple_inputs.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} ) form = JobCreateForm( algorithm=algorithm_with_multiple_inputs.algorithm, user=algorithm_with_multiple_inputs.editor, + interface=interface, data={}, ) diff --git a/app/tests/algorithms_tests/test_models.py b/app/tests/algorithms_tests/test_models.py index 6c7d8028be..9c5f6b797a 100644 --- a/app/tests/algorithms_tests/test_models.py +++ b/app/tests/algorithms_tests/test_models.py @@ -656,12 +656,15 @@ def test_job_with_same_image_different_model( data = self.get_civ_data(civs=civs) j = AlgorithmJobFactory( - algorithm_image=alg.active_image, time_limit=10 + algorithm_image=alg.active_image, + time_limit=10, + algorithm_interface=alg.default_interface, ) j.inputs.set(civs) jobs = Job.objects.get_jobs_with_same_inputs( inputs=data, + interface=alg.default_interface, algorithm_image=alg.active_image, algorithm_model=alg.active_model, ) @@ -678,10 +681,12 @@ def test_job_with_same_model_different_image( algorithm_image=AlgorithmImageFactory(), algorithm_model=alg.active_model, time_limit=10, + algorithm_interface=alg.default_interface, ) j.inputs.set(civs) jobs = Job.objects.get_jobs_with_same_inputs( inputs=data, + interface=alg.default_interface, algorithm_image=alg.active_image, algorithm_model=alg.active_model, ) @@ -698,10 +703,12 @@ def test_job_with_same_model_and_image( algorithm_model=alg.active_model, algorithm_image=alg.active_image, time_limit=10, + algorithm_interface=alg.default_interface, ) j.inputs.set(civs) jobs = Job.objects.get_jobs_with_same_inputs( inputs=data, + interface=alg.default_interface, algorithm_image=alg.active_image, algorithm_model=alg.active_model, ) @@ -719,10 +726,12 @@ def test_job_with_different_image_and_model( algorithm_model=AlgorithmModelFactory(), algorithm_image=AlgorithmImageFactory(), time_limit=10, + algorithm_interface=alg.default_interface, ) j.inputs.set(civs) jobs = Job.objects.get_jobs_with_same_inputs( inputs=data, + interface=alg.default_interface, algorithm_image=alg.active_image, algorithm_model=alg.active_model, ) @@ -739,10 +748,14 @@ def test_job_with_same_image_no_model_provided( algorithm_model=alg.active_model, algorithm_image=alg.active_image, time_limit=10, + algorithm_interface=alg.default_interface, ) j.inputs.set(civs) jobs = Job.objects.get_jobs_with_same_inputs( - inputs=data, algorithm_image=alg.active_image, algorithm_model=None + inputs=data, + interface=alg.default_interface, + algorithm_image=alg.active_image, + algorithm_model=None, ) assert len(jobs) == 0 @@ -754,11 +767,16 @@ def test_job_with_same_image_and_without_model( data = self.get_civ_data(civs=civs) j = AlgorithmJobFactory( - algorithm_image=alg.active_image, time_limit=10 + algorithm_image=alg.active_image, + time_limit=10, + algorithm_interface=alg.default_interface, ) j.inputs.set(civs) jobs = Job.objects.get_jobs_with_same_inputs( - inputs=data, algorithm_image=alg.active_image, algorithm_model=None + inputs=data, + interface=alg.default_interface, + algorithm_image=alg.active_image, + algorithm_model=None, ) assert j in jobs assert len(jobs) == 1 @@ -771,7 +789,9 @@ def test_job_with_different_input( data = self.get_civ_data(civs=civs) j = AlgorithmJobFactory( - algorithm_image=alg.active_image, time_limit=10 + algorithm_image=alg.active_image, + time_limit=10, + algorithm_interface=alg.default_interface, ) j.inputs.set( [ @@ -780,7 +800,10 @@ def test_job_with_different_input( ] ) jobs = Job.objects.get_jobs_with_same_inputs( - inputs=data, algorithm_image=alg.active_image, algorithm_model=None + inputs=data, + interface=alg.default_interface, + algorithm_image=alg.active_image, + algorithm_model=None, ) assert len(jobs) == 0 @@ -982,8 +1005,15 @@ def test_inputs_complete(): ci1, ci2, ci3 = ComponentInterfaceFactory.create_batch( 3, kind=ComponentInterface.Kind.STRING ) - alg.inputs.set([ci1, ci2, ci3]) - job = AlgorithmJobFactory(algorithm_image__algorithm=alg, time_limit=10) + interface = AlgorithmInterfaceFactory( + inputs=[ci1, ci2, ci3], outputs=[ComponentInterfaceFactory()] + ) + alg.interfaces.add(interface, through_defaults={"is_default": True}) + job = AlgorithmJobFactory( + algorithm_image__algorithm=alg, + time_limit=10, + algorithm_interface=alg.default_interface, + ) civ_with_value_1 = ComponentInterfaceValueFactory( interface=ci1, value="Foo" ) diff --git a/app/tests/algorithms_tests/test_permissions.py b/app/tests/algorithms_tests/test_permissions.py index 4349bd47e8..a6fd95afa8 100644 --- a/app/tests/algorithms_tests/test_permissions.py +++ b/app/tests/algorithms_tests/test_permissions.py @@ -18,6 +18,7 @@ from tests.algorithms_tests.factories import ( AlgorithmFactory, AlgorithmImageFactory, + AlgorithmInterfaceFactory, AlgorithmJobFactory, ) from tests.algorithms_tests.utils import TwoAlgorithms @@ -313,7 +314,12 @@ def test_job_permissions_from_template(self, client): kind=InterfaceKind.InterfaceKindChoices.ANY, store_in_database=True, ) - algorithm_image.algorithm.inputs.set([ci]) + interface = AlgorithmInterfaceFactory( + inputs=[ci], outputs=[ComponentInterfaceFactory()] + ) + algorithm_image.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} + ) response = get_view_for_user( viewname="algorithms:job-create", @@ -321,6 +327,7 @@ def test_job_permissions_from_template(self, client): method=client.post, reverse_kwargs={ "slug": algorithm_image.algorithm.slug, + "interface": algorithm_image.algorithm.default_interface.pk, }, user=user, follow=True, diff --git a/app/tests/algorithms_tests/test_tasks.py b/app/tests/algorithms_tests/test_tasks.py index 2e81afe8bf..b980118827 100644 --- a/app/tests/algorithms_tests/test_tasks.py +++ b/app/tests/algorithms_tests/test_tasks.py @@ -27,6 +27,7 @@ from tests.algorithms_tests.factories import ( AlgorithmFactory, AlgorithmImageFactory, + AlgorithmInterfaceFactory, AlgorithmJobFactory, AlgorithmModelFactory, ) @@ -219,8 +220,13 @@ def test_algorithm( slug="results-json-file" ) heatmap_interface = ComponentInterface.objects.get(slug="generic-overlay") - ai.algorithm.inputs.set([input_interface]) - ai.algorithm.outputs.set([json_result_interface, heatmap_interface]) + interface = AlgorithmInterfaceFactory( + inputs=[input_interface], + outputs=[json_result_interface, heatmap_interface], + ) + ai.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} + ) civ = ComponentInterfaceValueFactory( image=image_file.image, interface=input_interface, file=None @@ -343,9 +349,12 @@ def test_algorithm_with_invalid_output( slug="detection-json-file", kind=ComponentInterface.Kind.ANY, ) - ai.algorithm.inputs.add(input_interface) - ai.algorithm.outputs.add(detection_interface) - ai.save() + interface = AlgorithmInterfaceFactory( + inputs=[input_interface], outputs=[detection_interface] + ) + ai.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} + ) image_file = ImageFileFactory( file__from_path=Path(__file__).parent / "resources" / "input_file.tif" @@ -422,11 +431,17 @@ def test_execute_algorithm_job_for_missing_inputs(settings): # create the job without value for the ComponentInterfaceValues ci = ComponentInterface.objects.get(slug="generic-medical-image") ComponentInterfaceValue.objects.create(interface=ci) - alg.algorithm.inputs.add(ci) + interface = AlgorithmInterfaceFactory( + inputs=[ci], outputs=[ComponentInterfaceFactory()] + ) + alg.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} + ) job = AlgorithmJobFactory( creator=creator, algorithm_image=alg, time_limit=alg.algorithm.time_limit, + algorithm_interface=interface, ) execute_algorithm_job_for_inputs(job_pk=job.pk) diff --git a/app/tests/algorithms_tests/test_views.py b/app/tests/algorithms_tests/test_views.py index bd298c6626..1d741dd505 100644 --- a/app/tests/algorithms_tests/test_views.py +++ b/app/tests/algorithms_tests/test_views.py @@ -1,7 +1,6 @@ import datetime import io import json -import tempfile import zipfile from pathlib import Path from unittest.mock import patch @@ -53,7 +52,7 @@ UserUploadFactory, create_upload_from_file, ) -from tests.utils import get_view_for_user, recurse_callbacks +from tests.utils import get_view_for_user from tests.verification_tests.factories import VerificationFactory @@ -355,6 +354,13 @@ def test_permission_required_views(self, client): time_limit=ai.algorithm.time_limit, ) p = AlgorithmPermissionRequestFactory(algorithm=ai.algorithm) + interface = AlgorithmInterfaceFactory( + inputs=[ComponentInterfaceFactory()], + outputs=[ComponentInterfaceFactory()], + ) + ai.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} + ) VerificationFactory(user=u, is_verified=True) @@ -421,14 +427,10 @@ def test_permission_required_views(self, client): ), ( "job-create", - {"slug": ai.algorithm.slug}, - "execute_algorithm", - ai.algorithm, - None, - ), - ( - "job-interface-select", - {"slug": ai.algorithm.slug}, + { + "slug": ai.algorithm.slug, + "interface": ai.algorithm.default_interface.pk, + }, "execute_algorithm", ai.algorithm, None, @@ -933,158 +935,6 @@ def list_algorithm_images(self, **__): assert image_interface.kind == ComponentInterface.Kind.IMAGE -@pytest.mark.django_db -def test_create_job_with_json_file( - client, settings, algorithm_io_image, django_capture_on_commit_callbacks -): - settings.task_eager_propagates = (True,) - settings.task_always_eager = (True,) - - with django_capture_on_commit_callbacks() as callbacks: - ai = AlgorithmImageFactory(image__from_path=algorithm_io_image) - recurse_callbacks( - callbacks=callbacks, - django_capture_on_commit_callbacks=django_capture_on_commit_callbacks, - ) - - editor = UserFactory() - VerificationFactory(user=editor, is_verified=True) - ai.algorithm.add_editor(editor) - ci = ComponentInterfaceFactory( - kind=InterfaceKind.InterfaceKindChoices.ANY, store_in_database=False - ) - ai.algorithm.inputs.set([ci]) - - with tempfile.NamedTemporaryFile(mode="w+", suffix=".json") as file: - json.dump('{"Foo": "bar"}', file) - file.seek(0) - upload = create_upload_from_file( - creator=editor, file_path=Path(file.name) - ) - with django_capture_on_commit_callbacks(execute=True): - with django_capture_on_commit_callbacks(execute=True): - response = get_view_for_user( - viewname="algorithms:job-create", - client=client, - method=client.post, - reverse_kwargs={ - "slug": ai.algorithm.slug, - }, - user=editor, - follow=True, - data={ - **get_interface_form_data( - interface_slug=ci.slug, data=upload.pk - ) - }, - ) - assert response.status_code == 200 - assert ( - file.name.split("/")[-1] - in Job.objects.get().inputs.first().file.name - ) - - -@pytest.mark.django_db -def test_algorithm_job_create_with_image_input( - settings, client, algorithm_io_image, django_capture_on_commit_callbacks -): - settings.task_eager_propagates = (True,) - settings.task_always_eager = (True,) - - with django_capture_on_commit_callbacks() as callbacks: - ai = AlgorithmImageFactory(image__from_path=algorithm_io_image) - recurse_callbacks( - callbacks=callbacks, - django_capture_on_commit_callbacks=django_capture_on_commit_callbacks, - ) - - editor = UserFactory() - VerificationFactory(user=editor, is_verified=True) - ai.algorithm.add_editor(editor) - ci = ComponentInterfaceFactory( - kind=InterfaceKind.InterfaceKindChoices.IMAGE, store_in_database=False - ) - ai.algorithm.inputs.set([ci]) - - image1, image2 = ImageFactory.create_batch(2) - assign_perm("cases.view_image", editor, image1) - assign_perm("cases.view_image", editor, image2) - - civ = ComponentInterfaceValueFactory(interface=ci, image=image1) - with django_capture_on_commit_callbacks(execute=True): - with django_capture_on_commit_callbacks(execute=True): - response = get_view_for_user( - viewname="algorithms:job-create", - client=client, - method=client.post, - reverse_kwargs={ - "slug": ai.algorithm.slug, - }, - user=editor, - follow=True, - data={ - **get_interface_form_data( - interface_slug=ci.slug, - data=image1.pk, - existing_data=True, - ) - }, - ) - assert response.status_code == 200 - assert str(Job.objects.get().inputs.first().image.pk) == str(image1.pk) - # same civ reused - assert Job.objects.get().inputs.first() == civ - - with django_capture_on_commit_callbacks(execute=True): - with django_capture_on_commit_callbacks(execute=True): - response = get_view_for_user( - viewname="algorithms:job-create", - client=client, - method=client.post, - reverse_kwargs={ - "slug": ai.algorithm.slug, - }, - user=editor, - follow=True, - data={ - **get_interface_form_data( - interface_slug=ci.slug, - data=image2.pk, - existing_data=True, - ) - }, - ) - assert response.status_code == 200 - assert str(Job.objects.last().inputs.first().image.pk) == str(image2.pk) - assert Job.objects.last().inputs.first() != civ - - upload = create_upload_from_file( - file_path=RESOURCE_PATH / "image10x10x10.mha", - creator=editor, - ) - with django_capture_on_commit_callbacks(execute=True): - with django_capture_on_commit_callbacks(execute=True): - response = get_view_for_user( - viewname="algorithms:job-create", - client=client, - method=client.post, - reverse_kwargs={ - "slug": ai.algorithm.slug, - }, - user=editor, - follow=True, - data={ - **get_interface_form_data( - interface_slug=ci.slug, data=upload.pk - ) - }, - ) - assert response.status_code == 200 - assert Job.objects.last().inputs.first().image.name == "image10x10x10.mha" - assert Job.objects.last().inputs.first() != civ - - @pytest.mark.django_db class TestJobCreateView: @@ -1110,6 +960,7 @@ def create_job( user=user, reverse_kwargs={ "slug": algorithm.slug, + "interface": algorithm.default_interface.pk, }, follow=True, data=inputs, @@ -1148,15 +999,19 @@ def test_create_job_with_multiple_new_inputs( algorithm_with_multiple_inputs, ): # configure multiple inputs - algorithm_with_multiple_inputs.algorithm.inputs.set( - [ + interface = AlgorithmInterfaceFactory( + inputs=[ algorithm_with_multiple_inputs.ci_json_in_db_with_schema, algorithm_with_multiple_inputs.ci_existing_img, algorithm_with_multiple_inputs.ci_str, algorithm_with_multiple_inputs.ci_bool, algorithm_with_multiple_inputs.ci_json_file, algorithm_with_multiple_inputs.ci_img_upload, - ] + ], + outputs=[ComponentInterfaceFactory()], + ) + algorithm_with_multiple_inputs.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} ) assert ComponentInterfaceValue.objects.count() == 0 @@ -1213,7 +1068,7 @@ def test_create_job_with_multiple_new_inputs( assert sorted( [ int.pk - for int in algorithm_with_multiple_inputs.algorithm.inputs.all() + for int in algorithm_with_multiple_inputs.algorithm.default_interface.inputs.all() ] ) == sorted([civ.interface.pk for civ in job.inputs.all()]) @@ -1240,14 +1095,18 @@ def test_create_job_with_existing_inputs( algorithm_with_multiple_inputs, ): # configure multiple inputs - algorithm_with_multiple_inputs.algorithm.inputs.set( - [ + interface = AlgorithmInterfaceFactory( + inputs=[ algorithm_with_multiple_inputs.ci_json_in_db_with_schema, algorithm_with_multiple_inputs.ci_existing_img, algorithm_with_multiple_inputs.ci_str, algorithm_with_multiple_inputs.ci_bool, algorithm_with_multiple_inputs.ci_json_file, - ] + ], + outputs=[ComponentInterfaceFactory()], + ) + algorithm_with_multiple_inputs.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} ) civ1, civ2, civ3, civ4, civ5 = self.create_existing_civs( @@ -1314,13 +1173,17 @@ def test_create_job_is_idempotent( algorithm_with_multiple_inputs, ): # configure multiple inputs - algorithm_with_multiple_inputs.algorithm.inputs.set( - [ + interface = AlgorithmInterfaceFactory( + inputs=[ algorithm_with_multiple_inputs.ci_str, algorithm_with_multiple_inputs.ci_bool, algorithm_with_multiple_inputs.ci_existing_img, algorithm_with_multiple_inputs.ci_json_in_db_with_schema, - ] + ], + outputs=[ComponentInterfaceFactory()], + ) + algorithm_with_multiple_inputs.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} ) civ1, civ2, civ3, civ4, civ5 = self.create_existing_civs( interface_data=algorithm_with_multiple_inputs @@ -1378,8 +1241,12 @@ def test_create_job_with_faulty_file_input( algorithm_with_multiple_inputs, ): # configure file input - algorithm_with_multiple_inputs.algorithm.inputs.set( - [algorithm_with_multiple_inputs.ci_json_file] + interface = AlgorithmInterfaceFactory( + inputs=[algorithm_with_multiple_inputs.ci_json_file], + outputs=[ComponentInterfaceFactory()], + ) + algorithm_with_multiple_inputs.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} ) file_upload = UserUploadFactory( filename="file.json", creator=algorithm_with_multiple_inputs.editor @@ -1425,8 +1292,12 @@ def test_create_job_with_faulty_json_input( django_capture_on_commit_callbacks, algorithm_with_multiple_inputs, ): - algorithm_with_multiple_inputs.algorithm.inputs.set( - [algorithm_with_multiple_inputs.ci_json_in_db_with_schema] + interface = AlgorithmInterfaceFactory( + inputs=[algorithm_with_multiple_inputs.ci_json_in_db_with_schema], + outputs=[ComponentInterfaceFactory()], + ) + algorithm_with_multiple_inputs.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} ) response = self.create_job( client=client, @@ -1455,8 +1326,12 @@ def test_create_job_with_faulty_image_input( django_capture_on_commit_callbacks, algorithm_with_multiple_inputs, ): - algorithm_with_multiple_inputs.algorithm.inputs.set( - [algorithm_with_multiple_inputs.ci_img_upload] + interface = AlgorithmInterfaceFactory( + inputs=[algorithm_with_multiple_inputs.ci_img_upload], + outputs=[ComponentInterfaceFactory()], + ) + algorithm_with_multiple_inputs.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} ) user_upload = create_upload_from_file( creator=algorithm_with_multiple_inputs.editor, @@ -1508,11 +1383,11 @@ def test_create_job_with_multiple_faulty_existing_image_inputs( ] ci.save() - algorithm_with_multiple_inputs.algorithm.inputs.set( - [ - ci1, - ci2, - ] + interface = AlgorithmInterfaceFactory( + inputs=[ci1, ci2], outputs=[ComponentInterfaceFactory()] + ) + algorithm_with_multiple_inputs.algorithm.interfaces.add( + interface, through_defaults={"is_default": True} ) assert ComponentInterfaceValue.objects.count() == 0 @@ -1665,7 +1540,10 @@ def test_job_time_limit(client): ci = ComponentInterfaceFactory( kind=InterfaceKind.InterfaceKindChoices.ANY, store_in_database=True ) - algorithm.inputs.set([ci]) + interface = AlgorithmInterfaceFactory( + inputs=[ci], outputs=[ComponentInterfaceFactory()] + ) + algorithm.interfaces.add(interface, through_defaults={"is_default": True}) response = get_view_for_user( viewname="algorithms:job-create", @@ -1673,6 +1551,7 @@ def test_job_time_limit(client): method=client.post, reverse_kwargs={ "slug": algorithm.slug, + "interface": algorithm.default_interface.pk, }, user=user, follow=True, @@ -1712,7 +1591,10 @@ def test_job_gpu_type_set(client, settings): ci = ComponentInterfaceFactory( kind=InterfaceKind.InterfaceKindChoices.ANY, store_in_database=True ) - algorithm.inputs.set([ci]) + interface = AlgorithmInterfaceFactory( + inputs=[ci], outputs=[ComponentInterfaceFactory()] + ) + algorithm.interfaces.add(interface, through_defaults={"is_default": True}) response = get_view_for_user( viewname="algorithms:job-create", @@ -1720,6 +1602,7 @@ def test_job_gpu_type_set(client, settings): method=client.post, reverse_kwargs={ "slug": algorithm.slug, + "interface": algorithm.default_interface.pk, }, user=user, follow=True, @@ -1802,9 +1685,18 @@ def test_job_create_view_for_verified_users_only(client): alg.add_user(user) alg.add_user(editor) + interface = AlgorithmInterfaceFactory( + inputs=[ComponentInterfaceFactory()], + outputs=[ComponentInterfaceFactory()], + ) + alg.interfaces.add(interface, through_defaults={"is_default": True}) + response = get_view_for_user( viewname="algorithms:job-create", - reverse_kwargs={"slug": alg.slug}, + reverse_kwargs={ + "slug": alg.slug, + "interface": alg.default_interface.pk, + }, client=client, user=user, ) @@ -1812,7 +1704,10 @@ def test_job_create_view_for_verified_users_only(client): response2 = get_view_for_user( viewname="algorithms:job-create", - reverse_kwargs={"slug": alg.slug}, + reverse_kwargs={ + "slug": alg.slug, + "interface": alg.default_interface.pk, + }, client=client, user=editor, ) @@ -1910,7 +1805,10 @@ def test_job_create_denied_for_same_input_model_and_image(client): ci = ComponentInterfaceFactory( kind=InterfaceKind.InterfaceKindChoices.IMAGE ) - alg.inputs.set([ci]) + interface = AlgorithmInterfaceFactory( + inputs=[ci], outputs=[ComponentInterfaceFactory()] + ) + alg.interfaces.add(interface, through_defaults={"is_default": True}) ai = AlgorithmImageFactory( algorithm=alg, is_manifest_valid=True, @@ -1933,6 +1831,7 @@ def test_job_create_denied_for_same_input_model_and_image(client): method=client.post, reverse_kwargs={ "slug": alg.slug, + "interface": alg.default_interface.pk, }, user=creator, data={ @@ -2250,3 +2149,79 @@ def test_algorithm_interfaces_list_queryset(client): assert iots[1] in response.context["object_list"] assert iots[2] not in response.context["object_list"] assert iots[3] not in response.context["object_list"] + + +@pytest.mark.django_db +def test_interface_select_for_job_view_permission(client): + verified_user, unverified_user = UserFactory.create_batch(2) + VerificationFactory(user=verified_user, is_verified=True) + alg = AlgorithmFactory() + alg.add_user(verified_user) + alg.add_user(unverified_user) + + interface1 = AlgorithmInterfaceFactory( + inputs=[ComponentInterfaceFactory()], + outputs=[ComponentInterfaceFactory()], + ) + interface2 = AlgorithmInterfaceFactory( + inputs=[ComponentInterfaceFactory()], + outputs=[ComponentInterfaceFactory()], + ) + alg.interfaces.set([interface1, interface2]) + + response = get_view_for_user( + viewname="algorithms:job-interface-select", + reverse_kwargs={"slug": alg.slug}, + client=client, + user=unverified_user, + ) + assert response.status_code == 403 + + response = get_view_for_user( + viewname="algorithms:job-interface-select", + reverse_kwargs={"slug": alg.slug}, + client=client, + user=verified_user, + ) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_interface_select_automatic_redirect(client): + verified_user = UserFactory() + VerificationFactory(user=verified_user, is_verified=True) + alg = AlgorithmFactory() + alg.add_user(verified_user) + + interface = AlgorithmInterfaceFactory( + inputs=[ComponentInterfaceFactory()], + outputs=[ComponentInterfaceFactory()], + ) + alg.interfaces.add(interface, through_defaults={"is_default": True}) + + # with just 1 interface, user gets redirected immediately + response = get_view_for_user( + viewname="algorithms:job-interface-select", + reverse_kwargs={"slug": alg.slug}, + client=client, + user=verified_user, + ) + assert response.status_code == 302 + assert response.url == reverse( + "algorithms:job-create", + kwargs={"slug": alg.slug, "interface": interface.pk}, + ) + + # with more than 1 interfaces, user has to choose the interface + interface2 = AlgorithmInterfaceFactory( + inputs=[ComponentInterfaceFactory()], + outputs=[ComponentInterfaceFactory()], + ) + alg.interfaces.add(interface2) + response = get_view_for_user( + viewname="algorithms:job-interface-select", + reverse_kwargs={"slug": alg.slug}, + client=client, + user=verified_user, + ) + assert response.status_code == 200 diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 14ecdcdd37..a46b213262 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -21,6 +21,7 @@ from tests.algorithms_tests.factories import ( AlgorithmFactory, AlgorithmImageFactory, + AlgorithmInterfaceFactory, AlgorithmModelFactory, ) from tests.cases_tests import RESOURCE_PATH @@ -480,7 +481,10 @@ def algorithm_with_image_and_model_and_two_inputs(): ci1, ci2 = ComponentInterfaceFactory.create_batch( 2, kind=ComponentInterface.Kind.STRING ) - alg.inputs.set([ci1, ci2]) + interface = AlgorithmInterfaceFactory( + inputs=[ci1, ci2], outputs=[ComponentInterfaceFactory()] + ) + alg.interfaces.add(interface, through_defaults={"is_default": True}) civs = [ ComponentInterfaceValueFactory(interface=ci1, value="foo"), ComponentInterfaceValueFactory(interface=ci2, value="bar"),