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

Add create and list views for algorithm interfaces #3735

Merged
merged 12 commits into from
Dec 12, 2024
72 changes: 72 additions & 0 deletions app/grandchallenge/algorithms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@
from django.db.models import Count, Exists, Max, OuterRef, Q
from django.db.transaction import on_commit
from django.forms import (
BooleanField,
CharField,
Form,
HiddenInput,
ModelChoiceField,
ModelForm,
ModelMultipleChoiceField,
Select,
TextInput,
URLField,
Expand All @@ -44,6 +46,7 @@

from grandchallenge.algorithms.models import (
Algorithm,
AlgorithmAlgorithmInterface,
AlgorithmImage,
AlgorithmInterface,
AlgorithmModel,
Expand Down Expand Up @@ -1377,3 +1380,72 @@ def clean(self):
)

return cleaned_data


class AlgorithmInterfaceGetOrCreateForm(AlgorithmInterfaceBaseForm):
inputs = ModelMultipleChoiceField(
queryset=ComponentInterface.objects.exclude(
slug__in=[*NON_ALGORITHM_INTERFACES, "results-json-file"]
),
widget=Select2MultipleWidget,
)
outputs = ModelMultipleChoiceField(
queryset=ComponentInterface.objects.exclude(
slug__in=[*NON_ALGORITHM_INTERFACES, "results-json-file"]
),
widget=Select2MultipleWidget,
)
set_as_default = BooleanField(required=False)

class Meta(AlgorithmInterfaceBaseForm.Meta):
fields = (
*AlgorithmInterfaceBaseForm.Meta.fields,
"set_as_default",
)

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

if self._algorithm.needs_default_interface:
self.fields["set_as_default"].initial = True

def clean(self):
cleaned_data = super().clean()

if (
cleaned_data["existing_io"]
and AlgorithmAlgorithmInterface.objects.filter(
algorithm=self._algorithm,
interface=cleaned_data["existing_io"],
).exists()
):
raise ValidationError(
"Your algorithm already has an interface with these inputs "
"and outputs. If you would like to update the 'is_default' "
"property of the interface, you can do so by updating the "
"existing interface on the interface list."
)

return cleaned_data

def save(self):
io = (
self.cleaned_data["existing_io"]
if self.cleaned_data["existing_io"]
else super().save()
amickan marked this conversation as resolved.
Show resolved Hide resolved
)

if self.cleaned_data["set_as_default"]:
AlgorithmAlgorithmInterface.objects.filter(
algorithm=self._algorithm
).update(is_default=False)

self._algorithm.interfaces.add(
io,
through_defaults={
"is_default": self.cleaned_data["set_as_default"]
},
)

return io
4 changes: 4 additions & 0 deletions app/grandchallenge/algorithms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,10 @@ def default_workstation(self):

return w

@property
def needs_default_interface(self):
return not self.interfaces.exists()

def is_editor(self, user):
return user.groups.filter(pk=self.editors_group.pk).exists()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@

{% if "change_algorithm" in algorithm_perms %}
{% if perms.algorithms.add_algorithm %}
<a class="nav-link"
href="{% url 'algorithms:interface-list' slug=object.slug %}"
><i class="fas fa-sliders-h fa-fw"></i>&nbsp;Interfaces
</a>
<a class="nav-link" id="v-pills-templates-tab" data-toggle="pill"
href="#templates" role="tab"
aria-controls="v-pills-templates"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load remove_whitespace %}

{% block title %}
Interfaces - {{ algorithm.title }} - {{ block.super }}
{% endblock %}

{% block breadcrumbs %}
<ol class="breadcrumb">
<li class="breadcrumb-item"><a
href="{% url 'algorithms:list' %}">Algorithms</a>
</li>
<li class="breadcrumb-item"><a
href="{{ algorithm.get_absolute_url }}">{{ algorithm.title }}
</a>
</li>
<li class="breadcrumb-item active"
aria-current="page">Interfaces
</li>
</ol>
{% endblock %}

{% block content %}
<h2>Algorithm Interfaces for {{ algorithm }}</h2>

<p>
The following interfaces are configured for your algorithm:
</p>
<p><a class="btn btn-primary" href="{% url 'algorithms:interface-create' slug=algorithm.slug %}">Add new interface</a></p>

<div class="table-responsive">
<table class="table table-hover table-borderless">
<thead class="thead-light">
<th>Inputs</th>
<th>Outputs</th>
<th>Default</th>
</thead>
<tbody>
{% for object in object_list %}
<tr>
<td>{{ object.interface.inputs.all|oxford_comma }}</td>
<td>{{ object.interface.outputs.all|oxford_comma }}</td>
<td>{% if object.is_default %}<i class="fas fa-check-circle text-success mr-1"></i>{% else %}<i class="fas fa-times-circle text-danger mr-1"></i>{% endif %}</td>
</tr>
{% empty %}
<tr><td colspan="100%" class="text-center">This algorithm does not have any interfaces defined yet.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}

