Skip to content

Commit

Permalink
Add create and detail view for models
Browse files Browse the repository at this point in the history
  • Loading branch information
amickan committed May 27, 2024
1 parent ccca44a commit 79e4da5
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 4 deletions.
18 changes: 18 additions & 0 deletions app/grandchallenge/algorithms/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
AlgorithmImage,
AlgorithmImageGroupObjectPermission,
AlgorithmImageUserObjectPermission,
AlgorithmModel,
AlgorithmModelGroupObjectPermission,
AlgorithmModelUserObjectPermission,
AlgorithmPermissionRequest,
AlgorithmUserObjectPermission,
Job,
Expand Down Expand Up @@ -110,6 +113,15 @@ class AlgorithmPermissionRequestAdmin(GuardedModelAdmin):
readonly_fields = ("user", "algorithm")


@admin.register(AlgorithmModel)
class AlgorithmModelAdmin(GuardedModelAdmin):
exclude = ("model",)
list_display = ("algorithm", "created", "is_desired_version", "comment")
list_filter = ("is_desired_version",)
search_fields = ("algorithm__title", "comment")
readonly_fields = ("creator", "algorithm", "sha256", "size_in_storage")


admin.site.register(AlgorithmUserObjectPermission, UserObjectPermissionAdmin)
admin.site.register(AlgorithmGroupObjectPermission, GroupObjectPermissionAdmin)
admin.site.register(AlgorithmImage, ComponentImageAdmin)
Expand All @@ -121,3 +133,9 @@ class AlgorithmPermissionRequestAdmin(GuardedModelAdmin):
)
admin.site.register(JobUserObjectPermission, UserObjectPermissionAdmin)
admin.site.register(JobGroupObjectPermission, GroupObjectPermissionAdmin)
admin.site.register(
AlgorithmModelUserObjectPermission, UserObjectPermissionAdmin
)
admin.site.register(
AlgorithmModelGroupObjectPermission, GroupObjectPermissionAdmin
)
77 changes: 76 additions & 1 deletion app/grandchallenge/algorithms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
)
from dal import autocomplete
from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.files.base import ContentFile
from django.core.validators import RegexValidator
Expand Down Expand Up @@ -41,14 +42,18 @@
from grandchallenge.algorithms.models import (
Algorithm,
AlgorithmImage,
AlgorithmModel,
AlgorithmPermissionRequest,
Job,
)
from grandchallenge.algorithms.serializers import (
AlgorithmImageSerializer,
AlgorithmSerializer,
)
from grandchallenge.algorithms.tasks import import_remote_algorithm_image
from grandchallenge.algorithms.tasks import (
assign_algorithm_model_from_upload,
import_remote_algorithm_image,
)
from grandchallenge.components.form_fields import InterfaceFormField
from grandchallenge.components.forms import ContainerImageForm
from grandchallenge.components.models import (
Expand All @@ -72,6 +77,8 @@
from grandchallenge.hanging_protocols.models import VIEW_CONTENT_SCHEMA
from grandchallenge.reader_studies.models import ReaderStudy
from grandchallenge.subdomains.utils import reverse, reverse_lazy
from grandchallenge.uploads.models import UserUpload
from grandchallenge.uploads.widgets import UserUploadSingleWidget
from grandchallenge.workstations.models import Workstation


Expand Down Expand Up @@ -1161,3 +1168,71 @@ def _save_new_algorithm_image(self):
}
).apply_async
)


class AlgorithmModelForm(SaveFormInitMixin, ModelForm):
algorithm = ModelChoiceField(widget=HiddenInput(), queryset=None)
user_upload = ModelChoiceField(
widget=UserUploadSingleWidget(
allowed_file_types=[
"application/x-gzip",
"application/gzip",
]
),
label="Algorithm Model",
queryset=None,
help_text=(
".tar.gz file of the algorithm model that will be extracted"
" to /opt/ml/model/ during inference"
),
)
creator = ModelChoiceField(
widget=HiddenInput(),
queryset=(
get_user_model()
.objects.exclude(username=settings.ANONYMOUS_USER_NAME)
.filter(verification__is_verified=True)
),
)

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

self.fields["user_upload"].queryset = get_objects_for_user(
user,
"uploads.change_userupload",
).filter(status=UserUpload.StatusChoices.COMPLETED)

self.fields["creator"].initial = user
self.fields["algorithm"].queryset = Algorithm.objects.filter(
pk=algorithm.pk
)
self.fields["algorithm"].initial = algorithm

def clean_creator(self):
creator = self.cleaned_data["creator"]

if AlgorithmModel.objects.filter(
import_status=ImportStatusChoices.INITIALIZED,
creator=creator,
).exists():
self.add_error(
None,
"You have an existing model importing, please wait for it to complete",
)

return creator

def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
on_commit(
assign_algorithm_model_from_upload.signature(
kwargs={"algorithm_model_pk": instance.pk},
immutable=True,
).apply_async
)
return instance

class Meta:
model = AlgorithmModel
fields = ("algorithm", "user_upload", "creator", "comment")
22 changes: 22 additions & 0 deletions app/grandchallenge/algorithms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,28 @@ def delete_model_file(self):
Bucket=self.model.storage.bucket_name, Key=self.model.name
)

def get_absolute_url(self):
return reverse(
"algorithms:model-detail",
kwargs={"slug": self.algorithm.slug, "pk": self.pk},
)

