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.""" """Admin Django pour les configurations d'expérience."""
#readonly_fields = ('experiment', ) #readonly_fields = ('experiment', )
readonly_fields = ('px_per_mm', 'fps', 'well_radius_mm',) 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", "thresh_immobile", "thresh_mobile",
"photo_mode", "chemo_strength", "created_at") "photo_mode", "chemo_strength", "created_at", )
list_filter = ("experiment", "photo_mode", "tube_axis") list_filter = ("experiment", "photo_mode", "tube_axis")
search_fields = ("experiment", "well", "description") search_fields = ("experiment", "well", "description")
ordering = ("-created_at",) ordering = ("-created_at",)
+3 -1
View File
@@ -5,15 +5,17 @@ import io
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from scanner import models
from .models import ExperimentConfig from .models import ExperimentConfig
class ExperimentConfigForm(forms.ModelForm): class ExperimentConfigForm(forms.ModelForm):
"""Formulaire de saisie/modification d'un ExperimentConfig.""" """Formulaire de saisie/modification d'un ExperimentConfig."""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['identifier'].disabled = True #self.fields['identifier'].disabled = True
class Meta: class Meta:
model = ExperimentConfig model = ExperimentConfig
+3 -1
View File
@@ -18,6 +18,8 @@ class ExperimentConfig(models.Model):
description = models.TextField( blank=True, verbose_name=_("Description"), default="-") description = models.TextField( blank=True, verbose_name=_("Description"), default="-")
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Créé le")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Créé le"))
active = models.BooleanField(_("Active"), default=True)
# --- Calibration optique --- # --- Calibration optique ---
# px_per_mm, fps, well_radius_mm # px_per_mm, fps, well_radius_mm
@@ -122,7 +124,7 @@ class ExperimentConfig(models.Model):
ordering = ["-created_at"] ordering = ["-created_at"]
def __str__(self): def __str__(self):
return f"{self.experiment} / {self.well.name}" return f"{self.experiment}:{self.well.name}"
def get_session(self): def get_session(self):
return self.experiment.session_experiments.first() if self.experiment else None 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" %} {% extends "scanner/base.html" %}
{% load i18n home_tags scanner_tags %} {% load i18n home_tags scanner_tags %}
{% block styles %}
{{ block.super }}
<link href="/static/planarian/css/planarian.css" rel="stylesheet">
{% endblock %}
{% block columns %}{% 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> <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=""> <form method="post" class="w3-bar-item" action="">
{% csrf_token %} {% csrf_token %}
<select id="_sid" name="_sid" class="w3-select w3-margin-bottom" onchange="this.form.submit()" title="{% trans "Ensemble d'expériences" %}"> <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> <option value="0">---- {% trans "Session" %}</option>
{% for s in sessions %} {% 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 %} {% endfor %}
</select> </select>
<div class="w3-margin-left w3-margin-bottom"> <div class="w3-margin-left w3-margin-bottom">
@@ -20,17 +22,17 @@
<label>{{ ss.multiwell }}</label> <label>{{ ss.multiwell }}</label>
{% endfor %} {% endfor %}
</div> </div>
{% if current_session.id and current_experiment %}
{% multiwell_cards current_session.id current_experiment %}
{% endif %}
</form> </form>
{% if current_session.id %} {% if current_session.id and current_experiment.id %}
<a href="{% url 'scanner:main' %}" class="w3-bar-item w3-btn w3-hover-opacity"> <hr class="w3-bar-item divider w3-grey">
<i class="fa-solid fa-film w3-text-green w3-xlarge""></i> {% trans "Balayage multi-puits" %} <form method="post" class="w3-bar-item" action="{% url 'planarian:export-csv' %}">
</a> {% csrf_token %}
{% endif %} <input type="hidden" name="_sid" value="{{ current_session.id }}">
--> <input type="hidden" name="_expid" value="{{ current_experiment.id }}">
{#% endblock %#} <button type="submit" class="w3-button w3-blue w3-round w3-padding w3-margin-top">📥 {% trans "Exporter CSV" %}</button>
</form>
{% endif %}
{% endblock %}
{% block js_footer %} {% block js_footer %}
{{ block.super }} {{ block.super }}
@@ -33,6 +33,9 @@
<form method="post" novalidate> <form method="post" novalidate>
{% csrf_token %} {% 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 --> <!-- Erreurs globales du formulaire -->
{% if form.non_field_errors %} {% if form.non_field_errors %}
<div class="w3-panel w3-red w3-round"> <div class="w3-panel w3-red w3-round">
@@ -309,5 +309,4 @@
if (e.target === modal) modal.style.display = 'none'; if (e.target === modal) modal.style.display = 'none';
}; };
</script> </script>
{% endblock %} {% endblock %}
@@ -1,4 +1,5 @@
{% extends "planarian/base.html" %} {% extends "planarian/base.html" %}
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
@@ -47,7 +48,9 @@
<!-- Formulaire --> <!-- Formulaire -->
<form method="post" novalidate> <form method="post" novalidate>
{% csrf_token %} {% 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 %} {% if form.non_field_errors %}
<div class="w3-panel w3-red w3-round"> <div class="w3-panel w3-red w3-round">
{% for error in form.non_field_errors %} {% for error in form.non_field_errors %}
@@ -69,33 +72,24 @@
<label class="w3-text-blue"><b>{{ form.experiment.label }}</b></label> <label class="w3-text-blue"><b>{{ form.experiment.label }}</b></label>
{% if form.experiment.help_text %} {% if form.experiment.help_text %}
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{{ form.experiment.help_text }}</p> <p class="w3-small w3-text-grey" style="margin:0 0 4px;">{{ form.experiment.help_text }}</p>
{% endif %} {% endif %}
<!-- Saisie libre ou sélection depuis les configs existantes --> <select id="experiment-list">
<input list="experiment-list" name="experiment" id="id_experiment" {% for cfg in experiments %}
value="{{ form.experiment.value|default:'' }}" <option value="{{ cfg.identifier}}" {% if current_experiment.identifier == cfg.identifier %}selected{% endif %} >{{ cfg.identifier }}</option>
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 }}">
{% endfor %} {% endfor %}
</datalist> </select>
{% if form.experiment.errors %} {% 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 %} {% endif %}
</div> </div>
<div class="w3-col m5 s12 w3-margin-bottom"> <div class="w3-col m5 s12 w3-margin-bottom">
<label class="w3-text-blue"><b>{{ form.well.label }}</b></label> <label class="w3-text-blue"><b>{{ form.well.label }}</b></label>
<input list="well-list" name="well" id="id_well" <select id="id_well">
value="{{ form.well.value|default:'' }}"
class="w3-input w3-border w3-round"
placeholder="{% trans 'Ex : A1' %}">
<datalist id="well-list">
{% for w in well_choices %} {% for w in well_choices %}
<option value="{{ w }}"> <option value="{{ w.name }}">{{ w.name }}</option>
{% endfor %} {% endfor %}
</datalist> </select>
{% if form.well.errors %} {% if form.well.errors %}
<span class="w3-text-red w3-small">{{ form.well.errors|join:", " }}</span> <span class="w3-text-red w3-small">{{ form.well.errors|join:", " }}</span>
{% endif %} {% endif %}
+85 -26
View File
@@ -15,9 +15,11 @@ from django.views.decorators.http import require_GET
from .forms import CsvImportForm, ExperimentConfigForm, ExportCsvForm from .forms import CsvImportForm, ExperimentConfigForm, ExportCsvForm
from .models import ExperimentConfig from .models import ExperimentConfig
from modules.planarian_metrics import ExperimentParams, ReductStoreClient from modules.planarian_metrics import ExperimentParams, ReductStoreClient
from modules.system_stats import get_cached_stats, start_background_updater from modules.system_stats import get_cached_stats, start_background_updater
from scanner.constants import ScannerConstants from scanner.constants import ScannerConstants
from scanner import models
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -35,17 +37,6 @@ def stats_view(request):
return JsonResponse(data, safe=False) return JsonResponse(data, safe=False)
except Exception as e: except Exception as e:
return JsonResponse({"error": str(e)}, status=500) 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: def _get_reduct_client() -> ReductStoreClient:
@@ -53,6 +44,63 @@ def _get_reduct_client() -> ReductStoreClient:
return ReductStoreClient(url=settings.REDUCTSTORE_URL, token=settings.REDUCTSTORE_TOKEN) 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 # Vue : liste des configurations
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -65,19 +113,22 @@ class ExperimentConfigListView(ListView):
context_object_name = "configs" context_object_name = "configs"
ordering = ["-created_at"] ordering = ["-created_at"]
'''
def get_queryset(self): def get_queryset(self):
# Récupérer le queryset de base qs, self.config_context = get_experiment_config(self.request)
queryset = ExperimentConfig.objects.filter(experiment__session_experiments__isnull=False).select_related() return qs
return queryset'''
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): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update(self.config_context)
return global_context(self.request, **context) return global_context(self.request, **context)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Vue : création / modification d'une configuration # Vue : création / modification d'une configuration
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -87,7 +138,11 @@ class ExperimentConfigFormView(FormView):
template_name = "planarian/experiment_form.html" template_name = "planarian/experiment_form.html"
form_class = ExperimentConfigForm 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): def get_form(self, form_class=None):
pk = self.kwargs.get("pk") pk = self.kwargs.get("pk")
@@ -102,15 +157,13 @@ class ExperimentConfigFormView(FormView):
return redirect("planarian:experiment-list") return redirect("planarian:experiment-list")
def form_invalid(self, form): def form_invalid(self, form):
# Called when form validation fails
print(f"Form validation failed: {form.errors}") print(f"Form validation failed: {form.errors}")
messages.error(self.request, form.errors) messages.error(self.request, form.errors)
return super().form_invalid(form) return super().form_invalid(form)
#def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
# # Custom logic before processing the form print(f"Received POST data: {request.POST}")
# print(f"Received POST data: {request.POST}") return super().post(request, *args, **kwargs)
# return response
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@@ -135,7 +188,11 @@ class ImportParamsView(FormView):
template_name = "planarian/import_params.html" template_name = "planarian/import_params.html"
form_class = CsvImportForm form_class = CsvImportForm
def get_queryset(self):
qs, self.config_context = get_experiment_config(self.request)
return qs
def form_valid(self, form): def form_valid(self, form):
rows = form.csv_rows rows = form.csv_rows
overwrite = form.cleaned_data["overwrite"] overwrite = form.cleaned_data["overwrite"]
@@ -192,7 +249,7 @@ class ExportCsvView(FormView):
template_name = "planarian/export_csv.html" template_name = "planarian/export_csv.html"
form_class = ExportCsvForm form_class = ExportCsvForm
def form_valid(self, form): def form_valid(self, form):
d = form.cleaned_data d = form.cleaned_data
@@ -235,6 +292,8 @@ class ExportCsvView(FormView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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) 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",), "classes": ("collapse",),
}), }),
(_("Calibration / Balayage"), { (_("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",), "classes": ("collapse",),
}), }),
(_("Tracking: valeurs par défaut"), { (_("Tracking: valeurs par défaut"), {
@@ -101,7 +101,7 @@ class SessionExperimentInlineAdmin(admin.TabularInline):
class SessionAdmin(admin.ModelAdmin): class SessionAdmin(admin.ModelAdmin):
list_filter = ('author',) list_filter = ('author',)
inlines = (SessionExperimentInlineAdmin, ) 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 = ( readonly_fields = (
'created', 'created',
'finished', 'finished',
+3 -3
View File
@@ -35,7 +35,7 @@ VIDEO_TYPE = [
CAPTURE_TYPE = [ CAPTURE_TYPE = [
('rpi', _("Arducam")), ('rpi', _("Arducam")),
('webcam', _("Webcam")), ('webcam', _("Webcam")),
('file', _("mp4")), ('file', _("Fichier vidéo (mp4, avi)")),
] ]
TUBE_AXIS_TYPE = [ TUBE_AXIS_TYPE = [
@@ -250,7 +250,7 @@ class Experiment(models.Model):
def save(self, *args, **kwargs): 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) super().save(*args, **kwargs)
@@ -265,7 +265,7 @@ class Experiment(models.Model):
verbose_name_plural = _("Expériences") verbose_name_plural = _("Expériences")
def __str__(self): def __str__(self):
return f'{self.id}:{self.title}-{self.created}' return f'{self.identifier}'
class Session(models.Model): 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' %}"> <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> <i class="fa-solid fa-maximize"></i>
</a> </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" %}"> <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" %} <i class="fa-solid fa-gear w3-text-pink"></i> {% trans "Base de données" %}
</a> </a>
@@ -37,7 +36,7 @@
<a href="{% url 'scanner:logs_worker' %}" class="w3-bar-item w3-btn w3-hover-opacity" target="_blank"> <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" %} <i class="fa-regular fa-file-lines w3-text-amber"></i> {% trans "Logs des services" %}
</a> </a>
{% endif %}
<hr class="w3-bar-item divider w3-grey"> <hr class="w3-bar-item divider w3-grey">
<form method="post" action="{% url 'logout' %}"> <form method="post" action="{% url 'logout' %}">
{% csrf_token %} {% csrf_token %}
@@ -47,22 +46,16 @@
</form> </form>
</div> </div>
</div> </div>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
{% block sidebar %} {% block sidebar %}
<div class="w3-dark-xlight w3-border-bottom"> <div class="w3-dark-xlight w3-border-bottom">
<div class="w3-large w3-bold"> <div class="w3-large w3-bold">
<a href="/" class="w3-btn w3-hover-opacity w100 w3-left-align">{{ app_title }}<br> <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> </a>
</div> </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> </div>
{% block system %} {% block system %}
<div class="w3-light-blue w3-padding-large"> <div class="w3-light-blue w3-padding-large">
@@ -124,7 +117,8 @@
{% endif %} {% endif %}
<a href="{% url 'scanner:scanning' %}" class="w3-bar-item w3-btn w3-hover-opacity"> <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" %} <i class="fa-solid fa-film w3-text-green w3-xlarge""></i> {% trans "Balayage multi-puits" %}
</a> </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"> <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" %} <i class="fa-regular fa-images w3-text-cyan w3-xlarge" w3-xlarge""></i> {% trans "Gestionnaire d'images" %}
</a> </a>
@@ -142,7 +136,7 @@
<script> <script>
const stats_endpoint = "{% url 'scanner:api_stats' %}"; const stats_endpoint = "{% url 'scanner:api_stats' %}";
</script> </script>
<script src="/static/scanner/js/stats.js"></script> <!--script src="/static/scanner/js/stats.js"></script-->
{% endblock %} {% endblock %}
@@ -10,7 +10,7 @@ def multiwell_cards(sid, experiment):
row_def = experiment.multiwell.row_def.split(',') row_def = experiment.multiwell.row_def.split(',')
multiwells.append( multiwells.append(
f''' 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"> <div class="w3-border multiwell_cards">
''') ''')
for row in range(experiment.multiwell.rows): for row in range(experiment.multiwell.rows):