From a6670bd228a34fefcc22fb4ec8fbcfd98fbc0bb0 Mon Sep 17 00:00:00 2001 From: Julien FumeyY <julien.fumey@pasteur.fr> Date: Thu, 24 Apr 2025 17:20:04 +0200 Subject: [PATCH] functional download link for simulator job results --- src/InSillyCloWeb/assemblies/models.py | 34 +++++++++++-------- .../assemblies/simulatorjob_card.html | 13 ++++--- .../assemblies/simulatorjob_detail.html | 2 +- src/InSillyCloWeb/assemblies/urls.py | 1 + src/InSillyCloWeb/assemblies/views.py | 23 +++++++++++++ 5 files changed, 54 insertions(+), 19 deletions(-) diff --git a/src/InSillyCloWeb/assemblies/models.py b/src/InSillyCloWeb/assemblies/models.py index 4402f61..8f4dfa7 100644 --- a/src/InSillyCloWeb/assemblies/models.py +++ b/src/InSillyCloWeb/assemblies/models.py @@ -19,7 +19,6 @@ from django.db.models.functions import Upper from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.conf import settings -from django.contrib.auth.signals import user_logged_in from . import utils from .insillyclo_impl import InSillyCloDjangoMessageObserver @@ -251,13 +250,10 @@ class Assembly(models.Model): class JobStatus(models.IntegerChoices): - NEW = 0, _("New") - QUEUED = 1, _("Queued") - FETCHING = 5, _("Fetching") - RUNNING = 10, _("Running") - DONE = 15, _("Done") + NEW = 0, _("CREATED") + DONE = 15, _("DONE") ERROR = 16, _("Error") - CANCELED = 17, _("Canceled") + CANCELED = 17, _("CANCELED") __empty__ = "(Unknown)" @@ -438,8 +434,12 @@ class SimulatorJob(models.Model): enzyme_names=self.enzyme_names, ) messages.info(request, f"TODO: make use of {str(output)}") + self.status = JobStatus.DONE + self.save() except insillyclo.additional_exception.InSillyCloFailureException as e: output = None + self.status = JobStatus.ERROR + self.save() messages.error(request, utils.clear_sensitive_info(str(e), self)) return output @@ -471,6 +471,10 @@ class SimulatorJob(models.Model): def results_dir(self) -> pathlib.Path: return self.job_dir / 'results' + @property + def is_downloadable(self): + return self.status == JobStatus.DONE and self.results_dir.exists() + def delete(self, *args, **kwargs): try: shutil.rmtree(self.job_dir) @@ -481,12 +485,14 @@ class SimulatorJob(models.Model): def get_absolute_url(self): return reverse("assemblies:simulator-detail", args=[self.uuid]) - def __str__(self): - return f'{str(self.uuid)[:8]} - {str(self.created_at)}' - - -def assign_object_to_user(sender, user, request, **kwargs): - print(request.session.session_key) + def results_file(self): + basename = f"insillyclo_{self.uuid_short}" + zipname = os.path.join(self.job_dir, basename) + destination_file = pathlib.Path(zipname + '.zip') + shutil.make_archive(zipname, 'zip', self.results_dir) + with open(destination_file, "rb") as fh: + return BytesIO(fh.read()), basename, 'application/zip' -user_logged_in.connect(assign_object_to_user) + def __str__(self): + return f'{str(self.uuid)[:8]} - {str(self.created_at)}' diff --git a/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_card.html b/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_card.html index 3a10920..e89cc32 100644 --- a/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_card.html +++ b/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_card.html @@ -1,18 +1,21 @@ {% load assemblies_tags %} {% load i18n %} -<div class="card context-simulator"> +<div class="card border-simulator"> <div class="card-body row"> - <a class="col-12 col-xl-2 col-lg-3 col-md-3 me-md-4 text-decoration-none" href="{% url 'assemblies:simulator-detail' uuid=object.uuid %}"> + <a class="col-12 col-xl-2 col-lg-3 col-md-3 me-md-4 text-simulator text-decoration-none" href="{% url 'assemblies:simulator-detail' uuid=object.uuid %}"> <div class="fw-bolder"> Job #{{object.uuid_short}} </div> + <small class="text-secondary"> + {{ object.get_status_display }} + </small> <small class="text-secondary"> {{object|field_verbose_name:'updated_at'}}{%trans ':'%}<br/> {{object.updated_at}} </small> </a> - <div class="col border-10 border-start border-primary ps-md-4"> + <div class="col border-10 border-start border-simulator text-simulator ps-md-4"> <div class="d-block"> <span class="fw-bolder"> @@ -42,12 +45,14 @@ </div> </div> + {% if object.is_downloadable %} <div class="col-auto"> - <a href="" + <a href="{% url 'assemblies:simulator-download' uuid=object.uuid %}" class="btn btn-designer btn-md"> <i class="bi bi-cloud-arrow-down fs-3"></i> </a> </div> + {% endif %} </div> </div> diff --git a/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html b/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html index a4203fc..e976034 100644 --- a/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html +++ b/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html @@ -30,7 +30,7 @@ {% block page_sub_title %}Assembly simulator{% endblock %} {%block content %} -<h6 class="col-12 text-center"> +<h6 class="col-12 text-center text-simulator"> Run on {{object.updated_at}} </h6> <h1 class="col-12 text-center"> diff --git a/src/InSillyCloWeb/assemblies/urls.py b/src/InSillyCloWeb/assemblies/urls.py index ba9d99e..f73cd66 100644 --- a/src/InSillyCloWeb/assemblies/urls.py +++ b/src/InSillyCloWeb/assemblies/urls.py @@ -69,6 +69,7 @@ urlpatterns = [ path('assembly-simulator/create/<str:step>/', simulator_wizard, name='simulator-create-step'), path('assembly-simulator/create/', simulator_wizard, name='simulator-create'), path('assembly-simulator/<slug:uuid>/', views.JobSimulatorResult.as_view(), name='simulator-detail'), + path('assembly-simulator/<slug:uuid>/download/', views.JobDownloadView.as_view(), name='simulator-download'), path('assembly-simulator/<slug:uuid>/pcr-edit/', views.JobPCREdit.as_view(), name='simulator-pcr-edit'), path( 'assembly-simulator/<slug:uuid>/enzyme-edit/', diff --git a/src/InSillyCloWeb/assemblies/views.py b/src/InSillyCloWeb/assemblies/views.py index ba59624..5688aae 100644 --- a/src/InSillyCloWeb/assemblies/views.py +++ b/src/InSillyCloWeb/assemblies/views.py @@ -192,6 +192,29 @@ class JobRestrictionEnzymeEdit( return _("Restriction enzyme to use for gel simulation") +class JobDownloadView( + SingleObjectMixin, + View, +): + model = models.SimulatorJob + slug_field = 'uuid' + slug_url_kwarg = 'uuid' + + def get_file(self) -> Tuple[BytesIO, str, str]: + return self.get_object().results_file() + + def get(self, request, *args, **kwargs): + try: + stream, filename, content_type = self.get_file() + response = HttpResponse(stream.read(), content_type=content_type) + response['Content-Disposition'] = f'attachment; filename={filename}' + return response + except Exception as e: + traceback.print_exc() + messages.error(request, str(e.__class__) + str(e)) + return redirect(request.headers['Referer']) + + def loginView(request): if request.user.is_authenticated: -- GitLab