From 13141bc46a4231502ef75f483d1f2d04c7e7bf70 Mon Sep 17 00:00:00 2001 From: denis defolie Date: Sun, 10 May 2026 22:28:13 +0200 Subject: [PATCH] experiment --- test_tube_scanner/planarian/admin.py | 4 +- test_tube_scanner/planarian/forms.py | 4 +- test_tube_scanner/planarian/models.py | 4 +- .../static/planarian/css/planarian.css | 12 ++ .../planarian/templates/planarian/base.html | 30 ++--- .../templates/planarian/experiment_form.html | 3 + .../templates/planarian/experiment_list.html | 1 - .../templates/planarian/export_csv.html | 32 ++--- test_tube_scanner/planarian/views.py | 111 ++++++++++++++---- test_tube_scanner/scanner/admin.py | 4 +- test_tube_scanner/scanner/models.py | 6 +- .../scanner/templates/scanner/base.html | 26 ++-- .../scanner/templatetags/scanner_tags.py | 2 +- 13 files changed, 153 insertions(+), 86 deletions(-) create mode 100644 test_tube_scanner/planarian/static/planarian/css/planarian.css diff --git a/test_tube_scanner/planarian/admin.py b/test_tube_scanner/planarian/admin.py index cc56ccd..28e00fc 100644 --- a/test_tube_scanner/planarian/admin.py +++ b/test_tube_scanner/planarian/admin.py @@ -10,9 +10,9 @@ class ExperimentConfigAdmin(admin.ModelAdmin): """Admin Django pour les configurations d'expérience.""" #readonly_fields = ('experiment', ) readonly_fields = ('px_per_mm', 'fps', 'well_radius_mm',) - list_display = ("experiment", "well", "px_per_mm", "fps", + list_display = ("experiment", "well", "active", "px_per_mm", "fps", "thresh_immobile", "thresh_mobile", - "photo_mode", "chemo_strength", "created_at") + "photo_mode", "chemo_strength", "created_at", ) list_filter = ("experiment", "photo_mode", "tube_axis") search_fields = ("experiment", "well", "description") ordering = ("-created_at",) diff --git a/test_tube_scanner/planarian/forms.py b/test_tube_scanner/planarian/forms.py index 1776ff7..05872cd 100644 --- a/test_tube_scanner/planarian/forms.py +++ b/test_tube_scanner/planarian/forms.py @@ -5,15 +5,17 @@ import io from django import forms from django.utils.translation import gettext_lazy as _ +from scanner import models from .models import ExperimentConfig + class ExperimentConfigForm(forms.ModelForm): """Formulaire de saisie/modification d'un ExperimentConfig.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['identifier'].disabled = True + #self.fields['identifier'].disabled = True class Meta: model = ExperimentConfig diff --git a/test_tube_scanner/planarian/models.py b/test_tube_scanner/planarian/models.py index 765ba7a..6376a92 100644 --- a/test_tube_scanner/planarian/models.py +++ b/test_tube_scanner/planarian/models.py @@ -18,6 +18,8 @@ class ExperimentConfig(models.Model): description = models.TextField( blank=True, verbose_name=_("Description"), default="-") created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Créé le")) + + active = models.BooleanField(_("Active"), default=True) # --- Calibration optique --- # px_per_mm, fps, well_radius_mm @@ -122,7 +124,7 @@ class ExperimentConfig(models.Model): ordering = ["-created_at"] def __str__(self): - return f"{self.experiment} / {self.well.name}" + return f"{self.experiment}:{self.well.name}" def get_session(self): return self.experiment.session_experiments.first() if self.experiment else None diff --git a/test_tube_scanner/planarian/static/planarian/css/planarian.css b/test_tube_scanner/planarian/static/planarian/css/planarian.css new file mode 100644 index 0000000..f526173 --- /dev/null +++ b/test_tube_scanner/planarian/static/planarian/css/planarian.css @@ -0,0 +1,12 @@ + +.multiwell_cards { + display: grid; + grid-template-columns: repeat(6, 1fr); + justify-items: center; + align-items: center; +} + +button.multiwell { + padding: 0.2em ; +} + diff --git a/test_tube_scanner/planarian/templates/planarian/base.html b/test_tube_scanner/planarian/templates/planarian/base.html index 27165c1..336fe9c 100644 --- a/test_tube_scanner/planarian/templates/planarian/base.html +++ b/test_tube_scanner/planarian/templates/planarian/base.html @@ -1,17 +1,19 @@ {% extends "scanner/base.html" %} {% load i18n home_tags scanner_tags %} - +{% block styles %} + {{ block.super }} + +{% endblock %} {% block columns %}{% endblock %} -{#% block sidebar_list %#} - -{#% endblock %#} + {% if current_session.id and current_experiment.id %} +
+
+ {% csrf_token %} + + + +
+ {% endif %} +{% endblock %} {% block js_footer %} {{ block.super }} diff --git a/test_tube_scanner/planarian/templates/planarian/experiment_form.html b/test_tube_scanner/planarian/templates/planarian/experiment_form.html index 7944f26..193d130 100644 --- a/test_tube_scanner/planarian/templates/planarian/experiment_form.html +++ b/test_tube_scanner/planarian/templates/planarian/experiment_form.html @@ -33,6 +33,9 @@
{% csrf_token %} + + + {% if form.non_field_errors %}
diff --git a/test_tube_scanner/planarian/templates/planarian/experiment_list.html b/test_tube_scanner/planarian/templates/planarian/experiment_list.html index 7d8cf81..b6e836e 100644 --- a/test_tube_scanner/planarian/templates/planarian/experiment_list.html +++ b/test_tube_scanner/planarian/templates/planarian/experiment_list.html @@ -309,5 +309,4 @@ if (e.target === modal) modal.style.display = 'none'; }; - {% endblock %} diff --git a/test_tube_scanner/planarian/templates/planarian/export_csv.html b/test_tube_scanner/planarian/templates/planarian/export_csv.html index c2390ed..5cbc991 100644 --- a/test_tube_scanner/planarian/templates/planarian/export_csv.html +++ b/test_tube_scanner/planarian/templates/planarian/export_csv.html @@ -1,4 +1,5 @@ {% extends "planarian/base.html" %} + {% load i18n %} {% block content %} @@ -47,7 +48,9 @@ {% csrf_token %} - + + + {% if form.non_field_errors %}
{% for error in form.non_field_errors %} @@ -69,33 +72,24 @@ {% if form.experiment.help_text %}

