export-all-verif

This commit is contained in:
2026-05-19 10:53:51 +02:00
parent 5477de46fe
commit 308ddaa048
14 changed files with 345 additions and 132 deletions
+1
View File
@@ -20,6 +20,7 @@ EXPORTS_LOCAL_PATH=/home/rpi4/exports
EXPORT_REMOTE_PATH=/mnt/exports EXPORT_REMOTE_PATH=/mnt/exports
REDUCTSTORE_PATH=/home/rpi4/medias REDUCTSTORE_PATH=/home/rpi4/medias
CSV_EXPORT_DIR=/home/rpi4/exports/csv CSV_EXPORT_DIR=/home/rpi4/exports/csv
BACKUP_DIR=/home/rpi4/backup/mariadb
#### ####
# django email # django email
+61
View File
@@ -0,0 +1,61 @@
'''
Created on 18 mai 2026
@author: denis
'''
from pathlib import Path
from datetime import datetime
import subprocess
import gzip
import shutil
from django.conf import settings
BACKUP_DIR = Path(settings.BACKUP_DIR)
def mariadb_backup():
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
db = settings.DATABASES["default"]
db_name = db["NAME"]
db_user = db["USER"]
db_password = db["PASSWORD"]
db_host = db.get("HOST", "localhost")
db_port = str(db.get("PORT", 3306))
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
sql_file = BACKUP_DIR / f"{db_name}_{timestamp}.sql"
gz_file = BACKUP_DIR / f"{db_name}_{timestamp}.sql.gz"
cmd = [
"mariadb-dump",
"--single-transaction",
"--quick",
"--routines",
"--triggers",
"-h", db_host,
"-P", db_port,
"-u", db_user,
f"-p{db_password}",
db_name,
]
with open(sql_file, "wb") as f:
result = subprocess.run(
cmd,
stdout=f,
stderr=subprocess.PIPE,
check=True,
)
with open(sql_file, "rb") as f_in:
with gzip.open(gz_file, "wb") as f_out:
shutil.copyfileobj(f_in, f_out)
sql_file.unlink()
return str(gz_file)
+18
View File
@@ -0,0 +1,18 @@
'''
Created on 18 mai 2026
@author: denis
'''
from celery import shared_task
from .services import mariadb_backup
@shared_task
def backup_mariadb_task():
path = mariadb_backup()
return {
"status": "ok",
"backup": path,
}
+3
View File
@@ -8,3 +8,6 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'home.settings')
appc = Celery('home') appc = Celery('home')
appc.config_from_object('django.conf:settings', namespace='CELERY') appc.config_from_object('django.conf:settings', namespace='CELERY')
appc.autodiscover_tasks() appc.autodiscover_tasks()
worker_prefetch_multiplier = 1
task_acks_late = True
+3
View File
@@ -345,6 +345,7 @@ DJANGO_CELERY_BEAT_TZ_AWARE = False
CELERY_ENABLE_UTC = True CELERY_ENABLE_UTC = True
CELERY_TASK_TRACK_STARTED = True CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 CELERY_TASK_TIME_LIMIT = 30 * 60
CELERY_TASK_SOFT_TIME_LIMIT = 20 * 60
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler' CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_RESULT_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json'
@@ -362,6 +363,8 @@ REDUCTSTORE_PORT = config('REDUCTSTORE_PORT', cast=int)
REDUCTSTORE_URL = f'http://{REDUCTSTORE_HOST}:{REDUCTSTORE_PORT}' REDUCTSTORE_URL = f'http://{REDUCTSTORE_HOST}:{REDUCTSTORE_PORT}'
REDUCTSTORE_PATH = config("REDUCTSTORE_PATH") REDUCTSTORE_PATH = config("REDUCTSTORE_PATH")
BACKUP_DIR = config('BACKUP_DIR')
## servers app ## servers app
# #
USER = config('USER') USER = config('USER')
+55 -45
View File
@@ -687,16 +687,8 @@ class ReductStoreClient:
logger.info(f"ReductStore connecté : {self.url} / {self.bucket_name}") logger.info(f"ReductStore connecté : {self.url} / {self.bucket_name}")
''' '''
async def store_metric( async def store_metric(self, record: dict, experiment: str, well: str, uuid: str,
self, planarian: int = 0, record_type: str = "frame", ts_us: Optional[int] = None, ):
record: dict,
experiment: str,
well: str,
planarian: int = 0,
record_type: str = "frame",
uuid: str = "",
ts_us: Optional[int] = None,
):
""" """
Stocke un enregistrement dans ReductStore. Stocke un enregistrement dans ReductStore.
@@ -705,17 +697,18 @@ class ReductStoreClient:
quand plusieurs planaires du même puits écrivent dans la même frame. quand plusieurs planaires du même puits écrivent dans la même frame.
Args: Args:
uuid : identifiant unique de session (permet de filtrer
plusieurs sessions d'un même puits/expérience)
record : dict de métriques (issu de EthoVisionMetrics.update()) record : dict de métriques (issu de EthoVisionMetrics.update())
experiment : identifiant de l'expérience experiment : identifiant de l'expérience
well : identifiant du puits well : identifiant du puits
planarian : index du planaire (défaut 0) planarian : index du planaire (défaut 0)
record_type : "frame" ou "summary" record_type : "frame" ou "summary"
uuid : identifiant unique de session (permet de filtrer
plusieurs sessions d'un même puits/expérience)
ts_us : timestamp en microsecondes (défaut : maintenant) ts_us : timestamp en microsecondes (défaut : maintenant)
""" """
if self._bucket is None: if self._bucket is None:
await self.connect() await self.connect()
# ts_us de base + offset planaire (0, 1, 2…) pour unicité garantie # ts_us de base + offset planaire (0, 1, 2…) pour unicité garantie
base_ts = ts_us or int(time.time() * 1_000_000) base_ts = ts_us or int(time.time() * 1_000_000)
unique_ts = base_ts + planarian unique_ts = base_ts + planarian
@@ -725,10 +718,13 @@ class ReductStoreClient:
"planarian": str(planarian), "planarian": str(planarian),
"record_type": record_type, "record_type": record_type,
} }
"""
if uuid: if uuid:
labels["uuid"] = uuid labels["uuid"] = uuid"""
await self._bucket.write( await self._bucket.write(
entry_name = "metrics", #entry_name = "metrics",
entry_name = uuid,
data = json.dumps(record).encode("utf-8"), data = json.dumps(record).encode("utf-8"),
timestamp = unique_ts, timestamp = unique_ts,
labels = labels, labels = labels,
@@ -740,8 +736,9 @@ class ReductStoreClient:
summary: dict, summary: dict,
experiment: str, experiment: str,
well: str, well: str,
uuid: str,
planarian: int = 0, planarian: int = 0,
uuid: str = "", record_type: str = "summary",
): ):
""" """
Stocke le résumé de fin de session dans ReductStore. Stocke le résumé de fin de session dans ReductStore.
@@ -750,26 +747,20 @@ class ReductStoreClient:
summary : dict issu de EthoVisionMetrics.summary() summary : dict issu de EthoVisionMetrics.summary()
experiment : identifiant de l'expérience experiment : identifiant de l'expérience
well : identifiant du puits well : identifiant du puits
planarian : index du planaire
uuid : identifiant unique de session (même valeur que uuid : identifiant unique de session (même valeur que
celle utilisée dans store_metric pour cette session) celle utilisée dans store_metric pour cette session)
planarian : index du planaire
""" """
await self.store_metric( await self.store_metric(record=summary,experiment=experiment,
record = summary, well=well, uuid=uuid, planarian=planarian, record_type=record_type)
experiment = experiment,
well = well,
planarian = planarian,
record_type = "summary",
uuid = uuid,
)
async def get_tracking_data( async def get_tracking_data(
self, self,
experiment: str, experiment: str,
well: str, well: str,
uuid: str,
planarian: int = 0, planarian: int = 0,
record_type: str = "frame", record_type: str = "frame",
uuid: str = "",
start: Optional[datetime] = None, start: Optional[datetime] = None,
stop: Optional[datetime] = None, stop: Optional[datetime] = None,
) -> list: ) -> list:
@@ -779,29 +770,31 @@ class ReductStoreClient:
Args: Args:
experiment : identifiant de l'expérience experiment : identifiant de l'expérience
well : identifiant du puits well : identifiant du puits
planarian : index du planaire
record_type : "frame" | "summary"
uuid : filtre sur une session spécifique (optionnel — uuid : filtre sur une session spécifique (optionnel —
si vide, retourne toutes les sessions) si vide, retourne toutes les sessions)
planarian : index du planaire
record_type : "frame" | "summary"
start, stop : plage temporelle (datetime UTC, optionnel) start, stop : plage temporelle (datetime UTC, optionnel)
""" """
if self._bucket is None: if self._bucket is None:
await self.connect() await self.connect()
labels = {
"experiment": experiment, when = {
"well": well, "$and": [
"planarian": str(planarian), #{"&experiment": {"$contains": experiment}},
"record_type": record_type, #{"&well": {"$contains": well}},
{"&planarian": {"$contains": str(planarian)}},
{"&record_type": {"$contains": record_type}},
]
} }
if uuid: kwargs = {'when': when}
labels["uuid"] = uuid
kwargs = {"include": labels}
if start: if start:
kwargs["start"] = int(start.timestamp() * 1_000_000) kwargs["start"] = int(start.timestamp() * 1_000_000)
if stop: if stop:
kwargs["stop"] = int(stop.timestamp() * 1_000_000) kwargs["stop"] = int(stop.timestamp() * 1_000_000)
records = [] records = []
async for rec in self._bucket.query("metrics", **kwargs): async for rec in self._bucket.query(uuid, **kwargs):
try: try:
records.append(json.loads(await rec.read_all())) records.append(json.loads(await rec.read_all()))
except Exception as e: except Exception as e:
@@ -854,16 +847,16 @@ class ReductStoreClient:
""" """
dirpath = os.path.abspath(output_dir) dirpath = os.path.abspath(output_dir)
os.makedirs(dirpath, exist_ok=True) os.makedirs(dirpath, exist_ok=True)
filename = f"{experiment}_{well}_planaire{planarian:02d}_{record_type}.csv" filename = f"{experiment}_{well}_planaire-{planarian}-{record_type}.csv"
return os.path.join(dirpath, filename) return os.path.join(dirpath, filename)
async def export_csv( async def export_csv(
self, self,
experiment: str, experiment: str,
well: str, well: str,
uuid: str,
planarian: int = 0, planarian: int = 0,
record_type: str = "frame", record_type: str = "frame",
uuid: str = "",
output_dir: str = ".", output_dir: str = ".",
start: Optional[datetime] = None, start: Optional[datetime] = None,
stop: Optional[datetime] = None, stop: Optional[datetime] = None,
@@ -876,25 +869,34 @@ class ReductStoreClient:
Args: Args:
experiment : identifiant de l'expérience experiment : identifiant de l'expérience
well : identifiant du puits well : identifiant du puits
uuid : filtre sur une session spécifique (optionnel —
si vide, retourne toutes les sessions)
planarian : index du planaire planarian : index du planaire
record_type : "frame" | "summary" record_type : "frame" | "summary"
output_dir : répertoire de sortie (défaut : répertoire courant) output_dir : répertoire de sortie (défaut : répertoire courant)
uuid : filtre sur une session spécifique (optionnel —
si vide, retourne toutes les sessions)
start, stop : plage temporelle (datetime UTC, optionnel) start, stop : plage temporelle (datetime UTC, optionnel)
Returns: Returns:
tuple (filepath, nb_lignes) tuple (filepath, nb_lignes)
""" """
# record, experiment, well, uuid,
records = await self.get_tracking_data( records = await self.get_tracking_data(
experiment, well, planarian, record_type, uuid, start, stop) experiment,
well,
uuid,
planarian=planarian,
record_type=record_type,
start=start,
stop=stop,
)
if not records: if not records:
logger.warning(f"Aucune donnée pour {experiment}/{well}/{planarian}") logger.warning(f"Aucune donnée pour {experiment}/{well}/{planarian}")
return "", 0 return "", 0
records = self._convert_timestamps(records) records = self._convert_timestamps(records)
filepath = self._build_filepath(output_dir, experiment, well, filepath = self._build_filepath(output_dir, experiment, well, planarian, record_type)
planarian, record_type)
fieldnames = list(dict.fromkeys(k for r in records for k in r.keys())) fieldnames = list(dict.fromkeys(k for r in records for k in r.keys()))
with open(filepath, "w", newline="", encoding="utf-8") as f: with open(filepath, "w", newline="", encoding="utf-8") as f:
@@ -909,9 +911,9 @@ class ReductStoreClient:
self, self,
experiment: str, experiment: str,
well: str, well: str,
uuid: str,
planarian: int = 0, planarian: int = 0,
record_type: str = "frame", record_type: str = "frame",
uuid: str = "",
start: Optional[datetime] = None, start: Optional[datetime] = None,
stop: Optional[datetime] = None, stop: Optional[datetime] = None,
) -> tuple: ) -> tuple:
@@ -923,7 +925,15 @@ class ReductStoreClient:
tuple (contenu_csv_str, nb_lignes) tuple (contenu_csv_str, nb_lignes)
""" """
records = await self.get_tracking_data( records = await self.get_tracking_data(
experiment, well, planarian, record_type, uuid, start, stop) experiment,
well,
uuid,
planarian=planarian,
record_type=record_type,
start=start,
stop=stop,
)
if not records: if not records:
return "", 0 return "", 0
records = self._convert_timestamps(records) records = self._convert_timestamps(records)
@@ -0,0 +1,68 @@
import logging
import asyncio
from asgiref.sync import async_to_sync
from django.conf import settings
from modules.planarian_metrics import ReductStoreClient
logger = logging.getLogger(__name__)
def _get_reduct_client() -> ReductStoreClient:
"""Instancie le client ReductStore depuis les settings Django."""
return ReductStoreClient(url=settings.REDUCTSTORE_URL, token=settings.REDUCTSTORE_TOKEN)
def export_csv_sync(
*,
experiment,
well,
uuid,
planarian,
record_type,
start=None,
stop=None,
):
#@async_to_sync
async def _run():
client = _get_reduct_client()
await client.connect()
return await client.export_csv_response(
experiment=experiment,
well=well,
uuid=uuid,
planarian=planarian,
record_type=record_type,
start=start,
stop=stop,
)
#return _run()
return asyncio.run(_run())
def export_csv_file_sync(
*,
experiment,
well,
uuid,
planarian,
record_type,
):
async def _run():
client = _get_reduct_client()
await client.connect()
try:
return await client.export_csv(
experiment=experiment,
well=well,
uuid=uuid,
planarian=planarian,
record_type=record_type,
output_dir=settings.CSV_EXPORT_DIR,
)
finally:
await client.close()
return asyncio.run(_run())
+2 -1
View File
@@ -143,7 +143,8 @@ class ExperimentConfig(models.Model):
def get_session(self): def get_session(self):
return self.experiment_key.session_experiments.first() if self.experiment_key else None se = self.experiment_key.session_experiments.first() if self.experiment_key else None
return se.session
def to_params_dict(self) -> dict: def to_params_dict(self) -> dict:
+59 -33
View File
@@ -1,54 +1,80 @@
# tasks.py # tasks.py
from celery import shared_task from celery import shared_task, group
from celery.utils.log import get_task_logger from celery.utils.log import get_task_logger
from django.conf import settings from scanner.models import SessionExperiment, get_uuid_from_session, Experiment
from asgiref.sync import async_to_sync
from modules.planarian_metrics import ReductStoreClient
from scanner.models import SessionExperiment
from .models import ExperimentConfig from .models import ExperimentConfig
from .export_service import export_csv_file_sync
logger = get_task_logger(__name__) logger = get_task_logger(__name__)
def _get_reduct_client() -> ReductStoreClient:
"""Instancie le client ReductStore depuis les settings Django.""" @shared_task(bind=True, autoretry_for=(Exception,), retry_backoff=True, retry_kwargs={"max_retries": 3},)
return ReductStoreClient(url=settings.REDUCTSTORE_URL, token=settings.REDUCTSTORE_TOKEN) def export_single_metric(self, *, experiment, well, uuid, planarian, record_type,):
"""
Exporte UN fichier CSV.
"""
logger.info( f"Export start {experiment} {well} {planarian} {record_type}")
f, n = export_csv_file_sync(
experiment=experiment,
well=well,
uuid=uuid,
planarian=planarian,
record_type=record_type,
)
logger.info( f"Export done {f} ({n} rows)" )
return {"file": f, "rows": n, }
@shared_task @shared_task
def export_experiment_metrics(experiment): def export_experiment_metrics_task(experiment_id):
experiment = Experiment.objects.filter(pk=experiment_id).first()
if not experiment:
return
@async_to_sync jobs = []
async def _do_export(well, planarian, record_type): configs = (ExperimentConfig.objects.filter(experiment_key_id=experiment.id).order_by("well"))
client = _get_reduct_client() for conf in configs:
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 well = conf.well
count = conf.planarian_count count = conf.planarian_count
session = conf.get_session()
uuid = get_uuid_from_session(session.id, conf.experiment_key.multiwell.position, well, )
for record_type in ["frame", "summary"]: for record_type in ["frame", "summary"]:
for planarian in range(count): for planarian in range(count):
_do_export(well, planarian, record_type) '''
jobs.append(
export_single_metric.s(
experiment=experiment.identifier,
well=well,
uuid=uuid,
planarian=planarian,
record_type=record_type,
)
)
'''
try:
f, n = export_csv_file_sync(
experiment=experiment.identifier,
well=well,
uuid=uuid,
planarian=planarian,
record_type=record_type,
)
logger.info( f"Export done {f} ({n} rows)" )
except Exception as e:
logger.exception(e)
'''
result = group(jobs).apply_async()
logger.info(f"{len(jobs)} exports launched group_id={result.id}")
return {"group_id": result.id,"jobs": len(jobs), }'''
@shared_task @shared_task
def export_session_metrics(session): def export_session_metrics_task(session_id):
experiments = SessionExperiment.experiment_by_session(session.id) experiments = SessionExperiment.experiment_by_session(session_id, active=False)
for experiment in experiments: for experiment in experiments:
export_experiment_metrics(experiment) export_experiment_metrics_task.delay(experiment.id)
@@ -27,6 +27,12 @@
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter les metrics de l'expérience" %}<br> <i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter les metrics de l'expérience" %}<br>
<span class="w3-margin-left-5">&nbsp;$> {{ export_csv_destination }} <span class="w3-margin-left-5">&nbsp;$> {{ export_csv_destination }}
</a> </a>
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="export_csv('{{ url_export }}', {{ current_session.id }}, 'session_csv')">
<i class="fa-solid fa-file-export w3-text-pink w3-xxlarge"></i> {% trans "Exporter les metrics de la session" %}<br>
<span class="w3-margin-left-5">&nbsp;$> {{ export_csv_destination }}
</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}
@@ -34,6 +40,7 @@
{% block js_footer %} {% block js_footer %}
{{ block.super }} {{ block.super }}
<script> <script>
const export_msg = "{% trans "Confirmer l'export des vidéos. ATTENTION le processus peut être long!" %}";
// Auto-dismiss messages after 5 seconds // Auto-dismiss messages after 5 seconds
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const messages = document.querySelectorAll('.alert'); const messages = document.querySelectorAll('.alert');
@@ -21,6 +21,7 @@
<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>
+34 -45
View File
@@ -20,7 +20,9 @@ from django.views import View
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 .tasks import export_experiment_metrics_task, export_session_metrics_task
from .export_service import export_csv_sync
from scanner import models, views as scanner_views from scanner import models, views as scanner_views
from .models import ExperimentConfig from .models import ExperimentConfig
@@ -82,7 +84,6 @@ def get_active_session(request, session_id=None, experiment_id=None):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Vue :Export CSV depuis ReductStore # Vue :Export CSV depuis ReductStore
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@login_required @login_required
@csrf_exempt @csrf_exempt
def export_metrics(request): def export_metrics(request):
@@ -92,48 +93,13 @@ def export_metrics(request):
if action=='experiment_csv': if action=='experiment_csv':
experiment = models.Experiment.objects.filter(pk=pid).first() experiment = models.Experiment.objects.filter(pk=pid).first()
if experiment: if experiment:
export_experiment_metrics(experiment) export_experiment_metrics_task.delay(experiment.id) # @UndefinedVariable
return JsonResponse({"state": True}) return JsonResponse({"success": True, "msg": str(_("Métrics en cours de téléchargement"))})
if action=='session_csv': if action=='session_csv':
session = models.Session.objects.filter(pk=pid).first() if pid:
if session: export_session_metrics_task(pid) # @UndefinedVariable
export_session_metrics(session) return JsonResponse({"success": True, "msg": str(_("Métrics en cours de téléchargement"))})
return JsonResponse({"state": True}) return JsonResponse({"success": False, "msg": str(_("Erreur: les métrics non pas été téléchargés"))})
return JsonResponse({"state": False})
def export_csv(request):
d = request.POST
print("export_csv===========", d)
@async_to_sync
async def _do_export():
client = _get_reduct_client()
await client.connect()
try:
csv_content, n = await client.export_csv_response(
experiment = d.get("experiment"),
well = d.get("well"),
planarian = d.get("planarian"),
record_type = d.get("record_type"),
start = d.get("start_dt"),
stop = d.get("stop_dt"),
)
print(f"Export CSV: export_csv_response done, {n} lignes, content size={len(csv_content)}")
except Exception as e:
logger.error(f"Erreur export CSV: {e}")
messages.error(request, _("Erreur lors de l'export CSV: %(error)s") % {"error": str(e)})
return None, 0
return csv_content, n
csv_content, n = _do_export()
logger.info(f"Export CSV: {n} lignes, content size={len(csv_content)}")
filename = (
f"{d['experiment']}_{d['well']}-planaire-{d['planarian']}"
f"_{d['record_type']}.csv"
)
return csv_content, filename
@login_required @login_required
@@ -148,11 +114,34 @@ def export_csv_view(request):
} }
if request.method == 'POST': if request.method == 'POST':
valid = request.POST.get('valid') valid = request.POST.get('valid')
if valid == 'ok': session = session_context['current_session']
csv_content, filename = export_csv(request) experiment = session_context['current_experiment']
if valid == 'ok' and session and experiment:
well_name = request.POST.get('well')
uuid = models.get_uuid_from_session(session.id, experiment.multiwell.position, well_name)
'''
csv_content, filename = export_csv(request, uuid)
if csv_content: if csv_content:
response = FileResponse(csv_content, content_type="text/csv; charset=utf-8") response = FileResponse(csv_content, content_type="text/csv; charset=utf-8")
response["Content-Disposition"] = f'attachment; filename="{filename}"' response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response'''
csv_content, n = export_csv_sync(
experiment=experiment.identifier,
well=well_name,
uuid=uuid,
planarian=request.POST.get("planarian"),
record_type=request.POST.get("record_type"),
start=request.POST.get("start_dt"),
stop=request.POST.get("stop_dt"),
)
if csv_content:
filename = (
f"{experiment.identifier}_{well_name}-{request.POST.get('planarian')}_"
f"{request.POST.get('record_type')}.csv"
)
logger.info(f"Export CSV: {n} lignes, content size={len(csv_content)}")
response = FileResponse(csv_content, content_type="text/csv; charset=utf-8")
response["Content-Disposition"] = (f'attachment; filename="{filename}"')
return response return response
messages.warning(request, _("Aucune donnée trouvée.")) messages.warning(request, _("Aucune donnée trouvée."))
+15 -1
View File
@@ -258,7 +258,6 @@ class Experiment(models.Model):
def by_identifier(cls, identifier): def by_identifier(cls, identifier):
return Experiment.objects.filter(identifier__exact=identifier).first() return Experiment.objects.filter(identifier__exact=identifier).first()
class Meta: class Meta:
ordering = ['-created', ] ordering = ['-created', ]
verbose_name = _("Expérience") verbose_name = _("Expérience")
@@ -376,15 +375,21 @@ def delete_periodic_task(sender, instance, **kwargs):
instance.scanning_task.delete() instance.scanning_task.delete()
def get_uuid_from_session(session_id, multiwel_position, well_name):
return f'{session_id}-{multiwel_position}-{well_name}'
class SessionExperiment(models.Model): class SessionExperiment(models.Model):
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True) author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
session = models.ForeignKey(Session, verbose_name=_("Session"), on_delete=models.CASCADE, null=True, blank=True) session = models.ForeignKey(Session, verbose_name=_("Session"), on_delete=models.CASCADE, null=True, blank=True)
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.CASCADE, null=True, blank=True, related_name="session_experiments") experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.CASCADE, null=True, blank=True, related_name="session_experiments")
@classmethod @classmethod
def experiment_by_session(cls, session_id, active=True): def experiment_by_session(cls, session_id, active=True):
return [ ss.experiment for ss in SessionExperiment.objects.filter(session__id=session_id, session__active=active).order_by('experiment__multiwell__order') ] return [ ss.experiment for ss in SessionExperiment.objects.filter(session__id=session_id, session__active=active).order_by('experiment__multiwell__order') ]
@classmethod @classmethod
def uuid_from_session(cls, sid): def uuid_from_session(cls, sid):
experiments = [ss.experiment for ss in SessionExperiment.objects.filter(session__id=sid, session__active=False)] experiments = [ss.experiment for ss in SessionExperiment.objects.filter(session__id=sid, session__active=False)]
@@ -397,6 +402,15 @@ class SessionExperiment(models.Model):
uuid_list.append(uuid) uuid_list.append(uuid)
return uuid_list return uuid_list
@classmethod
def get_uuid(cls, session_id, experiment_id, well_name):
ss = SessionExperiment.objects.filter(session__id=session_id, experiment_id=experiment_id).first()
if ss:
return get_uuid_from_session(session_id, ss.experiment.multiwel.position, well_name)
return None
class Meta: class Meta:
ordering = ['session',] ordering = ['session',]
unique_together = ["session", "experiment"] unique_together = ["session", "experiment"]
+14 -3
View File
@@ -39,6 +39,7 @@ def stats_view(request):
except Exception as e: except Exception as e:
return JsonResponse({"error": str(e)}, status=500) return JsonResponse({"error": str(e)}, status=500)
def global_context(request, **ctx): 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()
@@ -60,11 +61,12 @@ def global_context(request, **ctx):
def admin_view(request): def admin_view(request):
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/')) return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/'))
@login_required
@login_required
def admin_session_view(request): def admin_session_view(request):
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/scanner/session/')) return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/scanner/session/'))
@login_required @login_required
def admin_experiment_view(request): def admin_experiment_view(request):
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/scanner/experiment/')) return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/scanner/experiment/'))
@@ -73,15 +75,18 @@ def admin_experiment_view(request):
def admin_experimentconfig_view(request): def admin_experimentconfig_view(request):
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/planarian/experimentconfig/')) return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/planarian/experimentconfig/'))
@login_required @login_required
def admin_periodictask_view(request): def admin_periodictask_view(request):
return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/django_celery_beat/periodictask/')) return render(request, "scanner/iframe.html", context=global_context(request, link='/admin/django_celery_beat/periodictask/'))
@login_required @login_required
@user_passes_test(is_staff_or_admin) @user_passes_test(is_staff_or_admin)
def reductstore_view(request): def reductstore_view(request):
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:8383/')) return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:8383/'))
@login_required @login_required
@user_passes_test(is_staff_or_admin) @user_passes_test(is_staff_or_admin)
def adminer_view(request): def adminer_view(request):
@@ -92,21 +97,25 @@ def adminer_view(request):
def portainer_view(request): def portainer_view(request):
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9000/')) return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9000/'))
@login_required @login_required
@user_passes_test(is_staff_or_admin) @user_passes_test(is_staff_or_admin)
def supervisor_view(request): def supervisor_view(request):
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/')) return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/'))
@login_required @login_required
@user_passes_test(is_staff_or_admin) @user_passes_test(is_staff_or_admin)
def supervisor_worker(request): def supervisor_worker(request):
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:services')) return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:services'))
@login_required @login_required
@user_passes_test(is_staff_or_admin) @user_passes_test(is_staff_or_admin)
def supervisor_scheduler(request): def supervisor_scheduler(request):
return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:planification')) return render(request, "scanner/redirection.html", context=global_context(request, link=f'http://{settings.LOCAL_IP_SERVER}:9001/logtail/test_tube:planification'))
def documentation(request, template=None): def documentation(request, template=None):
ctx = dict( ctx = dict(
choice_title=_("Documentation") choice_title=_("Documentation")
@@ -225,6 +234,7 @@ def export_medias(request):
) )
return render(request, "scanner/export_medias.html", context=global_context(request, **ctx)) return render(request, "scanner/export_medias.html", context=global_context(request, **ctx))
@require_POST @require_POST
@csrf_exempt @csrf_exempt
def download_api(request): def download_api(request):
@@ -286,6 +296,7 @@ def replay_view(request):
) )
return render(request, "scanner/replay.html", context=global_context(request, **ctx)) return render(request, "scanner/replay.html", context=global_context(request, **ctx))
@require_POST @require_POST
@csrf_exempt @csrf_exempt
def export_api(request): def export_api(request):
@@ -295,10 +306,10 @@ def export_api(request):
try: try:
if action == 'export_images': if action == 'export_images':
export_all_images(session_id) export_all_images(session_id)
return JsonResponse({"success": True, "msg": str(_("Images téléchargées"))}) return JsonResponse({"success": True, "msg": str(_("Images en cours de téléchargement"))})
elif action == 'export_videos': elif action == 'export_videos':
export_all_videos(session_id) export_all_videos(session_id)
return JsonResponse({"success": True, "msg": str(_("Vidéos téléchargées"))}) return JsonResponse({"success": True, "msg": str(_("Vidéos en cours de téléchargement"))})
except: except:
return JsonResponse({"state": False, "success": False, "msg": str(_("Erreur d'exportation")), }) return JsonResponse({"state": False, "success": False, "msg": str(_("Erreur d'exportation")), })