Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable interface selection for algorithm job #3753

Draft
wants to merge 11 commits into
base: feat_optional_inputs
Choose a base branch
from
1 change: 1 addition & 0 deletions app/grandchallenge/algorithms/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ class JobAdmin(GuardedModelAdmin):
"task_on_success",
"task_on_failure",
"runtime_metrics",
"algorithm_interface",
)
search_fields = (
"creator__username",
Expand Down
58 changes: 54 additions & 4 deletions app/grandchallenge/algorithms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@
TextInput,
URLField,
)
from django.forms.widgets import MultipleHiddenInput, PasswordInput
from django.forms.widgets import (
MultipleHiddenInput,
PasswordInput,
RadioSelect,
)
from django.urls import Resolver404, resolve
from django.utils.functional import cached_property
from django.utils.html import format_html
Expand Down Expand Up @@ -116,10 +120,18 @@ class JobCreateForm(SaveFormInitMixin, Form):
creator = ModelChoiceField(
queryset=None, disabled=True, required=False, widget=HiddenInput
)
algorithm_interface = ModelChoiceField(
queryset=None,
disabled=True,
required=True,
widget=HiddenInput,
)

def __init__(self, *args, algorithm, user, **kwargs):
def __init__(self, *args, algorithm, user, interface, **kwargs):
super().__init__(*args, **kwargs)

self._algorithm = algorithm

self.helper = FormHelper()

self._user = user
Expand All @@ -128,7 +140,11 @@ def __init__(self, *args, algorithm, user, **kwargs):
)
self.fields["creator"].initial = self._user

self._algorithm = algorithm
self.fields["algorithm_interface"].queryset = (
self._algorithm.interfaces.all()
)
self.fields["algorithm_interface"].initial = interface

self._algorithm_image = self._algorithm.active_image

active_model = self._algorithm.active_model
Expand All @@ -145,7 +161,7 @@ def __init__(self, *args, algorithm, user, **kwargs):
)
self.fields["algorithm_model"].initial = active_model

for inp in self._algorithm.inputs.all():
for inp in interface.inputs.all():
prefixed_interface_slug = (
f"{INTERFACE_FORM_FIELD_PREFIX}{inp.slug}"
)
Expand Down Expand Up @@ -189,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"],
):
Expand Down Expand Up @@ -1427,3 +1444,36 @@ def save(self):
)

return interface


class JobInterfaceSelectForm(SaveFormInitMixin, Form):
algorithm_interface = ModelChoiceField(
queryset=None,
required=True,
help_text="Select an input-output combination to use for this job.",
widget=RadioSelect,
)

def __init__(self, *args, algorithm, **kwargs):
super().__init__(*args, **kwargs)

self._algorithm = algorithm

self.fields["algorithm_interface"].queryset = (
self._algorithm.interfaces.all()
)
self.fields["algorithm_interface"].initial = (
self._algorithm.default_interface
)
self.fields["algorithm_interface"].widget.choices = {
(
interface.pk,
format_html(
"<div>Inputs: {inputs}</div>"
"<div class='mb-3'>Outputs: {outputs}</div>",
inputs=oxford_comma(interface.inputs.all()),
outputs=oxford_comma(interface.outputs.all()),
),
)
for interface in self._algorithm.interfaces.all()
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,14 @@ class Migration(migrations.Migration):
name="unique_algorithm_interface_combination",
),
),
migrations.AddField(
model_name="job",
name="algorithm_interface",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="algorithms.algorithminterface",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.db import migrations


def add_algorithm_interfaces_to_jobs(apps, _schema_editor):
Algorithm = apps.get_model("algorithms", "Algorithm") # noqa: N806
Job = apps.get_model("algorithms", "Job") # noqa: N806

for algorithm in Algorithm.objects.prefetch_related("interfaces").all():
default_interface = algorithm.interfaces.get(
algorithmalgorithminterface__is_default=True
)
jobs = Job.objects.filter(algorithm_image__algorithm=algorithm)
amickan marked this conversation as resolved.
Show resolved Hide resolved
for job in jobs:
job.algorithm_interface = default_interface
jobs.bulk_update(jobs, ["algorithm_interface"])
amickan marked this conversation as resolved.
Show resolved Hide resolved


class Migration(migrations.Migration):
dependencies = [
(
"algorithms",
"0064_create_algorithm_interfaces",
),
]

operations = [
migrations.RunPython(add_algorithm_interfaces_to_jobs, elidable=True),
]
32 changes: 23 additions & 9 deletions app/grandchallenge/algorithms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.exceptions import (
MultipleObjectsReturned,
ObjectDoesNotExist,
ValidationError,
)
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Count, Q, Sum
Expand Down Expand Up @@ -50,6 +54,7 @@
public_s3_storage,
)
from grandchallenge.core.templatetags.bleach import md2html
from grandchallenge.core.templatetags.remove_whitespace import oxford_comma
from grandchallenge.core.utils.access_requests import (
AccessRequestHandlingOptions,
process_access_request,
Expand Down Expand Up @@ -106,6 +111,9 @@ class AlgorithmInterface(UUIDModel):

objects = AlgorithmInterfaceManager()

def __str__(self):
return f"Inputs: {oxford_comma(self.inputs.all())} \n Outputs: {oxford_comma(self.outputs.all())}"

def delete(self, *args, **kwargs):
raise ValidationError("AlgorithmInterfaces cannot be deleted.")

Expand Down Expand Up @@ -507,11 +515,14 @@ def default_workstation(self):
@cached_property
def default_interface(self):
try:
return self.interfaces.get(
algorithmalgorithminterface__is_default=True
)
except ObjectDoesNotExist:
return None
return self.interfaces.get()
except MultipleObjectsReturned:
try:
return self.interfaces.get(
algorithmalgorithminterface__is_default=True
)
except ObjectDoesNotExist:
return None
amickan marked this conversation as resolved.
Show resolved Hide resolved

def is_editor(self, user):
return user.groups.filter(pk=self.editors_group.pk).exists()
Expand Down Expand Up @@ -961,13 +972,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
Expand Down Expand Up @@ -1072,6 +1083,9 @@ class Job(CIVForObjectMixin, ComponentJob):
algorithm_model = models.ForeignKey(
AlgorithmModel, on_delete=models.PROTECT, null=True, blank=True
)
algorithm_interface = models.ForeignKey(
AlgorithmInterface, on_delete=models.PROTECT, null=True, blank=True
)
amickan marked this conversation as resolved.
Show resolved Hide resolved
creator = models.ForeignKey(
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
)
Expand Down Expand Up @@ -1125,7 +1139,7 @@ def inputs_complete(self):
# check if all inputs are present and if they all have a value
return {
civ.interface for civ in self.inputs.all() if civ.has_value
} == {*self.algorithm_image.algorithm.inputs.all()}
} == {*self.algorithm_interface.inputs.all()}

