experiment
This commit is contained in:
@@ -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",)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">☰ <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">☰ <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,22 +46,16 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block sidebar %}
|
||||
<div class="w3-dark-xlight w3-border-bottom">
|
||||
<div class="w3-large w3-bold">
|
||||
<a href="/" class="w3-btn w3-hover-opacity w100 w3-left-align">{{ app_title }}<br>
|
||||
<span class="w3-tiny">{{ app_sub_title }}</span>
|
||||
<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):
|
||||
|
||||
Reference in New Issue
Block a user