This commit is contained in:
2026-04-30 11:33:30 +02:00
parent f53b8d4932
commit b9bcdae282
8 changed files with 113 additions and 60 deletions
+2
View File
@@ -390,7 +390,9 @@ DATETIME_FORMAT = '%d-%m-%Y-%m %H:%M:%S'
EXPORTS_LOCAL_PATH = config("EXPORTS_LOCAL_PATH")
EXPORT_REMOTE_PATH = config("EXPORT_REMOTE_PATH")
EXPORT_DESTINATIONS = ["local", "remote"]
#EXPORT_DESTINATIONS = ["remote"] # only remote
TEST_VIDEOFILE = False
+17 -8
View File
@@ -144,6 +144,10 @@ def _copy_to_destinations(source_path: str, filename: str) -> dict:
Retourne un dict avec les chemins effectivement écrits.
"""
results = {"local": None, "remote": None}
filename = filename.replace(':', '_')
logger.info("%s %s", source_path, filename)
for dest in settings.EXPORT_DESTINATIONS:
@@ -151,10 +155,10 @@ def _copy_to_destinations(source_path: str, filename: str) -> dict:
# Déjà sur place, rien à copier
results["local"] = source_path
elif dest == "remote":
remote_path = os.path.join(settings.EXPORT_REMOTE_DIR, filename)
elif dest == "remote":
remote_path = os.path.join(settings.EXPORT_REMOTE_PATH, filename)
try:
if not remote_mount_available(settings.EXPORT_REMOTE_DIR):
if not remote_mount_available(settings.EXPORT_REMOTE_PATH):
logger.warning("Partage Samba non disponible, copie ignorée")
results["remote_error"] = "Montage indisponible"
continue
@@ -248,6 +252,7 @@ async def export_images_zip(
# --- Génération du ZIP ---
max_zip_bytes = max_zip_size_mb * 1024 * 1024 if max_zip_size_mb > 0 else 0
ts_s = unix_timestamp_to_iso(start_ts)
zip_filename = f"{uuid}_{ts_s}.zip"
zip_path = os.path.join(settings.EXPORTS_LOCAL_PATH, 'images', zip_filename)
os.makedirs(os.path.dirname(zip_path), exist_ok=True)
@@ -297,14 +302,16 @@ async def export_images_zip(
current_offset += size
## Copie vers les destinations (local + Samba)
#destinations = _copy_to_destinations(zip_path, zip_filename)
filename = os.path.join('images', zip_filename)
destinations = _copy_to_destinations(zip_path, filename)
return {
"status": "success",
"zip_path": zip_path,
"frames_written": written,
"frames_skipped": skipped,
"jpeg_quality": jpeg_quality,
#"destinations": destinations,
"destinations": destinations,
}
except Exception as exc:
@@ -461,8 +468,10 @@ async def export_video_mp4(
return {"status": "error", "message": f"Fichier {opencv_video_type} non créé"}
## Copie vers les destinations (local + Samba)
#filename = os.path.basename(video_path)
#destinations = _copy_to_destinations(video_path, filename)
filename = os.path.basename(video_path)
filename = os.path.join('videos', filename)
destinations = _copy_to_destinations(video_path, filename)
return {
"status": "success",
"video_path": video_path,
@@ -470,7 +479,7 @@ async def export_video_mp4(
"frames_skipped": skipped,
"frame_rate": frame_rate,
"file_size_mb": round(os.path.getsize(video_path) / 1024 / 1024, 2),
#"destinations": destinations,
"destinations": destinations,
}
except Exception as exc:
+7 -1
View File
@@ -227,6 +227,7 @@ class Experiment(models.Model):
created = models.DateTimeField(_("Date de création"), default=timezone.now)
started = models.DateTimeField (_("Date de début"), null=True, blank=True)
finished = models.DateTimeField (_("Date de fin"), null=True, blank=True)
class Meta:
ordering = ['-created', ]
@@ -270,7 +271,12 @@ class Session(models.Model):
null=True, blank=True, on_delete=models.SET_NULL, related_name="scanning_session")
scanning_finished_at = models.DateTimeField(_("Balayage terminé à"), null=True, blank=True)
@classmethod
def get_session(self, sid):
return Session.objects.filter(pk=sid).first()
class Meta:
ordering = ['-created', ]
verbose_name = _("Session d'expérience")
@@ -9,19 +9,26 @@
<a href="/" class="w3-bar-item w3-btn w3-hover-opacity"><i class="fa-solid fa-house w3-text-orange w3-xlarge"></i> {% trans "Retour accueil" %}</a>
<form method="post" class="w3-bar-item" action="">
{% csrf_token %}
<select id="_sid" name="_sid" class="w3-select w3-margin-bottom" onchange="this.form.submit()">
<option value="0">---- {% trans "Session d'expérience" %}</option>
<select id="_sid" name="_sid" class="w3-select w3-margin-bottom" onchange="this.form.submit()" title="{% trans "Ensemble d'expériences" %}">
<option value="0">---- {% trans "Session" %}</option>
{% for s in sessions %}
<option value="{{ s.id }}" {% if s.id == cursid %}selected{% endif %}>{{ s.name }}</option>
<option value="{{ s.id }}" {% if s.id == current_session.id %}selected{% endif %}>{{ s.name }}</option>
{% endfor %}
</select>
{% if cursid %}
{% multiwell_cards cursid experiments %}
<div class="w3-margin-left w3-margin-bottom">
{% for ss in experiments %}
<input class="" type="radio" name="_expid" value="{{ ss.id }}" {% if ss.id == current_experiment.id %}checked{% endif %} onchange="this.form.submit()" >
<label>{{ ss.multiwell }}</label>
{% endfor %}
</div>
{% if current_session.id and current_experiment %}
{% multiwell_cards current_session.id current_experiment %}
{% endif %}
</form>
{% if cursid %}
{% if current_session.id %}
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_images()">
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des images" %}
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des images" %}<br>
<span class="w3-margin-left">{{ export_destination }}
</a>
{% endif %}
{% endblock %}
@@ -95,14 +102,15 @@
action: 'export_images',
sid: sid,
})
}).then(res => {
console.log(res);
}).catch(error => {
})
.then(response => response.json())
.then(res => {
alert(res.msg);
})
.catch(error => {
console.error('Erreur:', error);
});
}
setGridColumns(columns);
if (uuid_btn)
handleMultiwell(uuid_btn);
@@ -11,18 +11,26 @@
<form method="post" class="w3-bar-item" action="">
{% csrf_token %}
<select id="_sid" name="_sid" class="w3-select w3-margin-bottom" onchange="this.form.submit()">
<option value="0">---- {% trans "Session d'expériences" %}</option>
<option value="0">---- {% trans "Session" %}</option>
{% for s in sessions %}
<option value="{{ s.id }}" {% if s.id == cursid %}selected{% endif %}>{{ s.name }}</option>
<option value="{{ s.id }}" {% if s.id == current_session.id %}selected{% endif %}>{{ s.name }}</option>
{% endfor %}
</select>
{% if cursid %}
{% multiwell_cards cursid experiments %}
<div class="w3-margin-left w3-margin-bottom">
{% for ss in experiments %}
<input class="" type="radio" name="_expid" value="{{ ss.id }}" {% if ss.id == current_experiment.id %}checked{% endif %} onchange="this.form.submit()" >
<label>{{ ss.multiwell }}</label>
{% endfor %}
</div>
{% if current_session.id and current_experiment %}
{% multiwell_cards current_session.id current_experiment %}
{% endif %}
</form>
{% if cursid %}
{% if current_session.id %}
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_videos()">
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des vidéos" %}
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des vidéos" %}<br>
<span class="w3-margin-left">{{ export_destination }}</span>
</a>
{% endif %}
{% endblock %}
@@ -80,9 +88,12 @@
action: 'export_videos',
sid: sid,
})
}).then(res => {
console.log(res);
}).catch(error => {
})
.then(response => response.json())
.then(res => {
alert(res.msg);
})
.catch(error => {
console.error('Erreur:', error);
});
}
@@ -5,22 +5,19 @@ from django.utils.html import mark_safe
register = template.Library()
@register.simple_tag
def multiwell_cards(sid, experiments):
def multiwell_cards(sid, experiment):
multiwells = []
for obs in experiments:
row_def = obs.multiwell.row_def.split(',')
multiwells.append(
f'''
<div class="w3-center w3-sand">{obs.title}</div>
<div class="w3-border multiwell_cards">
''')
for row in range(obs.multiwell.rows):
for col in range(obs.multiwell.cols):
btn = f'{row_def[row]}{col+1}'
uuid = f'{sid}-{obs.multiwell.position}-{btn}'
multiwells.append(f"""<button id="btn-{uuid}" name="_multiwell" class="multiwell w3-button" value="{uuid}" onclick="this.form.submit()">{btn}</button>""")
multiwells.append('''</div>''')
row_def = experiment.multiwell.row_def.split(',')
multiwells.append(
f'''
<div class="w3-center w3-sand">{experiment.title}</div>
<div class="w3-border multiwell_cards">
''')
for row in range(experiment.multiwell.rows):
for col in range(experiment.multiwell.cols):
btn = f'{row_def[row]}{col+1}'
uuid = f'{sid}-{experiment.multiwell.position}-{btn}'
multiwells.append(f"""<button id="btn-{uuid}" name="_multiwell" class="multiwell w3-button" value="{uuid}" onclick="this.form.submit()">{btn}</button>""")
multiwells.append('''</div>''')
return mark_safe("\n".join(multiwells))
+32 -14
View File
@@ -35,14 +35,13 @@ def stats_view(request):
def global_context(request, **ctx):
default_multiwell = models.MultiWell.objects.filter(default=True).first()
conf = ScannerConstants().get()
print(conf, type(conf))
return dict(
app_title=settings.APP_TITLE,
app_sub_title=settings.APP_SUB_TITLE,
domain_server=settings.DOMAIN_SERVER,
conf=conf,
default_position = default_multiwell.position or 'HD',
export_destination=settings.EXPORT_DESTINATIONS,
**ctx
)
@@ -97,6 +96,17 @@ def calibration_view(request):
return render(request, "scanner/calibration.html", context=global_context(request, **ctx))
def get_not_active_experiments(session, expid=None):
if session:
experiments = models.SessionExperiment.experiment_by_session(session.id, active=False) or []
if experiments and not expid:
return experiments, experiments[0]
for e in experiments:
if expid == str(e.id):
return experiments, e
return [], None
## images
def get_images(uuid):
oldest, latest, n, images = 0, 0, 0, []
@@ -124,17 +134,21 @@ def get_images(uuid):
@login_required
def images_view(request):
cursid, duration, uuid, images = 0, 0, "", []
cursid, expid, duration, uuid, images = 0, None, 0, "", []
if request.method == 'POST':
cursid = request.POST.get('_sid')
expid = request.POST.get('_expid')
uuid = request.POST.get('_multiwell')
duration, images = get_images(uuid)
current_session = models.Session.get_session(cursid)
experiments, current_experiment = get_not_active_experiments(current_session, expid)
ctx = dict(
choice_title=_("Gestionnaire d'images"),
sessions=models.Session.objects.filter(active=False).all(),
experiments=models.SessionExperiment.experiment_by_session(cursid, active=False),
cursid=int(cursid),
experiments=experiments or [],
current_session=current_session,
current_experiment=current_experiment,
images=images,
uuid=uuid,
duration=duration,
@@ -164,20 +178,24 @@ def get_video(uuid):
@login_required
def replay_view(request):
cursid, oldest, latest, uuid, image = 0, 0, 0, "", ""
cursid, expid, oldest, latest, uuid, image = 0, None, 0, 0, "", ""
if request.method == 'POST':
cursid = request.POST.get('_sid')
expid = request.POST.get('_expid')
uuid = request.POST.get('_multiwell')
if uuid:
oldest, latest, image = get_video(uuid)
current_session = models.Session.get_session(cursid)
experiments, current_experiment = get_not_active_experiments(current_session, expid)
ctx = dict(
choice_title=_("Gestionnaire de vidéos"),
ws_route=settings.REPLAY_WEBSOCKET_ROUTE,
sessions=models.Session.objects.filter(active=False).all(),
experiments=models.SessionExperiment.experiment_by_session(cursid, active=False),
cursid=int(cursid),
experiments=experiments or [],
current_session=current_session,
current_experiment=current_experiment,
image=image,
uuid=uuid,
oldest=oldest,
@@ -193,12 +211,12 @@ def export_api(request):
action = data.get("action")
if action == 'export_images':
job_zip = export_all_images(session_id)
return JsonResponse({"state": True, "tasks": job_zip})
export_all_images(session_id)
return JsonResponse({"success": True, "msg": str(_("Images téléchargées"))})
elif action == 'export_videos':
job_mp4 = export_all_videos(session_id)
return JsonResponse({"state": True, "tasks": job_mp4})
export_all_videos(session_id)
return JsonResponse({"success": True, "msg": str(_("Vidéos téléchargées"))})
else:
return JsonResponse({"state": False})
return JsonResponse({"success": False, "msg": str(_("Erreur d'exportation"))})