{{ form.experiment.help_text }}

- {% endif %} - - - - {% for cfg in experiment_choices %} -
- - + {% if form.well.errors %} {{ form.well.errors|join:", " }} {% endif %} diff --git a/test_tube_scanner/planarian/views.py b/test_tube_scanner/planarian/views.py index 22160c8..0e532f6 100644 --- a/test_tube_scanner/planarian/views.py +++ b/test_tube_scanner/planarian/views.py @@ -15,9 +15,11 @@ from django.views.decorators.http import require_GET from .forms import CsvImportForm, ExperimentConfigForm, ExportCsvForm from .models import ExperimentConfig + from modules.planarian_metrics import ExperimentParams, ReductStoreClient from modules.system_stats import get_cached_stats, start_background_updater from scanner.constants import ScannerConstants +from scanner import models logger = logging.getLogger(__name__) @@ -35,17 +37,6 @@ def stats_view(request): return JsonResponse(data, safe=False) except Exception as e: return JsonResponse({"error": str(e)}, status=500) - - -def global_context(request, **ctx): - conf = ScannerConstants().get() - return dict( - app_title=settings.APP_TITLE, - app_sub_title=settings.APP_SUB_TITLE, - domain_server=settings.DOMAIN_SERVER, - conf=conf, - **ctx - ) def _get_reduct_client() -> ReductStoreClient: @@ -53,6 +44,63 @@ def _get_reduct_client() -> ReductStoreClient: return ReductStoreClient(url=settings.REDUCTSTORE_URL, token=settings.REDUCTSTORE_TOKEN) +def global_context(request, **ctx): + default_multiwell = models.MultiWell.objects.filter(default=True).first() + conf = ScannerConstants().get() + return dict( + app_title=settings.APP_TITLE, + app_sub_title=settings.APP_SUB_TITLE, + domain_server=settings.DOMAIN_SERVER, + local_ip_server=settings.LOCAL_IP_SERVER, + host_port=settings.SERVER_HOST_PORT, + conf=conf, + default_position = default_multiwell.position or 'HD', + export_destination=settings.EXPORT_DESTINATIONS, + well_choices = models.Well.objects.order_by('name').all(), + + **ctx + ) + + +def get_active_experiments(session, expid=None): + if session: + experiments = models.SessionExperiment.experiment_by_session(session.id, active=True) or [] + eid = [str(e.id) for e in experiments] + + print(f"Found {eid} active experiments for session {session.id}") + + if experiments and not expid or expid not in eid: + return experiments, experiments[0] + + for e in experiments: + if expid == str(e.id): + return experiments, e + return [], None + + +def get_experiment_config(request): + cursid = request.POST.get('_sid') + expid = request.POST.get('_expid') + + print(request.POST) + + current_session = models.Session.get_session(cursid) + experiments, current_experiment = get_active_experiments(current_session, expid) + + qs = ExperimentConfig.objects + if not current_session: + qs = qs.filter(experiment__session_experiments__isnull=False).select_related() + elif current_experiment: + qs = qs.filter(experiment_id=current_experiment.id) + + context = dict( + current_session = current_session, + current_experiment = current_experiment, + experiments=experiments or [], + sessions=models.Session.objects.filter(active=True).all(), + ) + return qs.all(), context + # --------------------------------------------------------------------------- # Vue : liste des configurations # --------------------------------------------------------------------------- @@ -65,19 +113,22 @@ class ExperimentConfigListView(ListView): context_object_name = "configs" ordering = ["-created_at"] - ''' def get_queryset(self): - # Récupérer le queryset de base - queryset = ExperimentConfig.objects.filter(experiment__session_experiments__isnull=False).select_related() - return queryset''' - + qs, self.config_context = get_experiment_config(self.request) + return qs + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + + context.update(self.config_context) return global_context(self.request, **context) - - # --------------------------------------------------------------------------- # Vue : création / modification d'une configuration # --------------------------------------------------------------------------- @@ -87,7 +138,11 @@ class ExperimentConfigFormView(FormView): template_name = "planarian/experiment_form.html" form_class = ExperimentConfigForm - + + + def get_queryset(self): + qs, self.config_context = get_experiment_config(self.request) + return qs def get_form(self, form_class=None): pk = self.kwargs.get("pk") @@ -102,15 +157,13 @@ class ExperimentConfigFormView(FormView): return redirect("planarian:experiment-list") def form_invalid(self, form): - # Called when form validation fails print(f"Form validation failed: {form.errors}") messages.error(self.request, form.errors) return super().form_invalid(form) - #def post(self, request, *args, **kwargs): - # # Custom logic before processing the form - # print(f"Received POST data: {request.POST}") - # return response + def post(self, request, *args, **kwargs): + print(f"Received POST data: {request.POST}") + return super().post(request, *args, **kwargs) def get_context_data(self, **kwargs): @@ -135,7 +188,11 @@ class ImportParamsView(FormView): template_name = "planarian/import_params.html" form_class = CsvImportForm - + + def get_queryset(self): + qs, self.config_context = get_experiment_config(self.request) + return qs + def form_valid(self, form): rows = form.csv_rows overwrite = form.cleaned_data["overwrite"] @@ -192,7 +249,7 @@ class ExportCsvView(FormView): template_name = "planarian/export_csv.html" form_class = ExportCsvForm - + def form_valid(self, form): d = form.cleaned_data @@ -235,6 +292,8 @@ class ExportCsvView(FormView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + qs, config_context = get_experiment_config(self.request) + context.update(config_context) return global_context(self.request, choice_title=_("Tracking depuis ReductStore vers un fichier CSV"), **context) diff --git a/test_tube_scanner/scanner/admin.py b/test_tube_scanner/scanner/admin.py index e4322c6..c61ec33 100644 --- a/test_tube_scanner/scanner/admin.py +++ b/test_tube_scanner/scanner/admin.py @@ -30,7 +30,7 @@ class ConfigurationAdmin(admin.ModelAdmin): "classes": ("collapse",), }), (_("Calibration / Balayage"), { - "fields": ("calibration_crop_radius", "calibration_default_multiwell", "calibration_default_feed", "calibration_default_step", "calibration_default_duration"), + "fields": ("tube_axis", "calibration_crop_radius", "calibration_default_multiwell", "calibration_default_feed", "calibration_default_step", "calibration_default_duration"), "classes": ("collapse",), }), (_("Tracking: valeurs par défaut"), { @@ -101,7 +101,7 @@ class SessionExperimentInlineAdmin(admin.TabularInline): class SessionAdmin(admin.ModelAdmin): list_filter = ('author',) inlines = (SessionExperimentInlineAdmin, ) - list_display = ('name', 'author', 'created', 'finished', 'active', 'expected_export', 'expected_scanning', ) + list_display = ('name', 'id', 'author', 'created', 'finished', 'active', 'expected_export', 'expected_scanning', ) readonly_fields = ( 'created', 'finished', diff --git a/test_tube_scanner/scanner/models.py b/test_tube_scanner/scanner/models.py index 5568d5a..3ac64f5 100644 --- a/test_tube_scanner/scanner/models.py +++ b/test_tube_scanner/scanner/models.py @@ -35,7 +35,7 @@ VIDEO_TYPE = [ CAPTURE_TYPE = [ ('rpi', _("Arducam")), ('webcam', _("Webcam")), - ('file', _("mp4")), + ('file', _("Fichier vidéo (mp4, avi)")), ] TUBE_AXIS_TYPE = [ @@ -250,7 +250,7 @@ class Experiment(models.Model): def save(self, *args, **kwargs): - self.identifier = slugify(f'{self.created.isoformat()[:19]}-{self.id}-{self.multiwell.position}') + self.identifier = f'{self.id}-{self.multiwell.position}_{self.created.isoformat()[:19]}' super().save(*args, **kwargs) @@ -265,7 +265,7 @@ class Experiment(models.Model): verbose_name_plural = _("Expériences") def __str__(self): - return f'{self.id}:{self.title}-{self.created}' + return f'{self.identifier}' class Session(models.Model): diff --git a/test_tube_scanner/scanner/templates/scanner/base.html b/test_tube_scanner/scanner/templates/scanner/base.html index fbdb12f..9bd2e1e 100644 --- a/test_tube_scanner/scanner/templates/scanner/base.html +++ b/test_tube_scanner/scanner/templates/scanner/base.html @@ -11,12 +11,11 @@ - {% if request.user.is_superuser %} -
- -
-
+
+ +
+ {% if request.user.is_superuser %} {% trans "Base de données" %} @@ -37,7 +36,7 @@ {% trans "Logs des services" %} - + {% endif %}
{% csrf_token %} @@ -47,22 +46,16 @@
- {% endif %} +
{% endblock %} {% block sidebar %}
-
- {% csrf_token %} - -
{% block system %}
@@ -124,7 +117,8 @@ {% endif %} {% trans "Balayage multi-puits" %} - + +
{% trans "Résultats " %}
{% trans "Gestionnaire d'images" %} @@ -142,7 +136,7 @@ - + {% endblock %} diff --git a/test_tube_scanner/scanner/templatetags/scanner_tags.py b/test_tube_scanner/scanner/templatetags/scanner_tags.py index 71fc2de..2e733ea 100644 --- a/test_tube_scanner/scanner/templatetags/scanner_tags.py +++ b/test_tube_scanner/scanner/templatetags/scanner_tags.py @@ -10,7 +10,7 @@ def multiwell_cards(sid, experiment): row_def = experiment.multiwell.row_def.split(',') multiwells.append( f''' -
{experiment.title}
+
{experiment.title}
''') for row in range(experiment.multiwell.rows):