@cached_property
def rendered_result_text(self) -> str:
Expand Down
1 change: 1 addition & 0 deletions app/grandchallenge/algorithms/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@

{% if "execute_algorithm" in algorithm_perms and object.active_image %}
<a class="nav-link"
href="{% url 'algorithms:job-create' slug=object.slug %}">
href="{% url 'algorithms:job-interface-select' slug=object.slug %}">
<i class="fas fa-file-import fa-fw"></i>&nbsp;Try-out Algorithm
</a>
{% endif %}
Expand Down Expand Up @@ -118,7 +118,7 @@

{% if object.public and not object.public_test_case %}
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle mr-1"></i> Please upload a public test case for your latest algorithm image <a href="{% url 'algorithms:job-create' slug=object.slug %}">here</a>.
<i class="fas fa-exclamation-triangle mr-1"></i> Please upload a public test case for your latest algorithm image <a href="{% url 'algorithms:job-interface-select' slug=object.slug %}">here</a>.
</div>
{% endif %}
{% endif %}
Expand Down Expand Up @@ -618,7 +618,7 @@ <h5 class="modal-title">Make this algorithm public</h5>
<div class="col-12 d-flex justify-content-center">
<a type="button" class="btn btn-sm btn-secondary mr-1" href="{% url 'algorithms:update' slug=object.slug %}">Update Settings</a>
<a type="button" class="btn btn-sm btn-secondary mr-1" href="{% url 'algorithms:description-update' slug=object.slug %}">Update Description</a>
<a type="button" class="btn btn-sm btn-secondary" href="{% url 'algorithms:job-create' slug=object.slug %}">Add Test Case</a>
<a type="button" class="btn btn-sm btn-secondary" href="{% url 'algorithms:job-interface-select' slug=object.slug %}">Add Test Case</a>
</div>
<div class="col-12 d-flex justify-content-center">
<a type="button" class="btn btn-sm btn-primary {% if not object.mechanism or not object.summary or not object.public_test_case or not object.contact_email or not object.display_editors %}disabled{% endif %}" hx-post="{% url 'algorithms:publish' slug=object.slug %}" hx-vals='{"public": true}'>Publish algorithm</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ <h2>Results for {{ algorithm.title }}</h2>
{% if "execute_algorithm" in algorithm_perms and algorithm.active_image %}
<p>
<a class="btn btn-primary"
href="{% url 'algorithms:job-create' slug=algorithm.slug %}">
href="{% url 'algorithms:job-interface-select' slug=algorithm.slug %}">
<i class="fas fa-file-import fa-fw"></i>&nbsp;Try-out Algorithm
</a>
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ <h5 class="card-title">Creating inputs</h5>
<i class="text-success fa fa-check fa-2x"></i>
</div>
</div>
<a href="{% url 'algorithms:job-create' slug=object.algorithm_image.algorithm.slug %}"
<a href="{% url 'algorithms:job-interface-select' slug=object.algorithm_image.algorithm.slug %}"
class="stretched-link"></a>
</div>
</div>
Expand Down
12 changes: 11 additions & 1 deletion app/grandchallenge/algorithms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
EditorsUpdate,
JobCreate,
JobDetail,
JobInterfaceSelect,
JobProgressDetail,
JobsList,
JobStatusDetail,
Expand Down Expand Up @@ -133,7 +134,16 @@
name="model-update",
),
path("<slug>/jobs/", JobsList.as_view(), name="job-list"),
path("<slug>/jobs/create/", JobCreate.as_view(), name="job-create"),
path(
"<slug>/jobs/interface-select/",
JobInterfaceSelect.as_view(),
name="job-interface-select",
),
path(
"<slug>/<interface>/jobs/create/",
JobCreate.as_view(),
name="job-create",
),
amickan marked this conversation as resolved.
Show resolved Hide resolved
path("<slug>/jobs/<uuid:pk>/", JobDetail.as_view(), name="job-detail"),
path(
"<slug>/jobs/<uuid:pk>/status/",
Expand Down
Loading
Loading