Skip to content

Commit

Permalink
Merge branch 'feat_optional_inputs' of github.com:comic/grand-challen…
Browse files Browse the repository at this point in the history
…ge.org into feat_optional_inputs
  • Loading branch information
amickan committed Dec 12, 2024
2 parents 0c65cf2 + 2f9a6b1 commit 5990745
Show file tree
Hide file tree
Showing 11 changed files with 502 additions and 49 deletions.
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

0 comments on commit 5990745

Please sign in to comment.