planarian
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
{% extends "scanner/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="w3-container w3-padding-32" 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 %}
|
||||
|
||||
<!-- 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">
|
||||
<header class="w3-container w3-teal w3-round-top-large">
|
||||
<h3 class="w3-text-white">{% trans "Identification" %}</h3>
|
||||
</header>
|
||||
<div class="w3-container w3-padding-24">
|
||||
<div class="w3-row-padding">
|
||||
|
||||
<!-- Experiment -->
|
||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
||||
<label class="w3-text-teal"><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>
|
||||
|
||||
<!-- Well -->
|
||||
<div class="w3-col m6 s12 w3-margin-bottom">
|
||||
<label class="w3-text-teal"><b>{{ form.well.label }}</b></label>
|
||||
{% if form.well.help_text %}
|
||||
<p class="w3-small w3-text-grey" style="margin:0 0 4px;">{{ form.well.help_text }}</p>
|
||||
{% endif %}
|
||||
{{ 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">
|
||||
<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">
|
||||
<header class="w3-container w3-blue-grey 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-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>
|
||||
{{ 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>
|
||||
{{ 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 3 : Seuils de mobilité EthoVision
|
||||
============================================================ -->
|
||||
<div class="w3-card-4 w3-round-large w3-margin-bottom">
|
||||
<header class="w3-container w3-indigo 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 4 : Tracker
|
||||
============================================================ -->
|
||||
<div class="w3-card-4 w3-round-large w3-margin-bottom">
|
||||
<header class="w3-container w3-dark-grey 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-dark-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-dark-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-dark-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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================
|
||||
Section 5 : Comportements (accordéon W3.CSS)
|
||||
============================================================ -->
|
||||
<div class="w3-card-4 w3-round-large w3-margin-bottom">
|
||||
<header class="w3-container w3-teal 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>
|
||||
{{ 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>
|
||||
{{ 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>
|
||||
{{ 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>
|
||||
{{ 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>
|
||||
{{ 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 object %}
|
||||
💾 {% 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 "Annuler" %}
|
||||
</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 %}
|
||||
@@ -0,0 +1,334 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="w3-container w3-padding-32" 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
|
||||
{% 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 %}
|
||||
|
||||
<!-- 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>{{ cfg.experiment }}</b>
|
||||
{% if cfg.description %}
|
||||
<br><span class="w3-small w3-text-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">{{ cfg.px_per_mm }}</td>
|
||||
|
||||
<!-- FPS -->
|
||||
<td class="w3-hide-small">{{ 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;">
|
||||
<a href="{% url 'planarian:experiment-edit' cfg.pk %}"
|
||||
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 %}
|
||||
@@ -0,0 +1,254 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="w3-container w3-padding-32" style="max-width:760px; margin:auto;">
|
||||
|
||||
<!-- 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="{% url 'planarian:experiment-list' %}"
|
||||
class="w3-button w3-white w3-round w3-text-blue">
|
||||
← {% 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 %}
|
||||
|
||||
<!-- 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 %}
|
||||
|
||||
{% 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-round-large">
|
||||
<header class="w3-container w3-blue 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 %}
|
||||
<!-- Saisie libre ou sélection depuis les configs existantes -->
|
||||
<input list="experiment-list" name="experiment" id="id_experiment"
|
||||
value="{{ form.experiment.value|default:'' }}"
|
||||
class="w3-input w3-border w3-round"
|
||||
placeholder="{% trans 'Ex : exp_2026_04_25' %}">
|
||||
<datalist id="experiment-list">
|
||||
{% for cfg in experiment_choices %}
|
||||
<option value="{{ cfg }}">
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
{% 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>
|
||||
<input list="well-list" name="well" id="id_well"
|
||||
value="{{ form.well.value|default:'' }}"
|
||||
class="w3-input w3-border w3-round"
|
||||
placeholder="{% trans 'Ex : A1' %}">
|
||||
<datalist id="well-list">
|
||||
{% for w in well_choices %}
|
||||
<option value="{{ w }}">
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
{% 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-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-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;">
|
||||
{% for radio in form.record_type %}
|
||||
<div class="w3-col m6 s12">
|
||||
<label class="w3-button w3-block w3-round w3-border
|
||||
{% if radio.data.value == form.record_type.value %}w3-blue w3-text-white{% else %}w3-white{% endif %}"
|
||||
style="text-align:center; margin-bottom:6px; cursor:pointer;">
|
||||
{{ radio.tag }} {{ radio.choice_label }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</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-round w3-large w3-padding-large">
|
||||
📥 {% trans "Générer et télécharger le CSV" %}
|
||||
</button>
|
||||
<a href="{% url 'planarian:experiment-list' %}"
|
||||
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">
|
||||
<header class="w3-container w3-blue-grey 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>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,326 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="w3-container w3-padding-32" style="max-width:760px; margin:auto;">
|
||||
|
||||
<!-- En-tête -->
|
||||
<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 -->
|
||||
<form method="post" enctype="multipart/form-data" novalidate id="import-form">
|
||||
{% csrf_token %}
|
||||
|
||||
{% 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-round-large w3-margin-bottom">
|
||||
<header class="w3-container w3-blue-grey w3-round-top-large">
|
||||
<h3 class="w3-text-white">{% trans "Fichier CSV à importer" %}</h3>
|
||||
</header>
|
||||
<div class="w3-container w3-padding-24">
|
||||
|
||||
<!-- Zone de dépôt de fichier -->
|
||||
<div id="drop-zone"
|
||||
class="w3-border w3-border-blue-grey w3-round-large w3-padding-32"
|
||||
style="border-style:dashed !important; border-width:2px !important;
|
||||
text-align:center; cursor:pointer; transition:background 0.2s;"
|
||||
onclick="document.getElementById('id_csv_file').click()"
|
||||
ondragover="dragOver(event)"
|
||||
ondragleave="dragLeave(event)"
|
||||
ondrop="dropFile(event)">
|
||||
|
||||
<p style="font-size:2.5rem; margin:0;">📄</p>
|
||||
<p id="drop-label" class="w3-text-blue-grey">
|
||||
<b>{% trans "Glissez-déposez votre fichier CSV ici" %}</b><br>
|
||||
<span class="w3-small w3-text-grey">
|
||||
{% trans "ou cliquez pour sélectionner" %}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<!-- Input fichier caché -->
|
||||
<input type="file" id="id_csv_file" name="csv_file"
|
||||
accept=".csv,text/csv"
|
||||
style="display:none;"
|
||||
onchange="fileSelected(this)">
|
||||
</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é -->
|
||||
<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-size" class="w3-small w3-text-grey" style="margin:0;"></p>
|
||||
</div>
|
||||
|
||||
<!-- Option : écraser -->
|
||||
<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;">
|
||||
{{ form.overwrite }}
|
||||
<span>
|
||||
<b>{{ form.overwrite.label }}</b><br>
|
||||
<span class="w3-small w3-text-grey">
|
||||
{% trans "Si décoché, les configurations déjà existantes (même experiment + well) seront ignorées." %}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
{% if form.overwrite.errors %}
|
||||
<span class="w3-text-red w3-small">{{ form.overwrite.errors|join:", " }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Boutons -->
|
||||
<div class="w3-row-padding w3-margin-bottom">
|
||||
<div class="w3-col s12">
|
||||
<button type="submit" id="submit-btn"
|
||||
class="w3-button w3-blue-grey w3-round w3-large w3-padding-large"
|
||||
disabled>
|
||||
📂 {% trans "Importer" %}
|
||||
</button>
|
||||
<a href="{% url 'planarian:experiment-list' %}"
|
||||
class="w3-button w3-light-grey w3-round w3-large w3-padding-large w3-margin-left">
|
||||
✖ {% trans "Annuler" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- ============================================================
|
||||
Format attendu du CSV
|
||||
============================================================ -->
|
||||
<div class="w3-card-4 w3-round-large">
|
||||
<header class="w3-container w3-teal w3-round-top-large">
|
||||
<h3 class="w3-text-white">{% trans "Format du fichier CSV" %}</h3>
|
||||
</header>
|
||||
<div class="w3-container w3-padding-24">
|
||||
|
||||
<!-- Colonnes obligatoires -->
|
||||
<p><b>{% trans "Colonnes obligatoires" %}</b></p>
|
||||
<div class="w3-responsive w3-margin-bottom">
|
||||
<table class="w3-table w3-bordered w3-small">
|
||||
<thead>
|
||||
<tr class="w3-teal">
|
||||
<th>{% trans "Colonne" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Exemple" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>experiment</code></td>
|
||||
<td>{% trans "texte" %}</td>
|
||||
<td><code>exp_2026_04_25</code></td>
|
||||
<td>{% trans "Identifiant unique de l'expérience" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>well</code></td>
|
||||
<td>{% trans "texte" %}</td>
|
||||
<td><code>A1</code></td>
|
||||
<td>{% trans "Identifiant du puits" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>px_per_mm</code></td>
|
||||
<td>{% trans "flottant" %}</td>
|
||||
<td><code>26.25</code></td>
|
||||
<td>{% trans "Calibration optique (pixels/mm)" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fps</code></td>
|
||||
<td>{% trans "flottant" %}</td>
|
||||
<td><code>10.0</code></td>
|
||||
<td>{% trans "Fréquence de capture (images/s)" %}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Colonnes optionnelles -->
|
||||
<p><b>{% trans "Colonnes optionnelles" %}</b>
|
||||
<span class="w3-small w3-text-grey">{% trans "(valeurs par défaut utilisées si absentes)" %}</span>
|
||||
</p>
|
||||
<div class="w3-responsive w3-margin-bottom">
|
||||
<table class="w3-table w3-bordered w3-small w3-striped">
|
||||
<thead>
|
||||
<tr class="w3-blue-grey">
|
||||
<th>{% trans "Colonne" %}</th>
|
||||
<th>{% trans "Défaut" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td><code>well_radius_mm</code></td> <td>8.0</td> <td>{% trans "Rayon du puits (mm)" %}</td></tr>
|
||||
<tr><td><code>thresh_immobile</code></td> <td>0.2</td> <td>{% trans "Seuil Immobile EthoVision (mm/s)" %}</td></tr>
|
||||
<tr><td><code>thresh_mobile</code></td> <td>1.5</td> <td>{% trans "Seuil Mobile EthoVision (mm/s)" %}</td></tr>
|
||||
<tr><td><code>tube_axis</code></td> <td>vertical</td> <td>{% trans "Axe du tube : vertical | horizontal" %}</td></tr>
|
||||
<tr><td><code>min_area_px</code></td> <td>20</td> <td>{% trans "Surface min de détection (px²)" %}</td></tr>
|
||||
<tr><td><code>planarian_count</code></td> <td>1</td> <td>{% trans "Nombre de planaires par puits" %}</td></tr>
|
||||
<tr><td><code>photo_mode</code></td> <td>none</td> <td>{% trans "Phototactisme : none | fixed | sine | radial" %}</td></tr>
|
||||
<tr><td><code>photo_strength</code></td> <td>0.0</td> <td>{% trans "Intensité phototactisme (0-1)" %}</td></tr>
|
||||
<tr><td><code>chemo_strength</code></td> <td>0.0</td> <td>{% trans "Intensité chimiotactisme (0-1)" %}</td></tr>
|
||||
<tr><td><code>chemo_radius_mm</code></td> <td>2.0</td> <td>{% trans "Rayon zone nourriture (mm)" %}</td></tr>
|
||||
<tr><td><code>avoid_radius_mm</code></td> <td>3.0</td> <td>{% trans "Rayon évitement inter-individus (mm)" %}</td></tr>
|
||||
<tr><td><code>aggreg_radius_mm</code></td> <td>6.0</td> <td>{% trans "Rayon agrégation inter-individus (mm)" %}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Exemple de fichier -->
|
||||
<p><b>{% trans "Exemple minimal" %}</b></p>
|
||||
<div class="w3-code w3-round" style="font-size:12px; overflow-x:auto;">
|
||||
experiment,well,px_per_mm,fps,thresh_immobile,thresh_mobile,photo_mode<br>
|
||||
exp_ctrl_01,A1,26.25,10,0.2,1.5,none<br>
|
||||
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,B2,26.25,10,0.2,1.5,fixed<br>
|
||||
</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><!-- fin container -->
|
||||
|
||||
<style>
|
||||
/* Zone de dépôt active */
|
||||
#drop-zone.dragover {
|
||||
background-color: #e3f2fd;
|
||||
border-color: #1565c0 !important;
|
||||
}
|
||||
/* Checkbox alignée */
|
||||
input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* Bouton désactivé */
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// --- Drag & drop ---
|
||||
function dragOver(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('drop-zone').classList.add('dragover');
|
||||
}
|
||||
|
||||
function dragLeave(e) {
|
||||
document.getElementById('drop-zone').classList.remove('dragover');
|
||||
}
|
||||
|
||||
function dropFile(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('drop-zone').classList.remove('dragover');
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
const input = document.getElementById('id_csv_file');
|
||||
// Transfert du fichier vers l'input natif via DataTransfer
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(files[0]);
|
||||
input.files = dt.files;
|
||||
showFileInfo(files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sélection via clic ---
|
||||
function fileSelected(input) {
|
||||
if (input.files.length > 0) {
|
||||
showFileInfo(input.files[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Affichage du fichier sélectionné ---
|
||||
function showFileInfo(file) {
|
||||
const info = document.getElementById('file-info');
|
||||
const nameEl = document.getElementById('file-name').querySelector('b');
|
||||
const sizeEl = document.getElementById('file-size');
|
||||
const label = document.getElementById('drop-label');
|
||||
const btn = document.getElementById('submit-btn');
|
||||
|
||||
// Vérification extension
|
||||
if (!file.name.toLowerCase().endsWith('.csv')) {
|
||||
alert('{% trans "Le fichier doit être au format .csv" %}');
|
||||
return;
|
||||
}
|
||||
|
||||
nameEl.textContent = '📄 ' + file.name;
|
||||
sizeEl.textContent = formatSize(file.size);
|
||||
info.style.display = 'block';
|
||||
label.innerHTML = '<b>{% trans "Fichier prêt à importer" %}</b>';
|
||||
btn.disabled = false;
|
||||
|
||||
// Aperçu des premières lignes
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
const lines = e.target.result.split('\n').slice(0, 4);
|
||||
const preview = document.getElementById('csv-preview');
|
||||
if (preview) {
|
||||
preview.textContent = lines.join('\n');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file, 'utf-8');
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / 1048576).toFixed(1) + ' MB';
|
||||
}
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user