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