diff --git a/src/InSillyCloWeb/assemblies/models.py b/src/InSillyCloWeb/assemblies/models.py index 4402f6119d4a8cfee6a4f3b7a45026d1c29304a3..8f4dfa70bffc27adbbf1258e6eeb4d880f557fb7 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 3a109201bc4d28e8ecd4507e64a800e0518d0086..e89cc32b9494fe5a88a2d4bc54a1498e8a51cbec 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 a4203fc3e76aaade3f22fadfb87beb8ed96054c1..e9760345438369cbb5b9e7b97bf107a958c21fe8 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 ba9d99e20ff0e9105405ae1981495ffbc46c2b89..f73cd661e13aa92cca8d0f53c37484bda4be0bd6 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 ba59624e1d0d3287ba0ead133598b3bc1d6ee5a5..5688aaedf505e7dfb99715f7eeadca79206dd9bb 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: