diff --git a/src/InSillyCloWeb/assemblies/forms.py b/src/InSillyCloWeb/assemblies/forms.py index 4234daeba1655fe447ad3250bf674298799fa441..55ce365fbd577d9dcf90940d6c255a18b3706ed3 100644 --- a/src/InSillyCloWeb/assemblies/forms.py +++ b/src/InSillyCloWeb/assemblies/forms.py @@ -59,18 +59,30 @@ class PCRModelForm(forms.ModelForm): class Meta: model = SimulatorJob fields = ('pcr_pairs_str',) + widgets = {'pcr_pairs_str': forms.Textarea(attrs={'rows': 2})} primers_file = forms.FileField( required=False, label=_('Primers file'), help_text=_('A CSV file where the header is \"primerId;sequence\".\nNote that the separator is \";\".'), + # widget=forms.ClearableFileInput(attrs={'multiple': False}), ) + # def __init__(self, *args, **kwargs): + # # files=kwargs.pop('files', dict()) + # initial=kwargs.pop('initial', dict()) + # instance = kwargs.pop('instance', None) + # if instance: + # # files['primer_file'] =instance.primer_file + # initial['primers_file'] =instance.primer_file + # + # super().__init__(initial=initial,instance=instance,*args, **kwargs) + def save(self, commit=True): instance: SimulatorJob = super().save(commit=commit) if not commit or self.cleaned_data['primers_file'] is None: return instance - output_fp = instance.job_dir / insillyclo.main.DEFAULT_CONCENTRATIONS_FILENAME + output_fp = instance.job_dir / insillyclo.main.DEFAULT_PCR_FILENAME with open(output_fp, 'wb') as fh: for chunk in self.cleaned_data['primers_file'].chunks(): fh.write(chunk) diff --git a/src/InSillyCloWeb/assemblies/models.py b/src/InSillyCloWeb/assemblies/models.py index 3ab9c88f02a82f4fca6e82c618948fc792ff06ec..d22d772e95c307d74073bb63e73255a738bb134f 100644 --- a/src/InSillyCloWeb/assemblies/models.py +++ b/src/InSillyCloWeb/assemblies/models.py @@ -1,6 +1,7 @@ import json import pathlib import re +from functools import cached_property from io import BytesIO from tempfile import NamedTemporaryFile from typing import List, Tuple @@ -21,6 +22,7 @@ from .insillyclo_impl import InSillyCloDjangoMessageObserver import insillyclo.data_source +import insillyclo.main import insillyclo.models import insillyclo.template_generator import insillyclo @@ -294,6 +296,28 @@ class SimulatorJob(models.Model): ######################################################################### # Function and methods ######################################################################### + + # PCR + @property + def pcr_pairs(self) -> List[Tuple[str, str]]: + if not self.pcr_pairs_str: + return [] + for line in self.pcr_pairs_str.splitlines(): + forward, rev = line.split(',') + yield forward, rev + + @property + def primer_file(self) -> pathlib.Path: + return self.job_dir / insillyclo.main.DEFAULT_PCR_FILENAME + + @cached_property + def pcr_image_url(self) -> str: + file = self.results_dir / 'pcr.png' + if file.exists(): + return settings.MEDIA_URL / file.relative_to(settings.MEDIA_ROOT) + return '' + + # Others def save(self, *args, **kwargs): self.job_dir.mkdir(parents=True, exist_ok=True) self.results_dir.mkdir(parents=True, exist_ok=True) @@ -344,6 +368,8 @@ class SimulatorJob(models.Model): gb_plasmids=self.genbank_dir.glob('**/*.gb'), output_dir=self.results_dir, data_source=insillyclo.data_source.DataSourceHardCodedImplementation(), + primers_file=self.primer_file, + primer_id_pairs=list(self.pcr_pairs), ) @property diff --git a/src/InSillyCloWeb/assemblies/static/theme.scss b/src/InSillyCloWeb/assemblies/static/theme.scss index ad1d00e79693b906d39fad6cf67888277f9fd10e..a520313c33977d3bed0fcc0b1b76accf0f3dbf6a 100644 --- a/src/InSillyCloWeb/assemblies/static/theme.scss +++ b/src/InSillyCloWeb/assemblies/static/theme.scss @@ -534,6 +534,13 @@ $nav-pills-link-active-bg:$secondary; text-transform: uppercase !important; font-weight: $headings-font-weight; } +.context-simulator .form-label { + color: $color-tarocco; +} +.border-simulator { + --#{$prefix}border-color: #{$color-tarocco}; + --#{$prefix}modal-header-border-color: #{$color-tarocco}; +} legend>.asteriskField, label>.asteriskField{ color:$color-orange-crayola; @@ -635,4 +642,31 @@ label>.asteriskField{ background-position-x: -10vw; background-position-y: -5vw; background-size: 50vw; +} +.card-result{ + border-color: $simulator; +} +.card-result .card-header{ + text-align: left !important; + color: $color-orange-crayola; + padding-bottom: 0; +} +.card-result .card-header+.card-body:not(.collapsing):not(.collapse){ + padding-top: 0; +} +.card-result .card-body.collapsing, +.card-result .card-body.collapse{ + border-top-color: $simulator !important; + border-top: 1px; + border-top-style: solid; +} +.btn.btn-auto[data-bs-toggle="collapse"]{ + padding: 0 5px 0 5px; + float: right; +} +.btn.btn-auto.collapsed .is-expended{ + display: none; +} +.btn.btn-auto:not(.collapsed) .is-collapsed{ + display: none; } \ No newline at end of file diff --git a/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html b/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html index ab1b344ba42ed14bd274b476bf72573afebfc23c..5b7bb3d88e813fb8a450463eb2eda448b76436ac 100644 --- a/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html +++ b/src/InSillyCloWeb/assemblies/templates/assemblies/simulatorjob_detail.html @@ -36,4 +36,70 @@ <h1 class="col-12 text-center"> TODO </h1> +<div class="col-12"> + <div class="card card-result context-simulator"> + <div class="card-header"> + Simulate Verification + <a class="btn btn-auto {%if not object.pcr_image_url%}collapsed{%endif%}" data-bs-toggle="collapse" href="#verification" role="button" + aria-expanded="false" + aria-controls="verification"> + <i class="bi bi-chevron-down is-collapsed fs-6"></i> + <i class="bi bi-chevron-up is-expended fs-6"></i> + </a> + </div> + <div class="card-body"> + Simulate the agarose gel obtained after a PCR or a restriction digestion reaction. + </div> + <div class="{%if not object.pcr_image_url%}collapse{%endif%} border-top border-simulator" id="verification"> + {%if object.pcr_image_url%} + <div class="card-header"> + PCR gel simulation + <a href="#" data-bs-toggle="modal" data-bs-target="#pcrModal"> + <i class="bi bi-pencil text-simulator"></i> + </a> + <!-- Modal --> + <div class="modal fade" id="pcrModal" tabindex="-1" aria-labelledby="pcrModalLabel" aria-hidden="true"> + <div class="modal-dialog border-simulator"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="pcrModalLabel">Modal title</h1> + <button type="button" class="btn-close" data-bs-dismiss="modal" + aria-label="Close"></button> + </div> + <form method="post" enctype="multipart/form-data" + action="{%url 'assemblies:simulator-pcr-edit' object.pk %}"> + <div class="modal-body"> + {% csrf_token %} + {{form_pcr|crispy}} + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close + </button> + <button type="submit" class="btn btn-auto">Run simulation</button> + </div> + </form> + </div> + </div> + </div> + </div> + <div class="card-body text-center"> + <img class="w-auto" src="{{object.pcr_image_url}}"/> + </div> + {%else%} + <div class="card-body"> + <div class="card card-result"> + <form class="card-body" method="post" enctype="multipart/form-data" + action="{%url 'assemblies:simulator-pcr-edit' object.pk %}"> + {% csrf_token %} + {{form_pcr|crispy}} + <div class="text-center"> + <button type="submit" class="btn btn-auto">Run simulation</button> + </div> + </form> + </div> + </div> + {%endif%} + </div> + </div> +</div> {%endblock content %} \ No newline at end of file diff --git a/src/InSillyCloWeb/assemblies/views.py b/src/InSillyCloWeb/assemblies/views.py index c2c15d54646ad06d881d69f44619a9cceb053237..81f4c88ffa644c5aba572773e207057f69b2b5f6 100644 --- a/src/InSillyCloWeb/assemblies/views.py +++ b/src/InSillyCloWeb/assemblies/views.py @@ -119,6 +119,7 @@ class JobSimulatorResult(DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + context['form_pcr'] = forms.PCRModelForm(instance=self.object) return context @@ -139,8 +140,9 @@ class SimulatorJobEdit( return form def form_valid(self, form): + ret = super().form_valid(form) self.object.run_insillyclo(self.request) - return super().form_valid(form) + return ret def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs)