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
32 changes: 12 additions & 20 deletions app/grandchallenge/algorithms/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@

from django.contrib import admin
from django.contrib.admin import ModelAdmin
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count, Sum
from django.forms import ModelForm
from django.utils.html import format_html
from guardian.admin import GuardedModelAdmin

from grandchallenge.algorithms.forms import AlgorithmInterfaceBaseForm
from grandchallenge.algorithms.models import (
Algorithm,
AlgorithmAlgorithmInterface,
Expand Down Expand Up @@ -52,12 +51,17 @@ class Meta:

@admin.register(Algorithm)
class AlgorithmAdmin(GuardedModelAdmin):
readonly_fields = ("algorithm_forge_json", "inputs", "outputs")
readonly_fields = (
"algorithm_forge_json",
"default_interface",
"inputs",
"outputs",
)
list_display = (
"title",
"created",
"public",
"default_io",
"default_interface",
"time_limit",
"job_requires_gpu_type",
"job_requires_memory_gb",
Expand All @@ -79,11 +83,6 @@ def algorithm_forge_json(obj):
"<pre>{json_desc}</pre>", json_desc=json.dumps(json_desc, indent=2)
)

def default_io(self, obj):
return AlgorithmAlgorithmInterface.objects.get(
algorithm=obj, is_default=True
)

def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.annotate(
Expand Down Expand Up @@ -244,16 +243,6 @@ class AlgorithmModelAdmin(GuardedModelAdmin):
readonly_fields = ("creator", "algorithm", "sha256", "size_in_storage")


class AlgorithmInterfaceAdminForm(AlgorithmInterfaceBaseForm):
def clean(self):
cleaned_data = super().clean()
if cleaned_data["existing_io"]:
raise ValidationError(
"An AlgorithmIO with the same combination of inputs and outputs already exists."
)
return cleaned_data


@admin.register(AlgorithmInterface)
class AlgorithmInterfaceAdmin(GuardedModelAdmin):
list_display = (
Expand All @@ -262,10 +251,10 @@ class AlgorithmInterfaceAdmin(GuardedModelAdmin):
"algorithm_outputs",
)
search_fields = (
"pk",
"inputs__slug",
"outputs__slug",
)
form = AlgorithmInterfaceAdminForm

def algorithm_inputs(self, obj):
return oxford_comma(obj.inputs.all())
Expand All @@ -279,6 +268,9 @@ def has_change_permission(self, request, obj=None):
def has_delete_permission(self, request, obj=None):
return False

def has_add_permission(self, request, obj=None):
return False


@admin.register(AlgorithmAlgorithmInterface)
class AlgorithmAlgorithmInterfaceAdmin(GuardedModelAdmin):
Expand Down
68 changes: 59 additions & 9 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,12 +46,12 @@

from grandchallenge.algorithms.models import (
Algorithm,
AlgorithmAlgorithmInterface,
AlgorithmImage,
AlgorithmInterface,
AlgorithmModel,
AlgorithmPermissionRequest,
Job,
get_existing_interface_for_inputs_and_outputs,
)
from grandchallenge.algorithms.serializers import (
AlgorithmImageSerializer,
Expand Down Expand Up @@ -1345,10 +1347,35 @@ def clean_algorithm_model(self):
return algorithm_model


class AlgorithmInterfaceBaseForm(SaveFormInitMixin, ModelForm):
class AlgorithmInterfaceForm(SaveFormInitMixin, ModelForm):
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:
model = AlgorithmInterface
fields = ("inputs", "outputs")
fields = (
"inputs",
"outputs",
"set_as_default",
)

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

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

def clean(self):
cleaned_data = super().clean()
Expand All @@ -1369,11 +1396,34 @@ def clean(self):
f"{oxford_comma(duplicate_interfaces)} present in both"
)

cleaned_data["existing_io"] = (
get_existing_interface_for_inputs_and_outputs(
inputs=inputs,
outputs=outputs,
)
return cleaned_data

def save(self):
interface = AlgorithmInterface.objects.create(
inputs=self.cleaned_data["inputs"],
outputs=self.cleaned_data["outputs"],
)

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

matched_rows = AlgorithmAlgorithmInterface.objects.filter(
algorithm=self._algorithm, interface=interface
).update(is_default=self.cleaned_data["set_as_default"])

if matched_rows == 0:
self._algorithm.interfaces.add(
interface,
through_defaults={
"is_default": self.cleaned_data["set_as_default"]
},
)
elif matched_rows > 1:
raise RuntimeError(
"This algorithm and interface are associated "
"with each other more than once."
)

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

return w

@cached_property
def default_interface(self):
try:
return self.interfaces.get(
algorithmalgorithminterface__is_default=True
)
except ObjectDoesNotExist:
return None

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
Loading
Loading