export/import metrics
This commit is contained in:
@@ -2,7 +2,11 @@
|
|||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="alert">
|
<div class="alert">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="w3-bar {% if message.tags %}{{ message.tags }}{% endif %}" style="width: 100%" >
|
<div class="w3-panel w3-round
|
||||||
|
{% if message.tags == 'success' %}w3-green
|
||||||
|
{% elif message.tags == 'error' %}w3-red
|
||||||
|
{% elif message.tags == 'warning' %}w3-orange
|
||||||
|
{% else %}w3-blue{% endif %}" style="width: 100%" >
|
||||||
<div class="w3-bar-item">{{ message }}</div>
|
<div class="w3-bar-item">{{ message }}</div>
|
||||||
<div class="w3-bar-item w3-button w3-right" onclick="this.parentElement.style.display='none'">×</div>
|
<div class="w3-bar-item w3-button w3-right" onclick="this.parentElement.style.display='none'">×</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -618,7 +618,7 @@ class ExperimentParams:
|
|||||||
def from_csv_file(cls, filepath: str) -> list:
|
def from_csv_file(cls, filepath: str) -> list:
|
||||||
"""Charge toutes les expériences d'un fichier CSV."""
|
"""Charge toutes les expériences d'un fichier CSV."""
|
||||||
results = []
|
results = []
|
||||||
with open(filepath, newline="", encoding="utf-8") as f:
|
with open(filepath, newline="", encoding="utf-8-sig") as f:
|
||||||
for row in csv.DictReader(f):
|
for row in csv.DictReader(f):
|
||||||
try:
|
try:
|
||||||
results.append(cls.from_csv_row(row))
|
results.append(cls.from_csv_row(row))
|
||||||
|
|||||||
@@ -8,19 +8,18 @@ from .models import ExperimentConfig
|
|||||||
@admin.register(ExperimentConfig)
|
@admin.register(ExperimentConfig)
|
||||||
class ExperimentConfigAdmin(admin.ModelAdmin):
|
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', 'px_per_mm', 'fps', 'well_radius_mm',)
|
||||||
readonly_fields = ('px_per_mm', 'fps', 'well_radius_mm',)
|
list_display = ("experiment_key", "well", "active", "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_key", "photo_mode", "tube_axis")
|
||||||
search_fields = ("experiment", "well", "description")
|
search_fields = ("experiment_key", "well_name", "description")
|
||||||
ordering = ("-created_at",)
|
ordering = ("-created_at",)
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(_("Identification"), {
|
(_("Identification"), {
|
||||||
"fields": ("experiment", "well", "active", "description"),
|
"fields": ("experiment_key", "well", "active", "description"),
|
||||||
}),
|
}),
|
||||||
(_("Calibration optique: générée lors de la calibration"), {
|
(_("Calibration optique: générée lors de la calibration"), {
|
||||||
"fields": ("px_per_mm", "fps", "well_radius_mm"),
|
"fields": ("px_per_mm", "fps", "well_radius_mm"),
|
||||||
@@ -58,19 +57,19 @@ class ExperimentConfigAdmin(admin.ModelAdmin):
|
|||||||
@admin.action(description=_("Exporter un template CSV de ces configurations"))
|
@admin.action(description=_("Exporter un template CSV de ces configurations"))
|
||||||
def export_csv_template(self, request, queryset):
|
def export_csv_template(self, request, queryset):
|
||||||
import csv
|
import csv
|
||||||
from django.http import HttpResponse
|
from django.http import FileResponse
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
output = StringIO()
|
output = StringIO()
|
||||||
fields = [f.name for f in ExperimentConfig._meta.fields if f.name != "id"] # @UndefinedVariable
|
fields = [f.name for f in ExperimentConfig._meta.fields if f.name != "id"] # @UndefinedVariable
|
||||||
|
|
||||||
writer = csv.DictWriter(output, fieldnames=fields)
|
writer = csv.DictWriter(output, fieldnames=fields)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
for obj in queryset:
|
for obj in queryset:
|
||||||
row = {f: getattr(obj, f) for f in fields}
|
row = {f: getattr(obj, f) for f in fields}
|
||||||
writer.writerow(row)
|
writer.writerow(row)
|
||||||
|
|
||||||
response = HttpResponse(output.getvalue(), content_type="text/csv")
|
response = FileResponse(output.getvalue(), content_type='text/csv')
|
||||||
response["Content-Disposition"] = 'attachment; filename="experiment_configs.csv"'
|
response["Content-Disposition"] = 'attachment; filename="experiment_configs.csv"'
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
# planarian/forms.py
|
|
||||||
|
|
||||||
import csv
|
|
||||||
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
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = ExperimentConfig
|
|
||||||
fields = "__all__"
|
|
||||||
widgets = {
|
|
||||||
"description": forms.Textarea(attrs={"rows": 3}),
|
|
||||||
}
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
cleaned = super().clean()
|
|
||||||
if cleaned.get("thresh_immobile", 0) >= cleaned.get("thresh_mobile", 1):
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_("Le seuil Immobile doit être inférieur au seuil Mobile.")
|
|
||||||
)
|
|
||||||
if cleaned.get("avoid_radius_mm", 0) >= cleaned.get("aggreg_radius_mm", 1):
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_("Le rayon d'évitement doit être inférieur au rayon d'agrégation.")
|
|
||||||
)
|
|
||||||
return cleaned
|
|
||||||
|
|
||||||
|
|
||||||
class CsvImportForm(forms.Form):
|
|
||||||
"""Formulaire d'import de paramètres depuis un fichier CSV."""
|
|
||||||
|
|
||||||
csv_file = forms.FileField(
|
|
||||||
label=_("Fichier CSV"),
|
|
||||||
help_text=_(
|
|
||||||
"Colonnes obligatoires : experiment, well, px_per_mm, fps. "
|
|
||||||
"Toutes les autres colonnes sont optionnelles."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
overwrite = forms.BooleanField(
|
|
||||||
required=False,
|
|
||||||
initial=False,
|
|
||||||
label=_("Écraser les configurations existantes"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def clean_csv_file(self):
|
|
||||||
f = self.cleaned_data["csv_file"]
|
|
||||||
try:
|
|
||||||
content = f.read().decode("utf-8")
|
|
||||||
reader = csv.DictReader(io.StringIO(content))
|
|
||||||
rows = list(reader)
|
|
||||||
except Exception as e:
|
|
||||||
raise forms.ValidationError(_("Fichier CSV invalide : %(err)s") % {"err": e})
|
|
||||||
|
|
||||||
required = {"experiment", "well", "px_per_mm", "fps"}
|
|
||||||
if rows:
|
|
||||||
missing = required - set(rows[0].keys())
|
|
||||||
if missing:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_("Colonnes manquantes : %(cols)s") % {"cols": ", ".join(missing)}
|
|
||||||
)
|
|
||||||
self.csv_rows = rows
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
class ExportCsvForm(forms.Form):
|
|
||||||
"""Formulaire de demande d'export CSV depuis ReductStore."""
|
|
||||||
|
|
||||||
experiment = forms.CharField(label=_("Expérience"), max_length=100)
|
|
||||||
#well = forms.CharField(label=_("Puits"), max_length=20)
|
|
||||||
well = forms.ModelChoiceField(
|
|
||||||
queryset=models.Well.objects.filter().order_by('name').all(),
|
|
||||||
label=_('Puits'),
|
|
||||||
widget=forms.Select(attrs={'class': 'w3-select',}),
|
|
||||||
required=True,initial="D1",
|
|
||||||
)
|
|
||||||
planarian = forms.IntegerField(label=_("Index planaire"), initial=0, min_value=0, max_value=20)
|
|
||||||
record_type = forms.ChoiceField(
|
|
||||||
label=_("Type d'enregistrement"),
|
|
||||||
choices=[("frame", _("Frame par frame")), ("summary", _("Résumé"))],
|
|
||||||
initial="frame",
|
|
||||||
)
|
|
||||||
start_dt = forms.DateTimeField(
|
|
||||||
label=_("Début (UTC)"),
|
|
||||||
required=False,
|
|
||||||
widget=forms.DateTimeInput(attrs={"type": "datetime-local"}),
|
|
||||||
)
|
|
||||||
stop_dt = forms.DateTimeField(
|
|
||||||
label=_("Fin (UTC)"),
|
|
||||||
required=False,
|
|
||||||
widget=forms.DateTimeInput(attrs={"type": "datetime-local"}),
|
|
||||||
)
|
|
||||||
|
|
||||||
@@ -6,6 +6,12 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from scanner.models import Experiment, Well
|
from scanner.models import Experiment, Well
|
||||||
|
|
||||||
|
WELL_CHOICES = []
|
||||||
|
def get_well_choices():
|
||||||
|
wells = Well.objects.order_by('name').all()
|
||||||
|
for well in wells:
|
||||||
|
WELL_CHOICES.append((well.name, well.name))
|
||||||
|
|
||||||
|
|
||||||
class ExperimentConfig(models.Model):
|
class ExperimentConfig(models.Model):
|
||||||
"""
|
"""
|
||||||
@@ -13,12 +19,14 @@ class ExperimentConfig(models.Model):
|
|||||||
Peut être créé depuis Django admin, une vue formulaire ou un import CSV.
|
Peut être créé depuis Django admin, une vue formulaire ou un import CSV.
|
||||||
"""
|
"""
|
||||||
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
|
||||||
experiment = models.ForeignKey(Experiment, verbose_name="Expérience", on_delete=models.CASCADE, related_name="experiment_well", null=True, blank=False)
|
experiment_key = models.ForeignKey(Experiment, verbose_name="Expérience", on_delete=models.CASCADE, null=True, blank=False)
|
||||||
well = models.ForeignKey(Well, verbose_name="Puit", on_delete=models.CASCADE, related_name="well_experiment", null=True, blank=False )
|
experiment = models.CharField(_("Identifiant expérience"), max_length=128, null=True, blank=False, default='Identifier' )
|
||||||
|
well = models.CharField(_("Puit"), help_text=_("Nom du puit"), max_length=8, choices=WELL_CHOICES, null=True, blank=False, default='A1' )
|
||||||
|
|
||||||
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)
|
active = models.BooleanField(_("Active"), default=True)
|
||||||
|
|
||||||
# --- Calibration optique ---
|
# --- Calibration optique ---
|
||||||
@@ -117,24 +125,32 @@ class ExperimentConfig(models.Model):
|
|||||||
avoid_radius_mm = models.FloatField(default=3.0, verbose_name=_("Rayon évitement (mm)"))
|
avoid_radius_mm = models.FloatField(default=3.0, verbose_name=_("Rayon évitement (mm)"))
|
||||||
aggreg_radius_mm = models.FloatField(default=6.0, verbose_name=_("Rayon agrégation (mm)"))
|
aggreg_radius_mm = models.FloatField(default=6.0, verbose_name=_("Rayon agrégation (mm)"))
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("Configuration d'une expérience")
|
verbose_name = _("Configuration d'une expérience")
|
||||||
verbose_name_plural = _("Configurations des expériences")
|
verbose_name_plural = _("Configurations des expériences")
|
||||||
unique_together = ("experiment", "well")
|
unique_together = ("experiment_key", "well")
|
||||||
ordering = ["-created_at"]
|
ordering = ["-created_at"]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.experiment}:{self.well.name}"
|
return f"{self.experiment_key}:{self.well}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.author:
|
||||||
|
self.author = self.experiment_key.author
|
||||||
|
self.experiment = self.experiment_key.identifier
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_session(self):
|
def get_session(self):
|
||||||
return self.experiment.session_experiments.first() if self.experiment else None
|
return self.experiment_key.session_experiments.first() if self.experiment_key else None
|
||||||
|
|
||||||
|
|
||||||
def to_params_dict(self) -> dict:
|
def to_params_dict(self) -> dict:
|
||||||
"""Retourne un dict compatible avec ExperimentParams."""
|
"""Retourne un dict compatible avec ExperimentParams."""
|
||||||
return {
|
return {
|
||||||
"experiment": self.experiment.identifier,
|
"experiment": self.experiment,
|
||||||
"well": self.well.name,
|
"well": self.well,
|
||||||
|
|
||||||
"px_per_mm": self.px_per_mm,
|
"px_per_mm": self.px_per_mm,
|
||||||
"fps": self.fps,
|
"fps": self.fps,
|
||||||
"well_radius_mm": self.well_radius_mm,
|
"well_radius_mm": self.well_radius_mm,
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# tasks.py
|
||||||
|
from celery import shared_task
|
||||||
|
from celery.utils.log import get_task_logger
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from modules.planarian_metrics import ReductStoreClient
|
||||||
|
from scanner.models import SessionExperiment
|
||||||
|
from .models import ExperimentConfig
|
||||||
|
|
||||||
|
|
||||||
|
logger = get_task_logger(__name__)
|
||||||
|
|
||||||
|
def _get_reduct_client() -> ReductStoreClient:
|
||||||
|
"""Instancie le client ReductStore depuis les settings Django."""
|
||||||
|
return ReductStoreClient(url=settings.REDUCTSTORE_URL, token=settings.REDUCTSTORE_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def export_experiment_metrics(experiment):
|
||||||
|
|
||||||
|
@async_to_sync
|
||||||
|
async def _do_export(well, planarian, record_type):
|
||||||
|
client = _get_reduct_client()
|
||||||
|
|
||||||
|
await client.connect()
|
||||||
|
try:
|
||||||
|
f, n = await client.export_csv(
|
||||||
|
experiment = experiment.identifier,
|
||||||
|
well = well,
|
||||||
|
planarian = planarian,
|
||||||
|
record_type = record_type,
|
||||||
|
output_dir = settings.CSV_EXPORT_DIR,
|
||||||
|
)
|
||||||
|
logger.warning(f"Export CSV {settings.CSV_EXPORT_DIR}/{f} done, {n} lignes")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur export CSV: {e}")
|
||||||
|
|
||||||
|
experiment_configs = ExperimentConfig.objects.filter(experiment_key_id=experiment.id).order_by('well').all()
|
||||||
|
for conf in experiment_configs:
|
||||||
|
well = conf.well
|
||||||
|
count = conf.planarian_count
|
||||||
|
|
||||||
|
for record_type in ["frame", "summary"]:
|
||||||
|
for planarian in range(count):
|
||||||
|
_do_export(well, planarian, record_type)
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def export_session_metrics(session):
|
||||||
|
experiments = SessionExperiment.experiment_by_session(session.id)
|
||||||
|
for experiment in experiments:
|
||||||
|
export_experiment_metrics(experiment)
|
||||||
|
|
||||||
|
|
||||||
@@ -23,15 +23,15 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% if current_session.id and current_experiment.id %}
|
{% if current_session and current_experiment %}
|
||||||
<hr class="w3-bar-item divider w3-grey">
|
{% url "planarian:api-export-csv" as url_export %}
|
||||||
<form method="post" class="w3-bar-item" action="{% url 'planarian:export-csv' %}">
|
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="export_csv('{{ url_export }}', {{ current_experiment.id }}, 'experiment_csv')">
|
||||||
{% csrf_token %}
|
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter les metrics de l'expérience" %}<br>
|
||||||
<input type="hidden" name="_sid" value="{{ current_session.id }}">
|
<span class="w3-margin-left-5"> $> {{ export_csv_destination }}
|
||||||
<input type="hidden" name="_expid" value="{{ current_experiment.id }}">
|
</a>
|
||||||
<button type="submit" class="w3-button w3-blue w3-round w3-padding w3-margin-top">📥 {% trans "Exporter CSV" %}</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_footer %}
|
{% block js_footer %}
|
||||||
@@ -53,5 +53,20 @@
|
|||||||
}, 15000); // 15 seconds
|
}, 15000); // 15 seconds
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
function export_csv(url, pid, mode) {
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: mode,
|
||||||
|
pid: pid,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(res => { alert(res.msg); })
|
||||||
|
.catch(error => { console.error('Erreur:', error); });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,540 +0,0 @@
|
|||||||
{% extends "planarian/base.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="w3-container w3-padding-small" style="max-width:960px; margin:auto;">
|
|
||||||
|
|
||||||
<!-- En-tête de la page -->
|
|
||||||
<div class="w3-panel w3-teal w3-round-large w3-padding-16" style="margin-bottom:2rem;">
|
|
||||||
<div class="w3-row w3-bar-item">
|
|
||||||
<span class="w3-xlarge">
|
|
||||||
<i class="w3-margin-right">🔬</i>
|
|
||||||
{% if object %}
|
|
||||||
{% trans "Modifier la configuration" %} — {{ object }}
|
|
||||||
{% else %}
|
|
||||||
{% trans "Nouvelle configuration d'expérience" %}
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Messages Django -->
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="w3-panel w3-round
|
|
||||||
{% if message.tags == 'success' %}w3-green
|
|
||||||
{% elif message.tags == 'error' %}w3-red
|
|
||||||
{% elif message.tags == 'warning' %}w3-orange
|
|
||||||
{% else %}w3-blue{% endif %}">
|
|
||||||
<p>{{ message }}</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<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">
|
|
||||||
{% for error in form.non_field_errors %}
|
|
||||||
<p>⚠ {{ error }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
Section 1 : Identification
|
|
||||||
============================================================ -->
|
|
||||||
<div class="w3-card-4 w3-round-large w3-margin-bottom w3-border w3-round w3-round-large">
|
|
||||||
<header class="w3-container w3-teal w3-round w3-round-large">
|
|
||||||
<h3 class="w3-text-white">{% trans "Identification" %}</h3>
|
|
||||||
</header>
|
|
||||||
<div class="w3-container w3-padding">
|
|
||||||
<div class="w3-row w3-row-padding">
|
|
||||||
|
|
||||||
<!-- author -->
|
|
||||||
<div class="w3-half w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.author.label }}</b></label>
|
|
||||||
{{ form.author }}
|
|
||||||
{% if form.author.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.author.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Experiment -->
|
|
||||||
<div class="w3-half w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.experiment.label }}</b></label>
|
|
||||||
{{ form.experiment }}
|
|
||||||
{% if form.experiment.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.experiment.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Well -->
|
|
||||||
<div class="w3-half w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.well.label }}</b></label>
|
|
||||||
{{ form.well }}
|
|
||||||
{% if form.well.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.well.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<div class="w3-margin-bottom w3-padding">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.description.label }}</b></label>
|
|
||||||
{{ form.description }}
|
|
||||||
{% if form.description.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.description.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
Section 2 : Calibration optique
|
|
||||||
============================================================ -->
|
|
||||||
<div class="w3-card-4 w3-round-large w3-margin-bottom w3-border w3-round w3-round-large">
|
|
||||||
<header class="w3-container w3-blue-grey w3-round w3-round-top-large">
|
|
||||||
<h3 class="w3-text-white">{% trans "Calibration optique" %}</h3>
|
|
||||||
</header>
|
|
||||||
<div class="w3-container w3-padding-24">
|
|
||||||
<div class="w3-row w3-row-padding">
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-blue-grey"><b>{{ form.px_per_mm.label }}</b></label>
|
|
||||||
{% if form.px_per_mm.help_text %}
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{{ form.px_per_mm.help_text }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{{ form.px_per_mm }}
|
|
||||||
{% if form.px_per_mm.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.px_per_mm.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-blue-grey"><b>{{ form.fps.label }}</b></label>
|
|
||||||
{% if form.fps.help_text %}
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{{ form.fps.help_text }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{{ form.fps }}
|
|
||||||
{% if form.fps.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.fps.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-blue-grey"><b>{{ form.well_radius_mm.label }}</b></label>
|
|
||||||
{% if form.well_radius_mm.help_text %}
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{{ form.well_radius_mm.help_text }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{{ form.well_radius_mm }}
|
|
||||||
{% if form.well_radius_mm.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.well_radius_mm.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- ============================================================
|
|
||||||
Section 4 : Tracker
|
|
||||||
============================================================ -->
|
|
||||||
<div class="w3-card-4 w3-round-large w3-margin-bottom w3-border w3-round w3-round-large">
|
|
||||||
<header class="w3-container w3-dark-grey w3-round w3-round-top-large">
|
|
||||||
<h3 class="w3-text-white">{% trans "Tracker" %}</h3>
|
|
||||||
</header>
|
|
||||||
<div class="w3-container w3-padding-24">
|
|
||||||
<div class="w3-row-padding">
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-grey"><b>{{ form.tube_axis.label }}</b></label>
|
|
||||||
{{ form.tube_axis }}
|
|
||||||
{% if form.tube_axis.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.tube_axis.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-grey"><b>{{ form.min_area_px.label }}</b></label>
|
|
||||||
{{ form.min_area_px }}
|
|
||||||
{% if form.min_area_px.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.min_area_px.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-grey"><b>{{ form.max_area_ratio.label }}</b></label>
|
|
||||||
{{ form.max_area_ratio }}
|
|
||||||
{% if form.max_area_ratio.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.max_area_ratio.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-grey"><b>{{ form.planarian_count.label }}</b></label>
|
|
||||||
{{ form.planarian_count }}
|
|
||||||
{% if form.planarian_count.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.planarian_count.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-grey"><b>{{ form.min_contour_dist_px.label }}</b></label>
|
|
||||||
{{ form.min_contour_dist_px }}
|
|
||||||
{% if form.min_contour_dist_px.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.min_contour_dist_px.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-grey"><b>{{ form.merge_kernel_size.label }}</b></label>
|
|
||||||
{{ form.merge_kernel_size }}
|
|
||||||
{% if form.merge_kernel_size.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.merge_kernel_size.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- ============================================================
|
|
||||||
Section 3 : Seuils de mobilité EthoVision
|
|
||||||
============================================================ -->
|
|
||||||
<div class="w3-card-4 w3-round-large w3-margin-bottom w3-border w3-round w3-round-large">
|
|
||||||
<header class="w3-container w3-indigo w3-round w3-round-top-large">
|
|
||||||
<h3 class="w3-text-white">{% trans "Seuils de mobilité EthoVision XT" %}</h3>
|
|
||||||
</header>
|
|
||||||
<div class="w3-container w3-padding-24">
|
|
||||||
|
|
||||||
<!-- Visualisation des seuils -->
|
|
||||||
<div class="w3-light-grey w3-round w3-margin-bottom" style="height:32px; position:relative; overflow:hidden;">
|
|
||||||
<div style="position:absolute; left:0; width:13%; height:100%; background:#c0392b; display:flex; align-items:center; justify-content:center;">
|
|
||||||
<span class="w3-small w3-text-white"><b>{% trans "Immobile" %}</b></span>
|
|
||||||
</div>
|
|
||||||
<div style="position:absolute; left:13%; width:27%; height:100%; background:#e67e22; display:flex; align-items:center; justify-content:center;">
|
|
||||||
<span class="w3-small w3-text-white"><b>{% trans "Mobile" %}</b></span>
|
|
||||||
</div>
|
|
||||||
<div style="position:absolute; left:40%; right:0; height:100%; background:#27ae60; display:flex; align-items:center; justify-content:center;">
|
|
||||||
<span class="w3-small w3-text-white"><b>{% trans "Très mobile" %}</b></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin-top:-8px; margin-bottom:16px;">
|
|
||||||
{% trans "Représentation indicative des zones de mobilité (défauts EthoVision : 0.2 / 1.5 mm/s)" %}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="w3-row-padding">
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-indigo"><b>{{ form.thresh_immobile.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{% trans "En-dessous : Immobile (mm/s)" %}</p>
|
|
||||||
{{ form.thresh_immobile }}
|
|
||||||
{% if form.thresh_immobile.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.thresh_immobile.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-indigo"><b>{{ form.thresh_mobile.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{% trans "En-dessous : Mobile, au-delà : Très mobile (mm/s)" %}</p>
|
|
||||||
{{ form.thresh_mobile }}
|
|
||||||
{% if form.thresh_mobile.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.thresh_mobile.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
Section 5 : Comportements
|
|
||||||
============================================================ -->
|
|
||||||
<div class="w3-card-4 w3-round-large w3-margin-bottom w3-border w3-round w3-round-large">
|
|
||||||
<header class="w3-container w3-teal w3-round w3-round-top-large">
|
|
||||||
<h3 class="w3-text-white">{% trans "Comportements" %}</h3>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- --- Thigmotactisme --- -->
|
|
||||||
<div class="w3-container w3-padding-16 w3-border-bottom">
|
|
||||||
<button type="button" class="w3-button w3-block w3-left-align w3-hover-light-grey"
|
|
||||||
onclick="toggleSection('thigmo')">
|
|
||||||
<span class="w3-large">🫧</span>
|
|
||||||
<b class="w3-margin-left">{% trans "Thigmotactisme" %}</b>
|
|
||||||
<span class="w3-small w3-text-grey w3-margin-left">
|
|
||||||
{% trans "Attraction vers la paroi du puits" %}
|
|
||||||
</span>
|
|
||||||
<span id="thigmo-icon" class="w3-right">▼</span>
|
|
||||||
</button>
|
|
||||||
<div id="thigmo" class="w3-padding-top">
|
|
||||||
<div class="w3-row-padding">
|
|
||||||
<div class="w3-col m6 s12">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.thigmotaxis_wall_dist_mm.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">
|
|
||||||
{% trans "Distance à la paroi considérée « près du bord » (mm)" %}
|
|
||||||
</p>
|
|
||||||
{{ form.thigmotaxis_wall_dist_mm }}
|
|
||||||
{% if form.thigmotaxis_wall_dist_mm.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.thigmotaxis_wall_dist_mm.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- --- Phototactisme --- -->
|
|
||||||
<div class="w3-container w3-padding-16 w3-border-bottom">
|
|
||||||
<button type="button" class="w3-button w3-block w3-left-align w3-hover-light-grey" onclick="toggleSection('photo')">
|
|
||||||
<span class="w3-large">💡</span>
|
|
||||||
<b class="w3-margin-left">{% trans "Phototactisme" %}</b>
|
|
||||||
<span class="w3-small w3-text-grey w3-margin-left">
|
|
||||||
{% trans "Fuite de la lumière" %}
|
|
||||||
</span>
|
|
||||||
<span id="photo-icon" class="w3-right">▼</span>
|
|
||||||
</button>
|
|
||||||
<div id="photo" class="w3-hide w3-padding-top">
|
|
||||||
<div class="w3-row-padding">
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.photo_mode.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;"> </p>
|
|
||||||
{{ form.photo_mode }}
|
|
||||||
{% if form.photo_mode.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.photo_mode.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.photo_strength.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{% trans "0.0 = désactivé → 1.0 = fort" %}</p>
|
|
||||||
{{ form.photo_strength }}
|
|
||||||
{% if form.photo_strength.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.photo_strength.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.photo_x.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;"> </p>
|
|
||||||
{{ form.photo_x }}
|
|
||||||
{% if form.photo_x.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.photo_x.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.photo_y.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;"> </p>
|
|
||||||
{{ form.photo_y }}
|
|
||||||
{% if form.photo_y.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.photo_y.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- --- Chimiotactisme --- -->
|
|
||||||
<div class="w3-container w3-padding-16 w3-border-bottom">
|
|
||||||
<button type="button" class="w3-button w3-block w3-left-align w3-hover-light-grey"
|
|
||||||
onclick="toggleSection('chemo')">
|
|
||||||
<span class="w3-large">🍖</span>
|
|
||||||
<b class="w3-margin-left">{% trans "Chimiotactisme" %}</b>
|
|
||||||
<span class="w3-small w3-text-grey w3-margin-left">
|
|
||||||
{% trans "Attraction vers une source de nourriture" %}
|
|
||||||
</span>
|
|
||||||
<span id="chemo-icon" class="w3-right">▼</span>
|
|
||||||
</button>
|
|
||||||
<div id="chemo" class="w3-hide w3-padding-top">
|
|
||||||
<div class="w3-row-padding">
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.chemo_strength.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{% trans "0.0 = désactivé → 1.0 = fort" %}</p>
|
|
||||||
{{ form.chemo_strength }}
|
|
||||||
{% if form.chemo_strength.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.chemo_strength.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.chemo_x.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{% trans "Mini = 0, maxi = 1" %}</p>
|
|
||||||
{{ form.chemo_x }}
|
|
||||||
{% if form.chemo_x.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.chemo_x.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.chemo_y.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{% trans "Mini = 0, maxi = 1" %}</p>
|
|
||||||
{{ form.chemo_y }}
|
|
||||||
{% if form.chemo_y.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.chemo_y.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.chemo_radius_mm.label }}</b></label>
|
|
||||||
{{ form.chemo_radius_mm }}
|
|
||||||
{% if form.chemo_radius_mm.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.chemo_radius_mm.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- --- Interactions inter-individus --- -->
|
|
||||||
<div class="w3-container w3-padding-16">
|
|
||||||
<button type="button" class="w3-button w3-block w3-left-align w3-hover-light-grey"
|
|
||||||
onclick="toggleSection('social')">
|
|
||||||
<span class="w3-large">🔀</span>
|
|
||||||
<b class="w3-margin-left">{% trans "Interactions inter-individus" %}</b>
|
|
||||||
<span class="w3-small w3-text-grey w3-margin-left">
|
|
||||||
{% trans "Évitement et agrégation" %}
|
|
||||||
</span>
|
|
||||||
<span id="social-icon" class="w3-right">▼</span>
|
|
||||||
</button>
|
|
||||||
<div id="social" class="w3-hide w3-padding-top">
|
|
||||||
<div class="w3-row-padding">
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.avoid_radius_mm.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{% trans "Rayon de répulsion courte portée (mm)" %}</p>
|
|
||||||
{{ form.avoid_radius_mm }}
|
|
||||||
{% if form.avoid_radius_mm.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.avoid_radius_mm.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-teal"><b>{{ form.aggreg_radius_mm.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{% trans "Rayon d'attraction longue portée — doit être > rayon évitement (mm)" %}</p>
|
|
||||||
{{ form.aggreg_radius_mm }}
|
|
||||||
{% if form.aggreg_radius_mm.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.aggreg_radius_mm.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- fin card comportements -->
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
Boutons d'action
|
|
||||||
============================================================ -->
|
|
||||||
<div class="w3-row-padding w3-margin-top">
|
|
||||||
|
|
||||||
<div class="w3-col m8 s12 w3-margin-bottom">
|
|
||||||
<button type="submit" class="w3-button w3-teal w3-round w3-large w3-padding-large">
|
|
||||||
{% if is_update %}
|
|
||||||
💾 {% trans "Enregistrer les modifications" %}
|
|
||||||
{% else %}
|
|
||||||
➕ {% trans "Créer la configuration" %}
|
|
||||||
{% endif %}
|
|
||||||
</button>
|
|
||||||
<a href="{% url 'planarian:experiment-list' %}"
|
|
||||||
class="w3-button w3-light-grey w3-round w3-large w3-padding-large w3-margin-left">
|
|
||||||
✖ {% trans "Retour" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if object %}
|
|
||||||
<div class="w3-col m4 s12 w3-right-align w3-margin-bottom">
|
|
||||||
<a href="{% url 'planarian:export-csv' %}?experiment={{ object.experiment }}&well={{ object.well }}"
|
|
||||||
class="w3-button w3-blue w3-round w3-large w3-padding-large">
|
|
||||||
📥 {% trans "Exporter CSV" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form><!-- fin form -->
|
|
||||||
|
|
||||||
</div><!-- fin container -->
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
Styles W3.CSS pour les champs de formulaire Django
|
|
||||||
(Django génère des <input> bruts sans classes — on les stylise ici)
|
|
||||||
============================================================ -->
|
|
||||||
<style>
|
|
||||||
/* Champs texte, number, select */
|
|
||||||
input[type="text"],
|
|
||||||
input[type="number"],
|
|
||||||
input[type="email"],
|
|
||||||
textarea,
|
|
||||||
select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 15px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin-top: 2px;
|
|
||||||
background-color: #fafafa;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
input[type="text"]:focus,
|
|
||||||
input[type="number"]:focus,
|
|
||||||
textarea:focus,
|
|
||||||
select:focus {
|
|
||||||
border-color: #009688; /* teal W3.CSS */
|
|
||||||
outline: none;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
/* Champ en erreur */
|
|
||||||
input.errorfield, select.errorfield, textarea.errorfield {
|
|
||||||
border-color: #f44336;
|
|
||||||
background-color: #fff8f8;
|
|
||||||
}
|
|
||||||
/* Label au-dessus du champ */
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
/* Sections accordéon */
|
|
||||||
[id$="-icon"] {
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
/**
|
|
||||||
* Bascule la visibilité d'une section accordéon.
|
|
||||||
* @param {string} id - Identifiant de la section (sans le préfixe)
|
|
||||||
*/
|
|
||||||
function toggleSection(id) {
|
|
||||||
const section = document.getElementById(id);
|
|
||||||
const icon = document.getElementById(id + '-icon');
|
|
||||||
if (section.classList.contains('w3-hide')) {
|
|
||||||
section.classList.remove('w3-hide');
|
|
||||||
if (icon) icon.textContent = '▲';
|
|
||||||
} else {
|
|
||||||
section.classList.add('w3-hide');
|
|
||||||
if (icon) icon.textContent = '▼';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ouvrir automatiquement les sections qui contiennent des erreurs */
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
['thigmo', 'photo', 'chemo', 'social'].forEach(function (id) {
|
|
||||||
const section = document.getElementById(id);
|
|
||||||
if (section && section.querySelector('.w3-text-red')) {
|
|
||||||
section.classList.remove('w3-hide');
|
|
||||||
const icon = document.getElementById(id + '-icon');
|
|
||||||
if (icon) icon.textContent = '▲';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
{% extends "planarian/base.html" %}
|
|
||||||
{% load i18n home_tags scanner_tags %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="w3-container w3-padding" style="max-width:1200px; margin:auto;">
|
|
||||||
<!-- En-tête -->
|
|
||||||
<div class="w3-panel w3-teal w3-round-large w3-padding-16 w3-margin-bottom">
|
|
||||||
<div class="w3-row">
|
|
||||||
<div class="w3-col s12 m8 w3-bar-item">
|
|
||||||
<span class="w3-xlarge">
|
|
||||||
<i class="w3-margin-right">🔬</i>
|
|
||||||
{% trans "Configurations d'expériences" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="w3-col s12 m4 w3-bar-item w3-right-align" style="padding-top:6px;">
|
|
||||||
<a href="{% url 'planarian:experiment-new' %}"
|
|
||||||
class="w3-button w3-white w3-round w3-text-teal">
|
|
||||||
➕ {% trans "Nouvelle configuration" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Messages Django -->
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="w3-panel w3-round alert
|
|
||||||
{% if message.tags == 'success' %}w3-green
|
|
||||||
{% elif message.tags == 'error' %}w3-red
|
|
||||||
{% elif message.tags == 'warning' %}w3-orange
|
|
||||||
{% else %}w3-blue{% endif %}">
|
|
||||||
<button class="w3-btn w3-right w3-bold w3-black w3-round w3-margin" onclick="s('.alert').remove()">X</button>
|
|
||||||
<p>{{ message }}</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Barre d'outils : recherche + import -->
|
|
||||||
<div class="w3-row-padding w3-margin-bottom">
|
|
||||||
<!-- Recherche côté client -->
|
|
||||||
<div class="w3-col m7 s12 w3-margin-bottom">
|
|
||||||
<div class="w3-border w3-round" style="display:flex; align-items:center; background:#fafafa;">
|
|
||||||
<span style="padding:0 10px; font-size:18px; color:#888;">🔍</span>
|
|
||||||
<input type="text" id="search-input"
|
|
||||||
placeholder="{% trans 'Filtrer par expérience, puits…' %}"
|
|
||||||
class="w3-input w3-border-0"
|
|
||||||
style="background:transparent;"
|
|
||||||
oninput="filterTable(this.value)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Boutons import / export -->
|
|
||||||
<div class="w3-col m5 s12 w3-right-align w3-margin-bottom">
|
|
||||||
<a href="{% url 'planarian:import-params' %}"
|
|
||||||
class="w3-button w3-blue-grey w3-round w3-margin-right">
|
|
||||||
📂 {% trans "Importer CSV" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'planarian:export-csv' %}"
|
|
||||||
class="w3-button w3-blue w3-round">
|
|
||||||
📥 {% trans "Exporter CSV" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- ============================================================
|
|
||||||
Tableau des configurations
|
|
||||||
============================================================ -->
|
|
||||||
{% if configs %}
|
|
||||||
<!-- Compteur -->
|
|
||||||
<p class="w3-text-grey w3-small" id="row-count">
|
|
||||||
{{ configs|length }} {% trans "configuration(s)" %}
|
|
||||||
</p>
|
|
||||||
<div class="w3-responsive w3-card-4 w3-round-large">
|
|
||||||
<table class="w3-table w3-striped w3-hoverable w3-bordered" id="config-table">
|
|
||||||
<thead>
|
|
||||||
<tr class="w3-teal">
|
|
||||||
<th onclick="sortTable(0)" class="sortable" title="{% trans 'Trier' %}">
|
|
||||||
{% trans "Expérience" %} <span class="sort-icon">↕</span>
|
|
||||||
</th>
|
|
||||||
<th onclick="sortTable(1)" class="sortable" title="{% trans 'Trier' %}">
|
|
||||||
{% trans "Puits" %} <span class="sort-icon">↕</span>
|
|
||||||
</th>
|
|
||||||
<th onclick="sortTable(2)" class="sortable w3-hide-small" title="{% trans 'Trier' %}">
|
|
||||||
{% trans "px/mm" %} <span class="sort-icon">↕</span>
|
|
||||||
</th>
|
|
||||||
<th class="w3-hide-small">{% trans "FPS" %}</th>
|
|
||||||
<th class="w3-hide-small">{% trans "Seuils (mm/s)" %}</th>
|
|
||||||
<th class="w3-hide-small">{% trans "Comportements" %}</th>
|
|
||||||
<th onclick="sortTable(6)" class="sortable w3-hide-small" title="{% trans 'Trier' %}">
|
|
||||||
{% trans "Créé le" %} <span class="sort-icon">↕</span>
|
|
||||||
</th>
|
|
||||||
<th>{% trans "Actions" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="config-tbody">
|
|
||||||
{% for cfg in configs %}
|
|
||||||
<tr class="config-row">
|
|
||||||
<!-- Expérience -->
|
|
||||||
<td>
|
|
||||||
<b><span class="w3-text-grey">{{ cfg.experiment }}</span></b>
|
|
||||||
{% if cfg.description %}
|
|
||||||
<br><span class="w3-small w3-text-blue-grey">{{ cfg.description|truncatechars:40 }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<!-- Puits -->
|
|
||||||
<td>
|
|
||||||
<span class="w3-tag w3-teal w3-round">{{ cfg.well }}</span>
|
|
||||||
</td>
|
|
||||||
<!-- px/mm -->
|
|
||||||
<td class="w3-hide-small w3-text-grey">{{ cfg.px_per_mm }}</td>
|
|
||||||
<!-- FPS -->
|
|
||||||
<td class="w3-hide-small w3-text-grey">{{ cfg.fps }}</td>
|
|
||||||
<!-- Seuils de mobilité -->
|
|
||||||
<td class="w3-hide-small">
|
|
||||||
<span class="w3-tag w3-red w3-round w3-small"
|
|
||||||
title="{% trans 'Immobile' %}">
|
|
||||||
< {{ cfg.thresh_immobile }}
|
|
||||||
</span>
|
|
||||||
<span class="w3-tag w3-orange w3-round w3-small"
|
|
||||||
title="{% trans 'Mobile' %}">
|
|
||||||
< {{ cfg.thresh_mobile }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<!-- Comportements actifs -->
|
|
||||||
<td class="w3-hide-small">
|
|
||||||
{% if cfg.thigmotaxis_wall_dist_mm > 0 %}
|
|
||||||
<span class="w3-tag w3-blue-grey w3-round w3-small w3-margin-right"
|
|
||||||
title="{% trans 'Thigmotactisme actif' %}">🫧</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if cfg.photo_mode != 'none' and cfg.photo_strength > 0 %}
|
|
||||||
<span class="w3-tag w3-yellow w3-round w3-small w3-margin-right"
|
|
||||||
title="{% trans 'Phototactisme actif' %} ({{ cfg.photo_mode }})">💡</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if cfg.chemo_strength > 0 %}
|
|
||||||
<span class="w3-tag w3-green w3-round w3-small w3-margin-right"
|
|
||||||
title="{% trans 'Chimiotactisme actif' %}">🍖</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if cfg.avoid_radius_mm > 0 or cfg.aggreg_radius_mm > 0 %}
|
|
||||||
<span class="w3-tag w3-purple w3-round w3-small"
|
|
||||||
title="{% trans 'Interactions inter-individus' %}">🔀</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<!-- Date de création -->
|
|
||||||
<td class="w3-hide-small w3-small w3-text-grey">
|
|
||||||
{{ cfg.created_at|date:"d/m/Y H:i" }}
|
|
||||||
</td>
|
|
||||||
<!-- Actions -->
|
|
||||||
<td style="white-space:nowrap;">
|
|
||||||
{% if current_session and current_experiment %}
|
|
||||||
<a href="{% url 'planarian:experiment-edit' cfg.pk current_session.id current_experiment.id %}"
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'planarian:experiment-edit' cfg.pk %}"
|
|
||||||
{% endif %}
|
|
||||||
class="w3-button w3-small w3-teal w3-round"
|
|
||||||
title="{% trans 'Modifier' %}">✏</a>
|
|
||||||
<a href="{% url 'planarian:export-csv' %}?experiment={{ cfg.experiment }}&well={{ cfg.well }}"
|
|
||||||
class="w3-button w3-small w3-blue w3-round"
|
|
||||||
title="{% trans 'Exporter CSV' %}">📥</a>
|
|
||||||
<button type="button"
|
|
||||||
class="w3-button w3-small w3-red w3-round"
|
|
||||||
title="{% trans 'Supprimer' %}"
|
|
||||||
onclick="confirmDelete('{{ cfg.pk }}', '{{ cfg.experiment }}', '{{ cfg.well }}')"> 🗑 </button>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div><!-- fin responsive -->
|
|
||||||
|
|
||||||
<!-- Message "aucun résultat" (filtrage JS) -->
|
|
||||||
<div id="no-results" class="w3-panel w3-pale-yellow w3-round w3-margin-top" style="display:none;">
|
|
||||||
<p>{% trans "Aucune configuration ne correspond à la recherche." %}</p>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<!-- Liste vide -->
|
|
||||||
<div class="w3-panel w3-pale-blue w3-round-large w3-padding-32" style="text-align:center;">
|
|
||||||
<p style="font-size:3rem; margin:0;">🔬</p>
|
|
||||||
<h3>{% trans "Aucune configuration pour l'instant." %}</h3>
|
|
||||||
<p class="w3-text-grey">
|
|
||||||
{% trans "Créez une première configuration ou importez un fichier CSV." %}
|
|
||||||
</p>
|
|
||||||
<a href="{% url 'planarian:experiment-new' %}"
|
|
||||||
class="w3-button w3-teal w3-round w3-large w3-margin-right">
|
|
||||||
➕ {% trans "Nouvelle configuration" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'planarian:import-params' %}" class="w3-button w3-blue-grey w3-round w3-large"> 📂 {% trans "Importer CSV" %} </a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div><!-- fin container -->
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
Modal de confirmation de suppression
|
|
||||||
============================================================ -->
|
|
||||||
<div id="delete-modal" class="w3-modal">
|
|
||||||
<div class="w3-modal-content w3-round-large w3-card-4" style="max-width:420px; margin:auto; margin-top:10%;">
|
|
||||||
<header class="w3-container w3-red w3-round-top-large">
|
|
||||||
<h3 class="w3-text-white">🗑 {% trans "Confirmer la suppression" %}</h3>
|
|
||||||
</header>
|
|
||||||
<div class="w3-container w3-padding-24">
|
|
||||||
<p id="delete-msg"></p>
|
|
||||||
<form id="delete-form" method="post" action="">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="_method" value="delete">
|
|
||||||
<button type="submit" class="w3-button w3-red w3-round w3-margin-right"> 🗑 {% trans "Supprimer" %}</button>
|
|
||||||
<button type="button" class="w3-button w3-light-grey w3-round"
|
|
||||||
onclick="document.getElementById('delete-modal').style.display='none'">
|
|
||||||
✖ {% trans "Annuler" %}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
Styles
|
|
||||||
============================================================ -->
|
|
||||||
<style>
|
|
||||||
/* En-têtes de colonnes triables */
|
|
||||||
th.sortable {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
th.sortable:hover {
|
|
||||||
background-color: #00796b;
|
|
||||||
}
|
|
||||||
.sort-icon {
|
|
||||||
font-size: 11px;
|
|
||||||
opacity: 0.7;
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
th.sort-asc .sort-icon::after { content: " ▲"; opacity: 1; }
|
|
||||||
th.sort-desc .sort-icon::after { content: " ▼"; opacity: 1; }
|
|
||||||
|
|
||||||
/* Tableau responsive */
|
|
||||||
#config-table td, #config-table th {
|
|
||||||
vertical-align: middle;
|
|
||||||
padding: 10px 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- ============================================================
|
|
||||||
JavaScript : filtrage + tri + modal suppression
|
|
||||||
============================================================ -->
|
|
||||||
<script>
|
|
||||||
// --- Filtrage dynamique ---
|
|
||||||
function filterTable(query) {
|
|
||||||
const q = query.toLowerCase().trim();
|
|
||||||
const rows = document.querySelectorAll('.config-row');
|
|
||||||
const noRes = document.getElementById('no-results');
|
|
||||||
const count = document.getElementById('row-count');
|
|
||||||
let visible = 0;
|
|
||||||
|
|
||||||
rows.forEach(function (row) {
|
|
||||||
const text = row.textContent.toLowerCase();
|
|
||||||
if (!q || text.includes(q)) {
|
|
||||||
row.style.display = '';
|
|
||||||
visible++;
|
|
||||||
} else {
|
|
||||||
row.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
noRes.style.display = (visible === 0 && q) ? 'block' : 'none';
|
|
||||||
if (count) {
|
|
||||||
count.textContent = visible + ' {% trans "configuration(s)" %}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Tri de colonnes ---
|
|
||||||
let sortDir = {};
|
|
||||||
|
|
||||||
function sortTable(colIdx) {
|
|
||||||
const tbody = document.getElementById('config-tbody');
|
|
||||||
const rows = Array.from(tbody.querySelectorAll('.config-row'));
|
|
||||||
const th = document.querySelectorAll('#config-table thead th')[colIdx];
|
|
||||||
|
|
||||||
// Alterner ascendant / descendant
|
|
||||||
sortDir[colIdx] = sortDir[colIdx] === 'asc' ? 'desc' : 'asc';
|
|
||||||
|
|
||||||
// Réinitialiser les classes de toutes les colonnes
|
|
||||||
document.querySelectorAll('#config-table thead th').forEach(function (h) {
|
|
||||||
h.classList.remove('sort-asc', 'sort-desc');
|
|
||||||
});
|
|
||||||
th.classList.add(sortDir[colIdx] === 'asc' ? 'sort-asc' : 'sort-desc');
|
|
||||||
|
|
||||||
rows.sort(function (a, b) {
|
|
||||||
const cellA = a.querySelectorAll('td')[colIdx]?.textContent.trim() || '';
|
|
||||||
const cellB = b.querySelectorAll('td')[colIdx]?.textContent.trim() || '';
|
|
||||||
// Tri numérique si possible, sinon alphabétique
|
|
||||||
const numA = parseFloat(cellA);
|
|
||||||
const numB = parseFloat(cellB);
|
|
||||||
const cmp = (!isNaN(numA) && !isNaN(numB))
|
|
||||||
? numA - numB
|
|
||||||
: cellA.localeCompare(cellB, '{{ LANGUAGE_CODE|default:"fr" }}');
|
|
||||||
return sortDir[colIdx] === 'asc' ? cmp : -cmp;
|
|
||||||
});
|
|
||||||
|
|
||||||
rows.forEach(function (row) { tbody.appendChild(row); });
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Modal de suppression ---
|
|
||||||
function confirmDelete(pk, experiment, well) {
|
|
||||||
document.getElementById('delete-msg').textContent =
|
|
||||||
'{% trans "Supprimer la configuration" %} ' + experiment + ' / ' + well + ' ?';
|
|
||||||
document.getElementById('delete-form').action =
|
|
||||||
'{% url "planarian:experiment-list" %}' + pk + '/delete/';
|
|
||||||
document.getElementById('delete-modal').style.display = 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fermer le modal en cliquant en dehors
|
|
||||||
window.onclick = function (e) {
|
|
||||||
const modal = document.getElementById('delete-modal');
|
|
||||||
if (e.target === modal) modal.style.display = 'none';
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -21,7 +21,6 @@
|
|||||||
<input type="hidden" id="_sid" name="_sid" value="{{ current_session.id }}">
|
<input type="hidden" id="_sid" name="_sid" value="{{ current_session.id }}">
|
||||||
<input type="hidden" id="_expid" name="_expid" value="{{ current_experiment.id }}">
|
<input type="hidden" id="_expid" name="_expid" value="{{ current_experiment.id }}">
|
||||||
|
|
||||||
|
|
||||||
<div class="w3-card-4 w3-border w3-round w3-round-large">
|
<div class="w3-card-4 w3-border w3-round w3-round-large">
|
||||||
<header class="w3-container w3-blue w3-round w3-round-top-large">
|
<header class="w3-container w3-blue w3-round w3-round-top-large">
|
||||||
<h3 class="w3-text-white">{% trans "Paramètres d'export" %}</h3>
|
<h3 class="w3-text-white">{% trans "Paramètres d'export" %}</h3>
|
||||||
@@ -161,3 +160,18 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,233 +0,0 @@
|
|||||||
{% extends "planarian/base.html" %}
|
|
||||||
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="w3-container w3-padding" style="max-width:760px; margin:auto;">
|
|
||||||
{% if current_session and current_experiment %}
|
|
||||||
<!-- En-tête -->
|
|
||||||
<div class="w3-panel w3-blue w3-round-large w3-padding-16 w3-margin-bottom">
|
|
||||||
<div class="w3-row">
|
|
||||||
<div class="w3-col s12 m8 w3-bar-item">
|
|
||||||
<span class="w3-xlarge">
|
|
||||||
📥 {% trans "Exporter les données vers CSV" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="w3-col s12 m4 w3-bar-item w3-right-align" style="padding-top:6px;">
|
|
||||||
<a href="/"
|
|
||||||
class="w3-button w3-white w3-round w3-text-blue">
|
|
||||||
← {% trans "Retour" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Messages Django -->
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="w3-panel w3-round
|
|
||||||
{% if message.tags == 'success' %}w3-green
|
|
||||||
{% elif message.tags == 'error' %}w3-red
|
|
||||||
{% elif message.tags == 'warning' %}w3-orange
|
|
||||||
{% else %}w3-blue{% endif %}">
|
|
||||||
<p>{{ message }}</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Explication -->
|
|
||||||
<div class="w3-panel w3-pale-blue w3-round w3-margin-bottom">
|
|
||||||
<p>
|
|
||||||
{% trans "Sélectionnez l'expérience, le puits et le type d'enregistrement à exporter." %}
|
|
||||||
{% trans "Le fichier CSV sera généré depuis ReductStore et téléchargé directement." %}
|
|
||||||
</p>
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0;">
|
|
||||||
{% trans "Colonnes exportées compatibles EthoVision XT : distance, vitesse, états de mobilité, thigmotactisme." %}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 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">
|
|
||||||
{% for error in form.non_field_errors %}
|
|
||||||
<p>⚠ {{ error }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="w3-card-4 w3-border w3-round w3-round-large">
|
|
||||||
<header class="w3-container w3-blue w3-round w3-round-top-large">
|
|
||||||
<h3 class="w3-text-white">{% trans "Paramètres d'export" %}</h3>
|
|
||||||
</header>
|
|
||||||
<div class="w3-container w3-padding-24">
|
|
||||||
<!-- Ligne 1 : expérience + puits -->
|
|
||||||
<div class="w3-row-padding w3-margin-bottom">
|
|
||||||
|
|
||||||
<div class="w3-col m7 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-blue"><b>{{ form.experiment.label }}</b></label>
|
|
||||||
{% if form.experiment.help_text %}
|
|
||||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{{ form.experiment.help_text }}</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{{ form.experiment }}
|
|
||||||
{% if form.experiment.errors %}
|
|
||||||
<!--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>
|
|
||||||
{{ form.well }}
|
|
||||||
{% if form.well.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.well.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ligne 2 : planaire + type d'enregistrement -->
|
|
||||||
<div class="w3-row-padding w3-margin-bottom">
|
|
||||||
|
|
||||||
<div class="w3-col m4 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-blue"><b>{{ form.planarian.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-light-grey" style="margin:0 0 4px;">
|
|
||||||
{% trans "Index du planaire dans le puits (commence à 0)" %}
|
|
||||||
</p>
|
|
||||||
{{ form.planarian }}
|
|
||||||
{% if form.planarian.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.planarian.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m8 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-blue"><b>{{ form.record_type.label }}</b></label>
|
|
||||||
<p class="w3-small w3-text-light-grey" style="margin:0 0 4px;">
|
|
||||||
{% trans "Frame par frame : une ligne par image. Résumé : métriques agrégées de la session." %}
|
|
||||||
</p>
|
|
||||||
<!-- Radio buttons stylisés W3 -->
|
|
||||||
<div class="w3-row-padding" style="margin-top:6px;">
|
|
||||||
{{ form.record_type }}
|
|
||||||
</div>
|
|
||||||
{% if form.record_type.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.record_type.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ligne 3 : plage temporelle (optionnel) -->
|
|
||||||
<div class="w3-border-top w3-padding-top w3-margin-top">
|
|
||||||
<p class="w3-text-blue-grey">
|
|
||||||
<b>{% trans "Plage temporelle" %}</b>
|
|
||||||
<span class="w3-small w3-text-grey w3-margin-left">
|
|
||||||
{% trans "(optionnel — laisser vide pour exporter toute la session)" %}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<div class="w3-row-padding">
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-blue-grey">{{ form.start_dt.label }}</label>
|
|
||||||
{{ form.start_dt }}
|
|
||||||
{% if form.start_dt.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.start_dt.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
|
||||||
<label class="w3-text-blue-grey">{{ form.stop_dt.label }}</label>
|
|
||||||
{{ form.stop_dt }}
|
|
||||||
{% if form.stop_dt.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.stop_dt.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- fin padding -->
|
|
||||||
</div><!-- fin card -->
|
|
||||||
|
|
||||||
<!-- Boutons -->
|
|
||||||
<div class="w3-row-padding w3-margin-top">
|
|
||||||
<div class="w3-col s12">
|
|
||||||
<button type="submit" class="w3-button w3-blue w3-large w3-padding-large">
|
|
||||||
📥 {% trans "Générer et télécharger le CSV" %}
|
|
||||||
</button>
|
|
||||||
<a href="/"
|
|
||||||
class="w3-button w3-light-grey w3-round w3-large w3-padding-large w3-margin-left">
|
|
||||||
✖ {% trans "Annuler" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<!-- Rappel des colonnes exportées -->
|
|
||||||
<div class="w3-card w3-round-large w3-margin-top w3-border w3-round w3-round-large w3-margin-bottom">
|
|
||||||
<header class="w3-container w3-blue-grey w3-round w3-round-top-large">
|
|
||||||
<h4 class="w3-text-white" style="margin:8px 0;">
|
|
||||||
{% trans "Colonnes du fichier CSV exporté" %}
|
|
||||||
</h4>
|
|
||||||
</header>
|
|
||||||
<div class="w3-container w3-padding-16">
|
|
||||||
<div class="w3-row-padding">
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12">
|
|
||||||
<p class="w3-text-blue-grey w3-small"><b>{% trans "Identification / position" %}</b></p>
|
|
||||||
<ul class="w3-ul w3-small w3-border w3-round">
|
|
||||||
<li><code>timestamp</code></li>
|
|
||||||
<li><code>x_mm</code> / <code>y_mm</code></li>
|
|
||||||
<li><code>cx_px</code> / <code>cy_px</code></li>
|
|
||||||
<li><code>area_px</code></li>
|
|
||||||
<li><code>axial_pos</code> / <code>axial_speed</code></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="w3-col m6 s12">
|
|
||||||
<p class="w3-text-blue-grey w3-small"><b>{% trans "Métriques EthoVision XT" %}</b></p>
|
|
||||||
<ul class="w3-ul w3-small w3-border w3-round">
|
|
||||||
<li><code>velocity_mm_s</code> — {% trans "vitesse instantanée" %}</li>
|
|
||||||
<li><code>total_distance_mm</code> — {% trans "distance cumulée" %}</li>
|
|
||||||
<li><code>moving</code> / <code>duration_moving_s</code></li>
|
|
||||||
<li><code>mobility_state</code> — {% trans "Immobile / Mobile / Highly mobile" %}</li>
|
|
||||||
<li><code>dist_to_wall_mm</code> / <code>near_wall</code></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- fin container -->
|
|
||||||
|
|
||||||
<style>
|
|
||||||
input[type="text"],
|
|
||||||
input[type="number"],
|
|
||||||
input[type="datetime-local"],
|
|
||||||
select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 15px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
background-color: #fafafa;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
input:focus, select:focus {
|
|
||||||
border-color: #2196F3;
|
|
||||||
outline: none;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
/* Masquer le radio natif, laisser le label stylisé */
|
|
||||||
.w3-button input[type="radio"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
@@ -3,52 +3,23 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="w3-container w3-padding-32" style="max-width:760px; margin:auto;">
|
<div class="w3-container w3-padding-32" style="max-width:760px; margin:auto;">
|
||||||
|
{% if current_session and current_experiment %}
|
||||||
<!-- En-tête -->
|
{% include "inc/alert.html" %}
|
||||||
<div class="w3-panel w3-blue-grey w3-round-large w3-padding-16 w3-margin-bottom">
|
|
||||||
<div class="w3-row">
|
|
||||||
<div class="w3-col s12 m8 w3-bar-item">
|
|
||||||
<span class="w3-xlarge">
|
|
||||||
📂 {% trans "Importer des configurations depuis CSV" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="w3-col s12 m4 w3-bar-item w3-right-align" style="padding-top:6px;">
|
|
||||||
<a href="{% url 'planarian:experiment-list' %}"
|
|
||||||
class="w3-button w3-white w3-round w3-text-blue-grey">
|
|
||||||
← {% trans "Retour à la liste" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Messages Django -->
|
|
||||||
{% for message in messages %}
|
|
||||||
<div class="w3-panel w3-round
|
|
||||||
{% if message.tags == 'success' %}w3-green
|
|
||||||
{% elif message.tags == 'error' %}w3-red
|
|
||||||
{% elif message.tags == 'warning' %}w3-orange
|
|
||||||
{% else %}w3-blue{% endif %}">
|
|
||||||
<p>{{ message }}</p>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Formulaire d'import -->
|
<!-- Formulaire d'import -->
|
||||||
<form method="post" enctype="multipart/form-data" novalidate id="import-form">
|
<form method="post" enctype="multipart/form-data" novalidate id="import-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" id="_sid" name="_sid" value="{{ current_session.id }}">
|
||||||
{% if form.non_field_errors %}
|
<input type="hidden" id="_expid" name="_expid" value="{{ current_experiment.id }}">
|
||||||
<div class="w3-panel w3-red w3-round">
|
|
||||||
{% for error in form.non_field_errors %}
|
|
||||||
<p>⚠ {{ error }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="w3-card-4 w3-margin-bottom w3-border w3-round w3-round-large">
|
<div class="w3-card-4 w3-margin-bottom w3-border w3-round w3-round-large">
|
||||||
<header class="w3-container w3-blue-grey w3-round w3-round-top-large">
|
<header class="w3-container w3-blue-grey w3-round w3-round-top-large">
|
||||||
<h3 class="w3-text-white">{% trans "Fichier CSV à importer" %}</h3>
|
<h3 class="w3-text-white">{% trans "Fichier CSV à importer" %}: <span class="w3-large w3-text-lime">{{ current_experiment }}</span></h3>
|
||||||
</header>
|
</header>
|
||||||
<div class="w3-container w3-padding-24">
|
<div class="w3-container w3-padding-24">
|
||||||
|
<p class="w3-light-blue w3-padding w3-border w3-round" style="margin:0 0 4px;">
|
||||||
|
{% trans "Colonnes obligatoires : well, px_per_mm, fps." %}<br>
|
||||||
|
{% trans "Toutes les autres colonnes sont optionnelles." %}
|
||||||
|
|
||||||
<!-- Zone de dépôt de fichier -->
|
<!-- Zone de dépôt de fichier -->
|
||||||
<div id="drop-zone"
|
<div id="drop-zone"
|
||||||
@@ -67,22 +38,12 @@
|
|||||||
{% trans "ou cliquez pour sélectionner" %}
|
{% trans "ou cliquez pour sélectionner" %}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Input fichier caché -->
|
<!-- Input fichier caché -->
|
||||||
<input type="file" id="id_csv_file" name="csv_file"
|
<input type="file" id="id_csv_file" name="csv_file"
|
||||||
accept=".csv,text/csv"
|
accept=".csv,text/csv"
|
||||||
style="display:none;"
|
style="display:none;"
|
||||||
onchange="fileSelected(this)">
|
onchange="fileSelected(this)">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if form.csv_file.errors %}
|
|
||||||
<div class="w3-panel w3-red w3-round w3-margin-top">
|
|
||||||
{% for error in form.csv_file.errors %}
|
|
||||||
<p>⚠ {{ error }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Aperçu du fichier sélectionné -->
|
<!-- Aperçu du fichier sélectionné -->
|
||||||
<div id="file-info" class="w3-panel w3-pale-green w3-round w3-margin-top" style="display:none;">
|
<div id="file-info" class="w3-panel w3-pale-green w3-round w3-margin-top" style="display:none;">
|
||||||
<p id="file-name" class="w3-text-green"><b></b></p>
|
<p id="file-name" class="w3-text-green"><b></b></p>
|
||||||
@@ -92,39 +53,32 @@
|
|||||||
<!-- Option : écraser -->
|
<!-- Option : écraser -->
|
||||||
<div class="w3-margin-top w3-padding-top w3-border-top">
|
<div class="w3-margin-top w3-padding-top w3-border-top">
|
||||||
<label class="w3-text-blue-grey" style="cursor:pointer; display:flex; align-items:center; gap:10px;">
|
<label class="w3-text-blue-grey" style="cursor:pointer; display:flex; align-items:center; gap:10px;">
|
||||||
{{ form.overwrite }}
|
<input type="checkbox" name="overwriteoverwrite" class="w3-check" value="1" />
|
||||||
<span>
|
<span>
|
||||||
<b>{{ form.overwrite.label }}</b><br>
|
<b>{% trans "Écraser les configurations existantes" %}</b><br>
|
||||||
<span class="w3-small w3-text-grey">
|
<span class="w3-small w3-text-grey">
|
||||||
{% trans "Si décoché, les configurations déjà existantes (même experiment + well) seront ignorées." %}
|
{% trans "Si décoché, les configurations déjà existantes (même experiment + well) seront ignorées." %}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{% if form.overwrite.errors %}
|
|
||||||
<span class="w3-text-red w3-small">{{ form.overwrite.errors|join:", " }}</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Boutons -->
|
<!-- Boutons -->
|
||||||
<div class="w3-row-padding w3-margin-bottom">
|
<div class="w3-row-padding w3-margin-bottom">
|
||||||
<div class="w3-col s12">
|
<div class="w3-col s12">
|
||||||
<button type="submit" id="submit-btn"
|
<button type="submit" id="submit-btn" name="valid" value="ok"
|
||||||
class="w3-button w3-blue-grey w3-round w3-large w3-padding-large"
|
class="w3-button w3-blue-grey w3-round w3-large w3-padding-large"
|
||||||
disabled>
|
disabled>
|
||||||
📂 {% trans "Importer" %}
|
📂 {% trans "Importer" %}
|
||||||
</button>
|
</button>
|
||||||
<a href="{% url 'planarian:experiment-list' %}"
|
<a href="/"
|
||||||
class="w3-button w3-light-grey w3-round w3-large w3-padding-large w3-margin-left">
|
class="w3-button w3-light-grey w3-round w3-large w3-padding-large w3-margin-left">
|
||||||
✖ {% trans "Annuler" %}
|
✖ {% trans "Annuler" %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- ============================================================
|
<!-- ============================================================
|
||||||
Format attendu du CSV
|
Format attendu du CSV
|
||||||
============================================================ -->
|
============================================================ -->
|
||||||
@@ -214,18 +168,6 @@ exp_ctrl_01,A2,26.25,10,0.2,1.5,none<br>
|
|||||||
exp_light_01,B1,26.25,10,0.2,1.5,fixed<br>
|
exp_light_01,B1,26.25,10,0.2,1.5,fixed<br>
|
||||||
exp_light_01,B2,26.25,10,0.2,1.5,fixed<br>
|
exp_light_01,B2,26.25,10,0.2,1.5,fixed<br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Téléchargement du template -->
|
|
||||||
<div class="w3-margin-top">
|
|
||||||
<a href="{% url 'planarian:experiment-list' %}?export_template=1"
|
|
||||||
class="w3-button w3-teal w3-round w3-small">
|
|
||||||
⬇ {% trans "Télécharger un template CSV vide" %}
|
|
||||||
</a>
|
|
||||||
<span class="w3-small w3-text-grey w3-margin-left">
|
|
||||||
{% trans "Ou exportez vos configurations existantes depuis l'admin Django." %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -321,5 +263,9 @@ exp_light_01,B2,26.25,10,0.2,1.5,fixed<br>
|
|||||||
return (bytes / 1048576).toFixed(1) + ' MB';
|
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<div class="w3-container w3-padding-64 w3-margin" style="max-width:760px; margin:auto;">
|
||||||
|
<h3 class="w3-panel w3-blue w3-round-xlarge w3-padding-64 w3-center"> {% trans "Choisir une session" %}<br>{% trans "Puis une expérience." %}</h3>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,27 +7,13 @@ app_name = "planarian"
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Configurations expériences
|
# Configurations expériences
|
||||||
#path("experiments/", views.ExperimentConfigListView.as_view(), name="experiment-list2"),
|
|
||||||
#
|
|
||||||
#path("experiments/new/", views.ExperimentConfigFormView.as_view(), name="experiment-new"),
|
|
||||||
#path("experiments/<int:pk>/",views.ExperimentConfigFormView.as_view(), name="experiment-edit2"),
|
|
||||||
|
|
||||||
#path("config/list/", views.config_list_view, name="experiment-list"),
|
|
||||||
#path("config/list/<int:session_id>/<int:experiment_id>/", views.config_list_view, name="experiment-list"),
|
|
||||||
|
|
||||||
#path("config/edit/<int:pk>/", views.config_edit_view, name="experiment-edit"),
|
|
||||||
#path("config/edit/<int:pk>/<int:session_id>/<int:experiment_id>/", views.config_edit_view, name="experiment-edit"),
|
|
||||||
|
|
||||||
# Import / export
|
# Import / export
|
||||||
#path("import/", views.ImportParamsView.as_view(), name="import-params"),
|
path("import/csv/", views.import_csv_view, name="import-params"),
|
||||||
#path("export/", views.ExportCsvView.as_view(), name="export-csv-view"),
|
|
||||||
|
|
||||||
#path("import/", views.ImportParamsView.as_view(), name="import-params"),
|
|
||||||
path("export/csv/", views.export_csv_view, name="export-csv"),
|
path("export/csv/", views.export_csv_view, name="export-csv"),
|
||||||
path("export/csv/<int:session_id>/<int:experiment_id>/", views.export_csv_view, name="export-csv"),
|
path("api/export/csv/", views.export_metrics, name="api-export-csv"),
|
||||||
|
|
||||||
|
|
||||||
# API JSON pour le front-end
|
# API JSON pour le front-end
|
||||||
path("api/tracking/", views.TrackingDataView.as_view(), name="tracking-data-api"),
|
path("api/tracking/", views.TrackingDataView.as_view(), name="tracking-data-api"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -2,30 +2,27 @@
|
|||||||
|
|
||||||
#import asyncio
|
#import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
import json
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.http import HttpResponse, JsonResponse
|
from django.http import JsonResponse, FileResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect #, render
|
from django.shortcuts import redirect
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from django.shortcuts import render #, redirect
|
|
||||||
from django.views.decorators.http import require_GET, require_POST
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
from django.shortcuts import render #, redirect
|
||||||
|
from django.views.decorators.http import require_GET
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import FormView, ListView
|
|
||||||
|
|
||||||
|
|
||||||
from .forms import CsvImportForm, ExperimentConfigForm, ExportCsvForm
|
|
||||||
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 .tasks import export_experiment_metrics, export_session_metrics
|
||||||
from scanner import models
|
from scanner import models
|
||||||
|
from .models import ExperimentConfig
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -61,6 +58,7 @@ def global_context(request, **ctx):
|
|||||||
domain_server=settings.DOMAIN_SERVER,
|
domain_server=settings.DOMAIN_SERVER,
|
||||||
local_ip_server=settings.LOCAL_IP_SERVER,
|
local_ip_server=settings.LOCAL_IP_SERVER,
|
||||||
host_port=settings.SERVER_HOST_PORT,
|
host_port=settings.SERVER_HOST_PORT,
|
||||||
|
export_csv_destination=settings.CSV_EXPORT_DIR,
|
||||||
conf=conf,
|
conf=conf,
|
||||||
**ctx
|
**ctx
|
||||||
)
|
)
|
||||||
@@ -100,6 +98,26 @@ def get_active_session(request, session_id=None, experiment_id=None):
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Vue :Export CSV depuis ReductStore
|
# Vue :Export CSV depuis ReductStore
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@csrf_exempt
|
||||||
|
def export_metrics(request):
|
||||||
|
data = json.loads(request.body.decode() or "{}")
|
||||||
|
action = data.get("action")
|
||||||
|
pid = data.get("pid")
|
||||||
|
if action=='experiment_csv':
|
||||||
|
experiment = models.Experiment.objects.filter(pk=pid).first()
|
||||||
|
if experiment:
|
||||||
|
export_experiment_metrics(experiment)
|
||||||
|
return JsonResponse({"state": True})
|
||||||
|
if action=='session_csv':
|
||||||
|
session = models.Session.objects.filter(pk=pid).first()
|
||||||
|
if session:
|
||||||
|
export_session_metrics(session)
|
||||||
|
return JsonResponse({"state": True})
|
||||||
|
return JsonResponse({"state": False})
|
||||||
|
|
||||||
|
|
||||||
def export_csv(request):
|
def export_csv(request):
|
||||||
d = request.POST
|
d = request.POST
|
||||||
|
|
||||||
@@ -116,7 +134,7 @@ def export_csv(request):
|
|||||||
start = d.get("start_dt"),
|
start = d.get("start_dt"),
|
||||||
stop = d.get("stop_dt"),
|
stop = d.get("stop_dt"),
|
||||||
)
|
)
|
||||||
print(f"Export CSV: export_csv_response done, {n} lignes, content size={len(csv_content)}, {csv_content}")
|
print(f"Export CSV: export_csv_response done, {n} lignes, content size={len(csv_content)}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Erreur export CSV: {e}")
|
logger.error(f"Erreur export CSV: {e}")
|
||||||
messages.error(request, _("Erreur lors de l'export CSV: %(error)s") % {"error": str(e)})
|
messages.error(request, _("Erreur lors de l'export CSV: %(error)s") % {"error": str(e)})
|
||||||
@@ -124,31 +142,17 @@ def export_csv(request):
|
|||||||
return csv_content, n
|
return csv_content, n
|
||||||
|
|
||||||
csv_content, n = _do_export()
|
csv_content, n = _do_export()
|
||||||
|
|
||||||
logger.info(f"Export CSV: {n} lignes, content size={len(csv_content)}")
|
logger.info(f"Export CSV: {n} lignes, content size={len(csv_content)}")
|
||||||
if not csv_content:
|
|
||||||
messages.warning(request, _("Aucune donnée trouvée."))
|
|
||||||
return None
|
|
||||||
|
|
||||||
filename = (
|
filename = (
|
||||||
f"{d['experiment']}_{d['well']}_planaire{d['planarian']}"
|
f"{d['experiment']}_{d['well']}-planaire{d['planarian']}"
|
||||||
f"_{d['record_type']}.csv"
|
f"_{d['record_type']}.csv"
|
||||||
)
|
)
|
||||||
response = HttpResponse(csv_content, content_type="text/csv; charset=utf-8")
|
return csv_content, filename
|
||||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
||||||
messages.success(request, _("%(n)d lignes exportées.") % {"n": n})
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def export_csv_view(request, session_id=None, experiment_id=None):
|
def export_csv_view(request):
|
||||||
session_context = get_active_session(request, session_id, experiment_id)
|
session_context = get_active_session(request)
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
valid = request.POST.get('valid')
|
|
||||||
if valid == 'ok':
|
|
||||||
export_csv(request)
|
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
'choice_title': _("Export vers un fichier CSV depuis ReductStore"),
|
'choice_title': _("Export vers un fichier CSV depuis ReductStore"),
|
||||||
'well': 'A1',
|
'well': 'A1',
|
||||||
@@ -156,47 +160,23 @@ def export_csv_view(request, session_id=None, experiment_id=None):
|
|||||||
'record_type': 'frame',
|
'record_type': 'frame',
|
||||||
**session_context
|
**session_context
|
||||||
}
|
}
|
||||||
|
if request.method == 'POST':
|
||||||
|
valid = request.POST.get('valid')
|
||||||
|
if valid == 'ok':
|
||||||
|
csv_content, filename = export_csv(request)
|
||||||
|
if csv_content:
|
||||||
|
response = FileResponse(csv_content, content_type="text/csv; charset=utf-8")
|
||||||
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||||
|
return response
|
||||||
|
|
||||||
|
messages.warning(request, _("Aucune donnée trouvée."))
|
||||||
return render(request, "planarian/export_csv.html", context=global_context(request, **ctx))
|
return render(request, "planarian/export_csv.html", context=global_context(request, **ctx))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Vue : import CSV de paramètres
|
# Vue : import CSV de paramètres
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@login_required
|
def import_csv(request, current_experiment, rows, overwrite):
|
||||||
def import_csv_view(request, session_id=None, experiment_id=None):
|
|
||||||
"""
|
|
||||||
Import de configurations d'expérience depuis un fichier CSV.
|
|
||||||
Une ligne CSV = un puits = un ExperimentConfig.
|
|
||||||
|
|
||||||
Colonnes CSV obligatoires : experiment, well, px_per_mm, fps
|
|
||||||
Toutes les autres colonnes correspondent aux champs du modèle.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ctx = {
|
|
||||||
'choice_title': _("Importer des configurations depuis un fichier CSV"),
|
|
||||||
}
|
|
||||||
return render(request, "planarian/import_params.html", context=global_context(request, **ctx))
|
|
||||||
|
|
||||||
|
|
||||||
class ImportParamsView(FormView):
|
|
||||||
"""
|
|
||||||
Import de configurations d'expérience depuis un fichier CSV.
|
|
||||||
Une ligne CSV = un puits = un ExperimentConfig.
|
|
||||||
|
|
||||||
Colonnes CSV obligatoires : experiment, well, px_per_mm, fps
|
|
||||||
Toutes les autres colonnes correspondent aux champs du modèle.
|
|
||||||
"""
|
|
||||||
|
|
||||||
template_name = "planarian/import_params.html"
|
|
||||||
form_class = CsvImportForm
|
|
||||||
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
|
||||||
rows = form.csv_rows
|
|
||||||
overwrite = form.cleaned_data["overwrite"]
|
|
||||||
created = 0
|
created = 0
|
||||||
updated = 0
|
updated = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
@@ -207,13 +187,13 @@ class ImportParamsView(FormView):
|
|||||||
d = params.to_dict()
|
d = params.to_dict()
|
||||||
|
|
||||||
obj, is_new = ExperimentConfig.objects.get_or_create(
|
obj, is_new = ExperimentConfig.objects.get_or_create(
|
||||||
experiment = d["experiment"],
|
experiment = current_experiment.identifier,
|
||||||
well = d["well"],
|
well = d.get("well"),
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_new or overwrite:
|
if is_new or overwrite:
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if k not in ("experiment", "well") and hasattr(obj, k):
|
if k not in ["well", "experiment", "author", "experiment_key", "active"] and hasattr(obj, k):
|
||||||
setattr(obj, k, v)
|
setattr(obj, k, v)
|
||||||
obj.save()
|
obj.save()
|
||||||
if is_new:
|
if is_new:
|
||||||
@@ -224,23 +204,58 @@ class ImportParamsView(FormView):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Ligne ignorée ({row}): {e}")
|
logger.warning(f"Ligne ignorée ({row}): {e}")
|
||||||
errors += 1
|
errors += 1
|
||||||
|
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request,
|
request,
|
||||||
_("Import terminé : %(c)d créés, %(u)d mis à jour, %(e)d erreurs.")
|
_("Import terminé : %(c)d créés, %(u)d mis à jour, %(e)d erreurs.")
|
||||||
% {"c": created, "u": updated, "e": errors},
|
% {"c": created, "u": updated, "e": errors},
|
||||||
)
|
)
|
||||||
return redirect("planarian:experiment-list")
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
return redirect("redirect_to_mainboard")
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
return global_context(self.request, choice_title=_("Importer des configurations depuis un fichier CSV"), **context)
|
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def import_csv_view(request):
|
||||||
|
"""
|
||||||
|
Import de configurations d'expérience depuis un fichier CSV.
|
||||||
|
Une ligne CSV = un puits = un ExperimentConfig.
|
||||||
|
|
||||||
|
Colonnes CSV obligatoires: well, px_per_mm, fps
|
||||||
|
Toutes les autres colonnes correspondent aux champs du modèle.
|
||||||
|
"""
|
||||||
|
session_context = get_active_session(request)
|
||||||
|
if request.method == 'POST':
|
||||||
|
valid = request.POST.get('valid')
|
||||||
|
current_experiment = session_context.get('current_experiment')
|
||||||
|
|
||||||
|
if valid == 'ok' and current_experiment:
|
||||||
|
try:
|
||||||
|
f = request.FILES.get('csv_file')
|
||||||
|
overwrite = request.POST.get("overwrite")
|
||||||
|
try:
|
||||||
|
content = f.read().decode("utf-8-sig")
|
||||||
|
reader = csv.DictReader(io.StringIO(content))
|
||||||
|
rows = list(reader)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f'Fichier CSV invalide : {e}'
|
||||||
|
raise Exception(msg)
|
||||||
|
|
||||||
|
required = {"well", "px_per_mm", "fps"}
|
||||||
|
if rows:
|
||||||
|
missing = required - set(rows[0].keys())
|
||||||
|
if missing:
|
||||||
|
msg = _("Colonnes manquantes : %(cols)s") % {"cols": ", ".join(missing)}
|
||||||
|
raise Exception(msg)
|
||||||
|
|
||||||
|
return import_csv(request, current_experiment, rows, overwrite)
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, msg)
|
||||||
|
logger.error(msg)
|
||||||
|
ctx = { 'choice_title': _("Importer des configurations depuis un fichier CSV"), **session_context }
|
||||||
|
return render(request, "planarian/import_params.html", context=global_context(request, **ctx))
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Vue API JSON : données de tracking (pour polling front-end)
|
# Vue API JSON : données de tracking (pour polling front-end)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class TrackingDataView(View):
|
class TrackingDataView(View):
|
||||||
"""
|
"""
|
||||||
API JSON retournant les métriques de tracking d'un planaire.
|
API JSON retournant les métriques de tracking d'un planaire.
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import json
|
|||||||
from django_celery_beat.models import PeriodicTask, ClockedSchedule
|
from django_celery_beat.models import PeriodicTask, ClockedSchedule
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db.models.signals import post_save, post_delete
|
from django.db.models.signals import post_save, post_delete
|
||||||
from django.utils.text import slugify
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
@@ -440,12 +439,14 @@ def create_experiment_well(sender, instance, created, **kwargs):
|
|||||||
ExperimentWell.objects.get_or_create(experiment=instance, well=wp.well, author=instance.author, defaults={'active':True})
|
ExperimentWell.objects.get_or_create(experiment=instance, well=wp.well, author=instance.author, defaults={'active':True})
|
||||||
|
|
||||||
ExperimentConfig.objects.get_or_create(
|
ExperimentConfig.objects.get_or_create(
|
||||||
experiment=instance,
|
experiment_key=instance,
|
||||||
well=wp.well,
|
well=wp.well.name,
|
||||||
author=instance.author,
|
author=instance.author,
|
||||||
|
experiment=instance.identifier,
|
||||||
defaults={
|
defaults={
|
||||||
'px_per_mm': wp.px_per_mm,
|
'px_per_mm': wp.px_per_mm,
|
||||||
'fps': ScannerConstants().get().video_frame_rate,
|
'fps': ScannerConstants().get().video_frame_rate,
|
||||||
'well_radius_mm': instance.multiwell.diameter / 2,
|
'well_radius_mm': instance.multiwell.diameter / 2,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -276,3 +276,4 @@ def supervisor_restart_service(params):
|
|||||||
logger.error(f"supervisor_restart_all_services error {e}")
|
logger.error(f"supervisor_restart_all_services error {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -99,6 +99,11 @@
|
|||||||
<i class="fa-solid fa-gear w3-text-purple w3-xlarge"></i> {% trans "Création de sessions d'expériences" %}
|
<i class="fa-solid fa-gear w3-text-purple w3-xlarge"></i> {% trans "Création de sessions d'expériences" %}
|
||||||
</a>
|
</a>
|
||||||
<hr class="w3-bar-item divider w3-grey">
|
<hr class="w3-bar-item divider w3-grey">
|
||||||
|
{% if conf.tracking %}
|
||||||
|
<a href="{% url 'planarian:import-params' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||||
|
<i class="fa-solid fa-file-import w3-text-lime w3-xlarge"></i> {% trans "Importer des configurations depuis CSV" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url 'scanner:experiment_view' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
<a href="{% url 'scanner:experiment_view' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||||
<i class="fa-solid fa-gear w3-text-pink w3-xlarge"></i> {% trans "Gestions des expériences" %}
|
<i class="fa-solid fa-gear w3-text-pink w3-xlarge"></i> {% trans "Gestions des expériences" %}
|
||||||
</a>
|
</a>
|
||||||
@@ -110,15 +115,15 @@
|
|||||||
</a>
|
</a>
|
||||||
<hr class="w3-bar-item divider w3-grey w3-margin-bottom">
|
<hr class="w3-bar-item divider w3-grey w3-margin-bottom">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if conf.tracking %}
|
|
||||||
<a href="{% url 'planarian:export-csv' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
|
||||||
<i class="fa-solid fa-vials w3-text-lime w3-xlarge"></i> {% trans "Export CSV des expériences" %}
|
|
||||||
</a>
|
|
||||||
{% 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>
|
<div class="w3-bar-item w3-dark-xxlight">{% trans "Résultats " %}</div>
|
||||||
|
{% if conf.tracking %}
|
||||||
|
<a href="{% url 'planarian:export-csv' %}" class="w3-bar-item w3-btn w3-hover-opacity">
|
||||||
|
<i class="fa-solid fa-file-export w3-text-lime w3-xlarge"></i> {% trans "Export CSV des expériences" %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<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>
|
||||||
@@ -136,7 +141,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 %}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user