Skip to content

Commit

Permalink
Add option for algorithm editors to mark old image as active (#2827)
Browse files Browse the repository at this point in the history
This is the second part of
DIAGNijmegen/rse-roadmap#223

It updates the `latest_executable_image` methods on `Algorithms`,
`Phases` and `Workstations` to take into account `is_desired_version`
and renames this property to `active_image`.

This also adds a button on the algorithm image detail view that enables
algorithm editors to mark an image of their choice as the desired
version for an algorithm and some small UI additions related to that.



![image](https://user-images.githubusercontent.com/30069334/234216527-ce0dee16-eb9b-445a-9fa8-8b712109ae9b.png)


![image](https://user-images.githubusercontent.com/30069334/234216595-2bbccb58-bec9-45a9-8d27-da337d5c3f86.png)

---------

Co-authored-by: James Meakin <[email protected]>
  • Loading branch information
amickan and jmsmkn authored Apr 25, 2023
1 parent 9fcff1c commit eea2757
Show file tree
Hide file tree
Showing 33 changed files with 549 additions and 137 deletions.
41 changes: 40 additions & 1 deletion app/grandchallenge/algorithms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,13 +555,52 @@ class AlgorithmImageUpdateForm(SaveFormInitMixin, ModelForm):

class Meta:
model = AlgorithmImage
fields = ("requires_gpu", "requires_memory_gb")
fields = ("requires_gpu", "requires_memory_gb", "comment")
labels = {"requires_gpu": "GPU Supported"}
help_texts = {
"requires_gpu": "If true, inference jobs for this container will be assigned a GPU"
}


class ImageActivateForm(Form):
algorithm_image = ModelChoiceField(queryset=AlgorithmImage.objects.none())

def __init__(
self,
*args,
user,
algorithm,
hide_algorithm_image_input=False,
**kwargs,
):
super().__init__(*args, **kwargs)
self.fields["algorithm_image"].queryset = get_objects_for_user(
user,
"algorithms.change_algorithmimage",
).filter(
algorithm=algorithm,
is_manifest_valid=True,
is_desired_version=False,
)

if hide_algorithm_image_input:
self.fields["algorithm_image"].widget = HiddenInput()

self.helper = FormHelper(self)
self.helper.layout.append(Submit("save", "Activate algorithm image"))
self.helper.form_action = reverse(
"algorithms:image-activate", kwargs={"slug": algorithm.slug}
)

def clean_algorithm_image(self):
algorithm_image = self.cleaned_data["algorithm_image"]

if algorithm_image.algorithm.image_upload_in_progress:
raise ValidationError("Image updating already in progress.")

return algorithm_image


class UsersForm(UserGroupForm):
role = "user"

Expand Down
29 changes: 21 additions & 8 deletions app/grandchallenge/algorithms/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ComponentInterface,
ComponentJob,
ComponentJobManager,
ImportStatusChoices,
)
from grandchallenge.core.guardian import get_objects_for_group
from grandchallenge.core.models import RequestBase, UUIDModel
Expand Down Expand Up @@ -342,17 +343,29 @@ def assign_workstation_permissions(self):
)

@cached_property
def latest_executable_image(self):
def active_image(self):
"""
Returns
-------
The most recent container image for this algorithm
The desired version for this algorithm or None
"""
return (
self.algorithm_container_images.executable_images()
.order_by("-created")
.first()
)
try:
return (
self.algorithm_container_images.executable_images()
.filter(is_desired_version=True)
.get()
)
except ObjectDoesNotExist:
return None

@property
def image_upload_in_progress(self):
return self.algorithm_container_images.filter(
import_status__in=(
ImportStatusChoices.STARTED,
ImportStatusChoices.QUEUED,
)
).exists()

@cached_property
def default_workstation(self):
Expand Down Expand Up @@ -469,7 +482,7 @@ def usage_chart(self):
@cached_property
def public_test_case(self):
try:
return self.latest_executable_image.job_set.filter(
return self.active_image.job_set.filter(
status=Job.SUCCESS, public=True
).exists()
except AttributeError:
Expand Down
6 changes: 3 additions & 3 deletions app/grandchallenge/algorithms/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,14 @@ def validate(self, data):
alg = data.pop("algorithm")
user = self.context["request"].user

if not alg.latest_executable_image:
if not alg.active_image:
raise serializers.ValidationError(
"Algorithm image is not ready to be used"
)
data["creator"] = user
data["algorithm_image"] = alg.latest_executable_image
data["algorithm_image"] = alg.active_image

jobs_limit = alg.latest_executable_image.algorithm.get_jobs_limit(
jobs_limit = alg.active_image.algorithm.get_jobs_limit(
user=data["creator"]
)
if jobs_limit is not None and jobs_limit < 1:
Expand Down
6 changes: 3 additions & 3 deletions app/grandchallenge/algorithms/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def retry_with_delay():
blocking_timeout=10,
):
create_algorithm_jobs(
algorithm_image=algorithm.latest_executable_image,
algorithm_image=algorithm.active_image,
civ_sets=[
{*ai.values.all()}
for ai in archive_items.prefetch_related(
Expand Down Expand Up @@ -452,9 +452,9 @@ def set_credits_per_job():
)

for algorithm in Algorithm.objects.all().iterator():
if algorithm.average_duration and algorithm.latest_executable_image:
if algorithm.average_duration and algorithm.active_image:
executor = Job(
algorithm_image=algorithm.latest_executable_image
algorithm_image=algorithm.active_image
).get_executor(backend=settings.COMPONENTS_DEFAULT_BACKEND)

cents_per_job = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
aria-controls="v-pills-containers"
aria-selected="false">
{# @formatter:off #}
<i class="fab fa-docker fa-fw"></i>&nbsp;Containers{% if not object.latest_executable_image %}&nbsp;
<i class="fab fa-docker fa-fw"></i>&nbsp;Containers{% if not object.active_image %}&nbsp;
<i class="fas fa-exclamation-triangle text-danger"></i>{% endif %}
{# @formatter:on #}
</a>
Expand All @@ -58,7 +58,7 @@
</a>
{% endif %}

{% if "execute_algorithm" in algorithm_perms and object.latest_executable_image %}
{% if "execute_algorithm" in algorithm_perms and object.active_image %}
<a class="nav-link"
href="{% url 'algorithms:job-create' slug=object.slug %}">
<i class="fas fa-file-import fa-fw"></i>&nbsp;Try-out Algorithm
Expand Down Expand Up @@ -152,14 +152,14 @@ <h3 class="my-3">About</h3>
<div class="col-9"><a href="mailto:{{ object.contact_email }}">{{ object.contact_email }}</a></div>
</div>
{% endif %}
{% if object.latest_executable_image %}
{% if object.active_image %}
<div class="row mb-2">
<div class="col-3 font-weight-bold">Version:</div>
<div class="col-9">{{ object.latest_executable_image.pk }}</div>
<div class="col-9">{{ object.active_image.pk }}</div>
</div>
<div class="row mb-2">
<div class="col-3 font-weight-bold">Last updated:</div>
<div class="col-9">{{ object.latest_executable_image.created }}</div>
<div class="col-9">{{ object.active_image.created }}</div>
</div>
{% endif %}
{% if object.publications.all %}
Expand Down Expand Up @@ -388,7 +388,7 @@ <h2>Permission Requests</h2>

<h2>Container Images</h2>

{% if not object.latest_executable_image %}
{% if not object.active_image %}
<p>
You need to link your algorithm to a GitHub repo and create a new tag,
or upload a valid algorithm container image.
Expand Down Expand Up @@ -422,6 +422,8 @@ <h2>Container Images</h2>
</a>
</p>

<p>To re-activate a previously uploaded container image, click on the info button next to it and then on "Make active image for algorithm". </p>

<ul class="list-unstyled">
{% for image in object.algorithm_container_images.all %}
<li>
Expand All @@ -442,7 +444,7 @@ <h2>Container Images</h2>
Container image from
<a href="{{ image.build.webhook_message.tag_url }}">{{ image.build.webhook_message.repo_name }}:{{ image.build.webhook_message.tag }}</a>
{% else %}
Container image uploaded by {{ image.creator }} {{ image.created|naturaltime }}
Container image uploaded by {{ image.creator }} {{ image.created|naturaltime }} {% if image.comment %}({{ image.comment }}) {% endif %}
{% endif %}

<span class="badge badge-{{ image.import_status_context }}">
Expand All @@ -453,7 +455,7 @@ <h2>Container Images</h2>
Import {{ image.get_import_status_display }}
</span>

{% if image.can_execute and image == object.latest_executable_image %}
{% if image.can_execute and image == object.active_image %}
<span class="badge badge-success">
Active
</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,22 @@
<h2>Algorithm Container Image</h2>

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

{% if object.can_execute and "change_algorithmimage" in algorithm_image_perms %}
<p>
<a class="btn btn-primary"
href="{% url 'algorithms:image-update' slug=object.algorithm.slug pk=object.pk %}">
<i class="fa fa-edit"></i> Edit this algorithm image
</a>
</p>
<a class="btn btn-primary"
href="{% url 'algorithms:image-update' slug=object.algorithm.slug pk=object.pk %}">
<i class="fa fa-edit"></i> Edit this algorithm image
</a><br>
{% endif %}

{% if object.is_manifest_valid and not object.is_desired_version and "change_algorithm" in algorithm_perms %}
<div class="my-2">Activating an image will result in this image being used for future algorithm runs:</div>
<div class="mb-2">{% crispy image_activate_form %}</div>
{% endif %}

<span class="badge p-2 my-2 {% if object.can_execute and object.is_desired_version %} badge-success {% else %} badge-danger {% endif %}">{% if object.can_execute and object.is_desired_version %}<i class="fa fa-check-circle mr-1"></i> Active image 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>
Expand Down Expand Up @@ -111,6 +117,9 @@ <h2>Algorithm Container Image</h2>

<dt>Requires Memory</dt>
<dd>{{ object.requires_memory_gb }} GB</dd>

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

{% if object.build %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{% extends "base.html" %}
{% load crispy_forms_tags %}

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

{% block content %}
<h2>Activate Algorithm Image</h2>

<div class="mb-2">Activating an image will result in this image being used for all subsequent algorithm runs.</div>

{% crispy form %}
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ <h2>Results for {{ algorithm.title }}</h2>

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

{% if "execute_algorithm" in algorithm_perms and algorithm.latest_executable_image %}
{% if "execute_algorithm" in algorithm_perms and algorithm.active_image %}
<p>
<a class="btn btn-primary"
href="{% url 'algorithms:job-create' slug=algorithm.slug %}">
Expand Down
6 changes: 6 additions & 0 deletions app/grandchallenge/algorithms/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
AlgorithmCreate,
AlgorithmDescriptionUpdate,
AlgorithmDetail,
AlgorithmImageActivate,
AlgorithmImageCreate,
AlgorithmImageDetail,
AlgorithmImageUpdate,
Expand Down Expand Up @@ -51,6 +52,11 @@
AlgorithmImageDetail.as_view(),
name="image-detail",
),
path(
"<slug>/images/activate/",
AlgorithmImageActivate.as_view(),
name="image-activate",
),
path(
"<slug>/images/<uuid:pk>/update/",
AlgorithmImageUpdate.as_view(),
Expand Down
Loading

0 comments on commit eea2757

Please sign in to comment.