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 @@ -1435,3 +1452,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,47 @@
from django.db import migrations

from grandchallenge.algorithms.models import (
get_existing_interface_for_inputs_and_outputs,
)


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

jobs = (
Job.objects.select_related("algorithm_image__algorithm")
.prefetch_related("inputs", "outputs")
.all()
)
for job in jobs:
inputs = [input.interface for input in job.inputs.all()]
outputs = [output.interface for output in job.outputs.all()]

interface = get_existing_interface_for_inputs_and_outputs(
model=AlgorithmInterface, inputs=inputs, outputs=outputs
)
if not interface:
interface = AlgorithmInterface.objects.create()
interface.inputs.set(inputs)
interface.outputs.set(outputs)

job.algorithm_interface = interface
job.algorithm_image.algorithm.interfaces.add(interface)

jobs.bulk_update(jobs, ["algorithm_interface"])


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

operations = [
migrations.RunPython(add_algorithm_interfaces_to_jobs, elidable=True),
]
15 changes: 11 additions & 4 deletions app/grandchallenge/algorithms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,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 +107,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 @@ -967,13 +971,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 @@ -1078,6 +1082,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 @@ -1124,14 +1131,14 @@ def container(self):

@property
def output_interfaces(self):
return self.algorithm_image.algorithm.outputs
return self.algorithm_interface.outputs.all()

@cached_property
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
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 @@ -36,59 +36,64 @@
{% block content %}
<h2>Try-out Algorithm</h2>

{{ algorithm.job_create_page_markdown|md2html }}
{% if not algorithm.interfaces.all %}
<p>Your algorithm does not have any interfaces yet. You need to define at least one interface (i.e. input - output combination) before you can try it out.</p>
<p>To define an interface, navigate <a href="{% url 'algorithms:interface-list' slug=algorithm.slug %}">here</a>.</p>
{% else %}
{{ algorithm.job_create_page_markdown|md2html }}

{% get_obj_perms request.user for algorithm as "algorithm_perms" %}
{% get_obj_perms request.user for algorithm as "algorithm_perms" %}

{% if not algorithm.active_image %}
<p>
This algorithm is not ready to be used.
{% if 'change_algorithm' in algorithm_perms %}
Please upload a valid container image for this algorithm.
{% endif %}
</p>
{% elif form.jobs_limit < 1 %}
<p>
You have run out of credits to try this algorithm.
You can request more credits by sending an e-mail to
<a href="{{ 'mailto:[email protected]'|random_encode|safe }}" class="text-radboud">
[email protected]</a>.
</p>
{% else %}
<p>
Select the data that you would like to run the algorithm on.
</p>
<p>
{% if 'change_algorithm' in algorithm_perms %}
As an editor for this algorithm you can test and debug your algorithm in total {{ editors_job_limit }} times per unique algorithm image.
You share these credits with all other editors of this algorithm.
Once you have reached the limit, any extra jobs will be deducted from your personal algorithm credits,
of which you get {{ request.user.user_credit.credits }} per month.
{% else %}
You receive {{ request.user.user_credit.credits }} credits per month.
{% endif %}
Using this algorithm requires {{ algorithm.credits_per_job }}
credit{{ algorithm.credits_per_job|pluralize }} per job.
You can currently create up to {{ form.jobs_limit }} job{{ form.jobs_limit|pluralize }} for this algorithm.
</p>
{% if not algorithm.active_image %}
<p>
This algorithm is not ready to be used.
{% if 'change_algorithm' in algorithm_perms %}
Please upload a valid container image for this algorithm.
{% endif %}
</p>
{% elif form.jobs_limit < 1 %}
<p>
You have run out of credits to try this algorithm.
You can request more credits by sending an e-mail to
<a href="{{ 'mailto:[email protected]'|random_encode|safe }}" class="text-radboud">
[email protected]</a>.
</p>
{% else %}
<p>
Select the data that you would like to run the algorithm on.
</p>
<p>
{% if 'change_algorithm' in algorithm_perms %}
As an editor for this algorithm you can test and debug your algorithm in total {{ editors_job_limit }} times per unique algorithm image.
You share these credits with all other editors of this algorithm.
Once you have reached the limit, any extra jobs will be deducted from your personal algorithm credits,
of which you get {{ request.user.user_credit.credits }} per month.
{% else %}
You receive {{ request.user.user_credit.credits }} credits per month.
{% endif %}
Using this algorithm requires {{ algorithm.credits_per_job }}
credit{{ algorithm.credits_per_job|pluralize }} per job.
You can currently create up to {{ form.jobs_limit }} job{{ form.jobs_limit|pluralize }} for this algorithm.
</p>

<form method="POST">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" name="save" value="Submit" class="btn btn-primary" id="submit-id-save">
</form>
<form method="POST">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" name="save" value="Submit" class="btn btn-primary" id="submit-id-save">
</form>

<p>
By running this algorithm you agree to the
<a href="{% url 'policies:detail' slug='terms-of-service' %}"> General
Terms of Service</a>{% if algorithm.additional_terms_markdown %},
as well as this algorithm's specific Terms of Service:
{% else %}.
{% endif %}
</p>
<p>
By running this algorithm you agree to the
<a href="{% url 'policies:detail' slug='terms-of-service' %}"> General
Terms of Service</a>{% if algorithm.additional_terms_markdown %},
as well as this algorithm's specific Terms of Service:
{% else %}.
{% endif %}
</p>

{{ algorithm.additional_terms_markdown|md2html }}
{{ algorithm.additional_terms_markdown|md2html }}

{% endif %}
{% endif %}

{% endblock %}
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
Loading
Loading