@property
def import_status_context(self):
if self.import_status == ImportStatusChoices.COMPLETED:
return "success"
elif self.import_status in {
ImportStatusChoices.FAILED,
ImportStatusChoices.CANCELLED,
}:
return "danger"
else:
return "info"

@property
def import_in_progress(self):
return self.import_status == ImportStatusChoices.INITIALIZED


class AlgorithmModelUserObjectPermission(UserObjectPermissionBase):
content_object = models.ForeignKey(
Expand Down
15 changes: 12 additions & 3 deletions app/grandchallenge/algorithms/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,8 +483,8 @@ def set_credits_per_job():
algorithm.save(update_fields=("credits_per_job",))


@transaction.atomic()
@shared_task(**settings.CELERY_TASK_DECORATOR_KWARGS["acks-late-micro-short"])
@transaction.atomic
def assign_algorithm_model_from_upload(*, algorithm_model_pk, retries=0):
from grandchallenge.algorithms.models import AlgorithmModel

Expand Down Expand Up @@ -515,14 +515,23 @@ def assign_algorithm_model_from_upload(*, algorithm_model_pk, retries=0):
current_model.user_upload.copy_object(to_field=current_model.model)

sha256 = get_object_sha256(current_model.model)
if AlgorithmModel.objects.filter(sha256=sha256).exists():
if (
AlgorithmModel.objects.filter(sha256=sha256)
.exclude(pk=current_model.pk)
.exists()
):
current_model.import_status = ImportStatusChoices.FAILED
current_model.status = (
"Algorithm model with this sha256 already exists."
)
current_model.save()
current_model.user_upload.delete()

current_model.delete_model_file()
current_model.model = None
current_model.save()

current_model.user_upload.delete()

return

current_model.sha256 = sha256
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{% extends "base.html" %}
{% load static %}
{% load crispy_forms_tags %}
{% load url %}
{% load guardian_tags %}
{% load humanize %}
{% load user_profile_link from profiles %}
{% load naturaldelta %}

{% block title %}
Algorithm Model - {{ 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="{{ object.algorithm.get_absolute_url }}">{{ object.algorithm.title }}
</a>
</li>
<li class="breadcrumb-item active"
aria-current="page">{{ object }}
</li>
</ol>
{% endblock %}

{% block content %}
<h2>Algorithm Model</h2>

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

<span class="badge p-2 my-2 {% if object.is_desired_version %} badge-success {% else %} badge-danger {% endif %}">{% if object.is_desired_version %}<i class="fa fa-check-circle mr-1"></i> Active model for this algorithm {% else %} <i class="fa fa-times-circle mr-1"></i> Inactive {% endif %}</span>

<dl class="inline">
<dt>ID</dt>
<dd>{{ object.pk }}</dd>

<dt>Algorithm</dt>
<dd>
<a href="{{ object.algorithm.get_absolute_url }}">{{ object.algorithm.title }}</a>
</dd>

<dt>Creator</dt>
<dd>
{{ object.creator|user_profile_link }}
</dd>

<dt>Created</dt>
<dd>{{ object.created }}</dd>

{% if object.model %}
<dt>Model</dt>
<dd></dd>
<dd>{{ object.model.name }}</dd>

<dt>Model Size</dt>
<dd></dd>
<dd>{{ object.model.size|naturalsize }}</dd>
{% endif %}

{% if object.sha256 %}
<dt>SHA256</dt>
<dd>{{ object.sha256 }}</dd>
{% endif %}

<dt>Import Status</dt>
<dd>
<span class="badge badge-{{ object.import_status_context }}">
{% if object.import_in_progress %}
<span class="spinner-border spinner-border-sm" role="status"
aria-hidden="true"></span>
{% endif %}
{{ object.get_import_status_display }}
</span>
</dd>

{% if object.status %}
<dt>Validation Errors</dt>
<dd>{{ object.status }}</dd>
{% endif %}

<dt>Comment</dt>
<dd>{{ object.comment }}</dd>
</dl>

{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load url %}

{% block title %}
Create An Algorithm Model - {{ 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">Create model
</li>
</ol>
{% endblock %}

{% block content %}

<h2>Create An Algorithm Model</h2>

<p>
Upload a model that will be extracted to <code>/opt/ml/model/</code> during inference.
</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 @@ -10,6 +10,8 @@
AlgorithmImageUpdate,
AlgorithmImportView,
AlgorithmList,
AlgorithmModelCreate,
AlgorithmModelDetail,
AlgorithmPermissionRequestCreate,
AlgorithmPermissionRequestList,
AlgorithmPermissionRequestUpdate,
Expand Down Expand Up @@ -66,6 +68,16 @@
AlgorithmImageUpdate.as_view(),
name="image-update",
),
path(
"<slug>/models/<uuid:pk>/",
AlgorithmModelDetail.as_view(),
name="model-detail",
),
path(
"<slug>/models/create/",
AlgorithmModelCreate.as_view(),
name="model-create",
),
path("<slug>/jobs/", JobsList.as_view(), name="job-list"),
path("<slug>/jobs/create/", JobCreate.as_view(), name="job-create"),
path("<slug>/jobs/<uuid:pk>/", JobDetail.as_view(), name="job-detail"),
Expand Down
Loading

0 comments on commit 79e4da5

Please sign in to comment.