{% block title %}
Create Interface - {{ algorithm.title }} - {{ block.super }}
{% endblock %}

{% block breadcrumbs %}
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'algorithms:list' %}">Algorithms</a>
</li>
<li class="breadcrumb-item"><a
href="{{ algorithm.get_absolute_url }}">{{ algorithm.title }}
</a></li>
<li class="breadcrumb-item"><a
href="{% url 'algorithms:interface-list' slug=algorithm.slug %}">Interfaces
</a></li>
<li class="breadcrumb-item active"
aria-current="page">Create Interface
</li>
</ol>
{% endblock %}

{% block content %}

<h2>Create An Algorithm Interface</h2>
<br>
<p>
Create an interface for your algorithm: define any combination of inputs and outputs, and optionally mark the interface as default for the algorithm.
</p>
<p>
Please see the <a href="{% url 'components:component-interface-list-input' %}">list of input options</a> and the <a href="{% url 'components:component-interface-list-output' %}">
list of output options
</a> for more information and examples.
</p>
<p>
If you cannot find suitable inputs or outputs, please contact <a href="mailto:[email protected]">[email protected]</a>.
</p>

{% crispy form %}

{% endblock %}
12 changes: 12 additions & 0 deletions app/grandchallenge/algorithms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
AlgorithmImageTemplate,
AlgorithmImageUpdate,
AlgorithmImportView,
AlgorithmInterfaceForAlgorithmCreate,
AlgorithmInterfacesForAlgorithmList,
AlgorithmList,
AlgorithmModelCreate,
AlgorithmModelDetail,
Expand Down Expand Up @@ -50,6 +52,16 @@
AlgorithmDescriptionUpdate.as_view(),
name="description-update",
),
path(
"<slug>/interfaces/",
AlgorithmInterfacesForAlgorithmList.as_view(),
name="interface-list",
),
path(
"<slug>/interfaces/create/",
AlgorithmInterfaceForAlgorithmCreate.as_view(),
name="interface-create",
),
path(
"<slug>/repository/",
AlgorithmRepositoryUpdate.as_view(),
Expand Down
62 changes: 62 additions & 0 deletions app/grandchallenge/algorithms/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.mixins import (
AccessMixin,
PermissionRequiredMixin,
UserPassesTestMixin,
)
Expand Down Expand Up @@ -46,6 +47,7 @@
AlgorithmImageForm,
AlgorithmImageUpdateForm,
AlgorithmImportForm,
AlgorithmInterfaceGetOrCreateForm,
AlgorithmModelForm,
AlgorithmModelUpdateForm,
AlgorithmModelVersionControlForm,
Expand All @@ -61,7 +63,9 @@
)
from grandchallenge.algorithms.models import (
Algorithm,
AlgorithmAlgorithmInterface,
AlgorithmImage,
AlgorithmInterface,
AlgorithmModel,
AlgorithmPermissionRequest,
Job,
Expand Down Expand Up @@ -1091,3 +1095,61 @@ def get(self, *_, **__):
filename=f"{algorithm.slug}-template.zip",
content_type="application/zip",
)


class AlgorithmInterfacePermissionMixin(
VerificationRequiredMixin, AccessMixin
amickan marked this conversation as resolved.
Show resolved Hide resolved
):
@property
def algorithm(self):
return get_object_or_404(Algorithm, slug=self.kwargs["slug"])

def dispatch(self, request, *args, **kwargs):
if request.user.has_perm(
"change_algorithm", self.algorithm
) and request.user.has_perm("algorithms.add_algorithm"):
return super().dispatch(request, *args, **kwargs)
else:
return self.handle_no_permission()


class AlgorithmInterfaceForAlgorithmCreate(
AlgorithmInterfacePermissionMixin, CreateView
):
model = AlgorithmInterface
form_class = AlgorithmInterfaceGetOrCreateForm
success_message = "Algorithm interface successfully added"
amickan marked this conversation as resolved.
Show resolved Hide resolved

def get_success_url(self):
return reverse(
"algorithms:interface-list", kwargs={"slug": self.algorithm.slug}
)

def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update({"algorithm": self.algorithm})
return context

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({"algorithm": self.algorithm})
return kwargs


class AlgorithmInterfacesForAlgorithmList(
AlgorithmInterfacePermissionMixin, ListView
):
model = AlgorithmAlgorithmInterface

def get_queryset(self):
qs = super().get_queryset()
return qs.filter(algorithm=self.algorithm)

def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context.update(
{
"algorithm": self.algorithm,
}
)
return context
Loading
Loading