experiment

This commit is contained in:
2026-05-10 22:28:13 +02:00
parent 500950017f
commit 13141bc46a
13 changed files with 153 additions and 86 deletions
+2 -2
View File
@@ -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",)
+3 -1
View File
@@ -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
+3 -1
View File
@@ -19,6 +19,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
px_per_mm = models.FloatField(
@@ -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
@@ -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 ;
}
@@ -1,17 +1,19 @@
{% extends "scanner/base.html" %}
{% load i18n home_tags scanner_tags %}
{% block styles %}
{{ block.super }}
<link href="/static/planarian/css/planarian.css" rel="stylesheet">
{% endblock %}
{% block columns %}{% endblock %}
{#% block sidebar_list %#}
<!--
{% block sidebar_list %}
<a href="/" class="w3-bar-item w3-btn w3-hover-opacity"><i class="fa-solid fa-house w3-text-orange w3-xlarge"></i> {% trans "Retour accueil" %}</a>
<form method="post" class="w3-bar-item" action="">
{% csrf_token %}
<select id="_sid" name="_sid" class="w3-select w3-margin-bottom" onchange="this.form.submit()" title="{% trans "Ensemble d'expériences" %}">
<option value="0">---- {% trans "Session" %}</option>
{% for s in sessions %}
<option value="{{ s.id }}" {% if s.id == current_session.id %}selected{% endif %}>{{ s.name }}</option>
<option value="{{ s.id }}" {% if s.id == current_session.id %}selected{% endif %}>{{ s }}</option>
{% endfor %}
</select>
<div class="w3-margin-left w3-margin-bottom">
@@ -20,17 +22,17 @@
<label>{{ ss.multiwell }}</label>
{% endfor %}
</div>
{% if current_session.id and current_experiment %}
{% multiwell_cards current_session.id current_experiment %}
{% endif %}
</form>
{% if current_session.id %}
<a href="{% url 'scanner:main' %}" class="w3-bar-item w3-btn w3-hover-opacity">
<i class="fa-solid fa-film w3-text-green w3-xlarge""></i> {% trans "Balayage multi-puits" %}
</a>
{% if current_session.id and current_experiment.id %}
<hr class="w3-bar-item divider w3-grey">
<form method="post" class="w3-bar-item" action="{% url 'planarian:export-csv' %}">
{% csrf_token %}
<input type="hidden" name="_sid" value="{{ current_session.id }}">
<input type="hidden" name="_expid" value="{{ current_experiment.id }}">
<button type="submit" class="w3-button w3-blue w3-round w3-padding w3-margin-top">📥 {% trans "Exporter CSV" %}</button>
</form>
{% endif %}
-->
{#% endblock %#}
{% endblock %}
{% block js_footer %}
{{ block.super }}
@@ -33,6 +33,9 @@
<form method="post" novalidate>
{% csrf_token %}
<input type="hidden" id="_sid" name="_sid" value="{{ current_session.id }}">
<input type="hidden" id="_expid" name="_expid" value="{{ current_experiment.id }}">
<!-- Erreurs globales du formulaire -->
{% if form.non_field_errors %}
<div class="w3-panel w3-red w3-round">
@@ -309,5 +309,4 @@
if (e.target === modal) modal.style.display = 'none';
};
</script>
{% endblock %}
@@ -1,4 +1,5 @@
{% extends "planarian/base.html" %}
{% load i18n %}
{% block content %}
@@ -47,6 +48,8 @@
<!-- Formulaire -->
<form method="post" novalidate>
{% csrf_token %}
<input type="hidden" id="_sid" name="_sid" value="{{ current_session.id }}">
<input type="hidden" id="_expid" name="_expid" value="{{ current_experiment.id }}">
{% if form.non_field_errors %}
<div class="w3-panel w3-red w3-round">
@@ -70,32 +73,23 @@
{% if form.experiment.help_text %}
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{{ form.experiment.help_text }}</p>
{% endif %}
<!-- Saisie libre ou sélection depuis les configs existantes -->
<input list="experiment-list" name="experiment" id="id_experiment"
value="{{ form.experiment.value|default:'' }}"
class="w3-input w3-border w3-round"
placeholder="{% trans 'Ex : exp_2026_04_25' %}">
<datalist id="experiment-list">
{% for cfg in experiment_choices %}
<option value="{{ cfg }}">
<select id="experiment-list">
{% for cfg in experiments %}
<option value="{{ cfg.identifier}}" {% if current_experiment.identifier == cfg.identifier %}selected{% endif %} >{{ cfg.identifier }}</option>
{% endfor %}
</datalist>
</select>
{% if form.experiment.errors %}
<span class="w3-text-red w3-small">{{ form.experiment.errors|join:", " }}</span>
<!--span class="w3-text-red w3-small">{{ form.experiment.errors|join:", " }}</span-->
{% endif %}
</div>
<div class="w3-col m5 s12 w3-margin-bottom">
<label class="w3-text-blue"><b>{{ form.well.label }}</b></label>
<input list="well-list" name="well" id="id_well"
value="{{ form.well.value|default:'' }}"
class="w3-input w3-border w3-round"
placeholder="{% trans 'Ex : A1' %}">
<datalist id="well-list">
<select id="id_well">
{% for w in well_choices %}
<option value="{{ w }}">
<option value="{{ w.name }}">{{ w.name }}</option>
{% endfor %}
</datalist>
</select>
{% if form.well.errors %}
<span class="w3-text-red w3-small">{{ form.well.errors|join:", " }}</span>
{% endif %}
+73 -14
View File
@@ -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__)
@@ -37,21 +39,67 @@ def stats_view(request):
return JsonResponse({"error": str(e)}, status=500)
def _get_reduct_client() -> ReductStoreClient:
"""Instancie le client ReductStore depuis les settings Django."""
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_reduct_client() -> ReductStoreClient:
"""Instancie le client ReductStore depuis les settings Django."""
return ReductStoreClient(url=settings.REDUCTSTORE_URL, token=settings.REDUCTSTORE_TOKEN)
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
# ---------------------------------------------------------------------------
@@ -89,6 +140,10 @@ class ExperimentConfigFormView(FormView):
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")
if 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):
@@ -136,6 +189,10 @@ 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"]
@@ -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)
+2 -2
View File
@@ -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',
+3 -3
View File
@@ -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):
@@ -11,12 +11,11 @@
<a href="#" class="w3-bar-item w3-mobile w3-btn w3-hover-opacity w3-right w3-xlarge w3-text-green" onclick="goFullscreen()" title="{% trans 'Ecran plein' %}">
<i class="fa-solid fa-maximize"></i>
</a>
{% if request.user.is_superuser %}
<div class="w3-dropdown-hover w3-right">
<button class="w3-button w3-dark-plus">&#9776;&nbsp;<span>{% trans "Administrations" %}</span></button>
<div class="w3-dropdown-content w3-bar-block w3-border w3-dark-low" style="right:.25em;">
<hr class="w3-bar-item divider w3-grey">
<div class="w3-dropdown-hover w3-right">
<button class="w3-button w3-dark-plus">&#9776;&nbsp;<span>{{ request.user.username }}</span></button>
<div class="w3-dropdown-content w3-bar-block w3-border w3-dark-low" style="right:.25em;">
{% if request.user.is_superuser %}
<a href="{% url 'scanner:admin' %}" class="w3-bar-item w3-btn w3-hover-opacity" title="{% trans "Accès django" %}">
<i class="fa-solid fa-gear w3-text-pink"></i> {% trans "Base de données" %}
</a>
@@ -37,7 +36,7 @@
<a href="{% url 'scanner:logs_worker' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank">
<i class="fa-regular fa-file-lines w3-text-amber"></i> {% trans "Logs des services" %}
</a>
{% endif %}
<hr class="w3-bar-item divider w3-grey">
<form method="post" action="{% url 'logout' %}">
{% csrf_token %}
@@ -47,7 +46,7 @@
</form>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block sidebar %}
@@ -57,12 +56,6 @@
<span class="w3-tiny">{{ app_sub_title }}</span>
</a>
</div>
<form method="post" action="{% url 'logout' %}" class="w3-padding-small">
{% csrf_token %}
<button class="w3-bar-item w3-btn w3-hover-opacity" type="submit" title="{% trans 'Déconnexion' %}">
{{ request.user.username }}<i class="fa-solid fa-right-from-bracket w3-right"></i>
</button>
</form>
</div>
{% block system %}
<div class="w3-light-blue w3-padding-large">
@@ -125,6 +118,7 @@
<a href="{% url 'scanner:scanning' %}" class="w3-bar-item w3-btn w3-hover-opacity">
<i class="fa-solid fa-film w3-text-green w3-xlarge""></i> {% trans "Balayage multi-puits" %}
</a>
<div class="w3-bar-item w3-dark-xxlight">{% trans "Résultats " %}</div>
<a href="{% url 'scanner:images' %}" class="w3-bar-item w3-btn w3-hover-opacity">
<i class="fa-regular fa-images w3-text-cyan w3-xlarge" w3-xlarge""></i> {% trans "Gestionnaire d'images" %}
</a>
@@ -142,7 +136,7 @@
<script>
const stats_endpoint = "{% url 'scanner:api_stats' %}";
</script>
<script src="/static/scanner/js/stats.js"></script>
<!--script src="/static/scanner/js/stats.js"></script-->
{% endblock %}
@@ -10,7 +10,7 @@ def multiwell_cards(sid, experiment):
row_def = experiment.multiwell.row_def.split(',')
multiwells.append(
f'''
<div class="w3-center w3-sand">{experiment.title}</div>
<div class="w3-padding-small w3-sand">{experiment.title}</div>
<div class="w3-border multiwell_cards">
''')
for row in range(experiment.multiwell.rows):