forked from ScoDoc/ScoDoc
assiduites : grosses modifs WIP
- trace justificatifs - migrer entry_date - calcul des assiduités justifiées - ajout colonnes user_id et est_just - bug fix timezone max - remise à zero séquence, cmd downgrade assiduite (si dept none ) - API : filtrage par user_id et par est_just WIP
This commit is contained in:
parent
4c648212dd
commit
b73a02ac67
|
@ -7,7 +7,7 @@
|
|||
"""
|
||||
from datetime import datetime
|
||||
from flask import g, jsonify, request
|
||||
from flask_login import login_required
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -17,6 +17,7 @@ from app.api import api_web_bp
|
|||
from app.api import get_model_api_object
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Assiduite, FormSemestre, Identite, ModuleImpl
|
||||
from app.auth.models import User
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
@ -38,6 +39,8 @@ def assiduite(assiduite_id: int = None):
|
|||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "retard",
|
||||
"desc": "une description",
|
||||
"user_id: 1 or null,
|
||||
"est_just": False or True,
|
||||
}
|
||||
"""
|
||||
|
||||
|
@ -83,8 +86,16 @@ def count_assiduites(etudid: int = None, with_query: bool = False):
|
|||
ex: query?moduleimpl_id=1234
|
||||
query?moduleimpl_od=
|
||||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
query?formsemestre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
|
||||
|
||||
"""
|
||||
|
@ -142,6 +153,13 @@ def assiduites(etudid: int = None, with_query: bool = False):
|
|||
Formsemstre_id (l'id du formsemestre concerné par l'assiduité)
|
||||
query?formsemstre_id=[int]
|
||||
ex query?formsemestre_id=3
|
||||
user_id (l'id de l'auteur de l'assiduité)
|
||||
query?user_id=[int]
|
||||
ex query?user_id=3
|
||||
est_just (si l'assiduité est justifié (fait aussi filtre par abs/retard))
|
||||
query?est_just=[bool]
|
||||
query?est_just=f
|
||||
query?est_just=t
|
||||
|
||||
|
||||
"""
|
||||
|
@ -289,6 +307,8 @@ def assiduite_create(etudid: int = None):
|
|||
else:
|
||||
success[i] = obj
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({"errors": errors, "success": success})
|
||||
|
||||
|
||||
|
@ -343,7 +363,6 @@ def _create_singular(
|
|||
return (404, err)
|
||||
|
||||
# TOUT EST OK
|
||||
|
||||
try:
|
||||
nouv_assiduite: Assiduite = Assiduite.create_assiduite(
|
||||
date_debut=deb,
|
||||
|
@ -352,11 +371,12 @@ def _create_singular(
|
|||
etud=etud,
|
||||
moduleimpl=moduleimpl,
|
||||
description=desc,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
db.session.add(nouv_assiduite)
|
||||
db.session.commit()
|
||||
return (200, {"assiduite_id": nouv_assiduite.assiduite_id})
|
||||
return (200, {"assiduite_id": nouv_assiduite.id})
|
||||
except ScoValueError as excp:
|
||||
return (
|
||||
404,
|
||||
|
@ -421,6 +441,7 @@ def assiduite_edit(assiduite_id: int):
|
|||
"etat"?: str,
|
||||
"moduleimpl_id"?: int
|
||||
"desc"?: str
|
||||
"est_just"?: bool
|
||||
}
|
||||
"""
|
||||
assiduite_unique: Assiduite = Assiduite.query.filter_by(
|
||||
|
@ -525,6 +546,24 @@ def _count_manager(requested) -> tuple[str, dict]:
|
|||
# cas 6 : type
|
||||
metric = requested.args.get("metric", "all")
|
||||
|
||||
# cas 7 : est_just
|
||||
|
||||
est_just: str = requested.args.get("est_just")
|
||||
if est_just is not None:
|
||||
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||
falses: tuple[str] = ("f", "faux", "false")
|
||||
|
||||
if est_just.lower() in trues:
|
||||
filtered["est_just"] = True
|
||||
elif est_just.lower() in falses:
|
||||
filtered["est_just"] = False
|
||||
|
||||
# cas 8 : user_id
|
||||
|
||||
user_id = requested.args.get("user_id", False)
|
||||
if user_id is not False:
|
||||
filtered["user_id"] = user_id
|
||||
|
||||
return (metric, filtered)
|
||||
|
||||
|
||||
|
@ -574,4 +613,28 @@ def _filter_manager(requested, assiduites_query: Assiduite):
|
|||
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
|
||||
assiduites_query = scass.filter_by_formsemestre(assiduites_query, formsemestre)
|
||||
|
||||
# cas 6 : est_just
|
||||
|
||||
est_just: str = requested.args.get("est_just")
|
||||
if est_just is not None:
|
||||
trues: tuple[str] = ("v", "t", "vrai", "true")
|
||||
falses: tuple[str] = ("f", "faux", "false")
|
||||
|
||||
if est_just.lower() in trues:
|
||||
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query, True
|
||||
)
|
||||
elif est_just.lower() in falses:
|
||||
assiduites_query: Assiduite = scass.filter_assiduites_by_est_just(
|
||||
assiduites_query, False
|
||||
)
|
||||
|
||||
# cas 8 : user_id
|
||||
|
||||
user_id = requested.args.get("user_id", False)
|
||||
if user_id is not False:
|
||||
assiduites_query: Assiduite = scass.filter_assiduites_by_user_id(
|
||||
assiduites_query, user_id
|
||||
)
|
||||
|
||||
return assiduites_query
|
||||
|
|
|
@ -477,11 +477,8 @@ def formsemestre_resultat(formsemestre_id: int):
|
|||
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
table = res.get_table_recap(
|
||||
convert_values=convert_values,
|
||||
include_evaluations=False,
|
||||
mode_jury=False,
|
||||
allow_html=False,
|
||||
table = TableRecap(
|
||||
res, convert_values=convert_values, include_evaluations=False, mode_jury=False
|
||||
)
|
||||
# Supprime les champs inutiles (mise en forme)
|
||||
rows = table.to_list()
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
from datetime import datetime
|
||||
|
||||
from flask import g, jsonify, request
|
||||
from flask_login import login_required
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -18,23 +18,12 @@ from app.api import api_web_bp
|
|||
from app.api import get_model_api_object
|
||||
from app.decorators import permission_required, scodoc
|
||||
from app.models import Identite, Justificatif
|
||||
from app.models.assiduites import is_period_conflicting
|
||||
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
|
||||
|
||||
# @bp.route("/justificatif/remove")
|
||||
# @api_web_bp.route("/justificatif/remove")
|
||||
# @scodoc
|
||||
# def justremove():
|
||||
# """ """
|
||||
# archiver: JustificatifArchiver = JustificatifArchiver()
|
||||
|
||||
# archiver.delete_justificatif(etudid=1, archive_id="2023-02-01-10-29-20")
|
||||
# return jsonify("done")
|
||||
|
||||
# Partie Modèle
|
||||
@bp.route("/justificatif/<int:justif_id>")
|
||||
@api_web_bp.route("/justificatif/<int:justif_id>")
|
||||
|
@ -53,6 +42,7 @@ def justificatif(justif_id: int = None):
|
|||
"fichier": "archive_id",
|
||||
"raison": "une raison",
|
||||
"entry_date": "2022-10-31T08:00+01:00",
|
||||
"user_id": 1 or null,
|
||||
}
|
||||
|
||||
"""
|
||||
|
@ -203,11 +193,18 @@ def _create_singular(
|
|||
etat=etat,
|
||||
etud=etud,
|
||||
raison=raison,
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
db.session.add(nouv_justificatif)
|
||||
db.session.commit()
|
||||
return (200, {"justif_id": nouv_justificatif.id})
|
||||
return (
|
||||
200,
|
||||
{
|
||||
"justif_id": nouv_justificatif.id,
|
||||
"couverture": scass.justifies(nouv_justificatif),
|
||||
},
|
||||
)
|
||||
except ScoValueError as excp:
|
||||
return (
|
||||
404,
|
||||
|
@ -236,9 +233,10 @@ def justif_edit(justif_id: int):
|
|||
justificatif_unique: Justificatif = Justificatif.query.filter_by(
|
||||
id=justif_id
|
||||
).first_or_404()
|
||||
|
||||
errors: list[str] = []
|
||||
data = request.get_json(force=True)
|
||||
|
||||
avant_ids: list[int] = scass.justifies(justificatif_unique)
|
||||
# Vérifications de data
|
||||
|
||||
# Cas 1 : Etat
|
||||
|
@ -279,21 +277,12 @@ def justif_edit(justif_id: int):
|
|||
if justificatif_unique.date_debut <= fin:
|
||||
errors.append("param 'date_fin': date de fin située avant date de début ")
|
||||
|
||||
# Vérification du conflit d'horaire
|
||||
if (deb is not None) or (fin is not None):
|
||||
deb = deb if deb is not None else justificatif_unique.date_debut
|
||||
fin = fin if fin is not None else justificatif_unique.date_fin
|
||||
# Mise à jour des dates
|
||||
deb = deb if deb is not None else justificatif_unique.date_debut
|
||||
fin = fin if fin is not None else justificatif_unique.date_fin
|
||||
|
||||
justificatifs_list: list[Justificatif] = Justificatif.query.filter_by(
|
||||
etuid=justificatif_unique.etudid
|
||||
).all()
|
||||
|
||||
if is_period_conflicting(deb, fin, justificatifs_list, Justificatif):
|
||||
errors.append(
|
||||
"Modification de la plage horaire impossible: conflit avec les autres justificatifs"
|
||||
)
|
||||
justificatif_unique.date_debut = deb
|
||||
justificatif_unique.date_fin = fin
|
||||
justificatif_unique.date_debut = deb
|
||||
justificatif_unique.date_fin = fin
|
||||
|
||||
if errors:
|
||||
err: str = ", ".join(errors)
|
||||
|
@ -301,7 +290,14 @@ def justif_edit(justif_id: int):
|
|||
|
||||
db.session.add(justificatif_unique)
|
||||
db.session.commit()
|
||||
return jsonify({"OK": True})
|
||||
return jsonify(
|
||||
{
|
||||
"couverture": {
|
||||
"avant": avant_ids,
|
||||
"après": scass.justifies(justificatif_unique),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/justificatif/delete", methods=["POST"])
|
||||
|
|
|
@ -24,7 +24,7 @@ class Assiduite(db.Model):
|
|||
|
||||
__tablename__ = "assiduites"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
id = db.Column(db.Integer, primary_key=True, nullable=False)
|
||||
assiduite_id = db.synonym("id")
|
||||
|
||||
date_debut = db.Column(
|
||||
|
@ -50,6 +50,14 @@ class Assiduite(db.Model):
|
|||
|
||||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
)
|
||||
|
||||
est_just = db.Column(db.Boolean, server_default="false", nullable=False)
|
||||
|
||||
def to_dict(self, format_api=True) -> dict:
|
||||
"""Retourne la représentation json de l'assiduité"""
|
||||
etat = self.etat
|
||||
|
@ -57,7 +65,7 @@ class Assiduite(db.Model):
|
|||
if format_api:
|
||||
etat = EtatAssiduite.inverse().get(self.etat).name
|
||||
data = {
|
||||
"assiduite_id": self.assiduite_id,
|
||||
"assiduite_id": self.id,
|
||||
"etudid": self.etudid,
|
||||
"moduleimpl_id": self.moduleimpl_id,
|
||||
"date_debut": self.date_debut,
|
||||
|
@ -65,6 +73,8 @@ class Assiduite(db.Model):
|
|||
"etat": etat,
|
||||
"desc": self.desc,
|
||||
"entry_date": self.entry_date,
|
||||
"user_id": self.user_id,
|
||||
"est_just": self.est_just,
|
||||
}
|
||||
return data
|
||||
|
||||
|
@ -78,6 +88,8 @@ class Assiduite(db.Model):
|
|||
moduleimpl: ModuleImpl = None,
|
||||
description: str = None,
|
||||
entry_date: datetime = None,
|
||||
user_id: int = None,
|
||||
est_just: bool = False,
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
|
@ -97,6 +109,8 @@ class Assiduite(db.Model):
|
|||
moduleimpl_id=moduleimpl.id,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
user_id=user_id,
|
||||
est_just=est_just,
|
||||
)
|
||||
else:
|
||||
raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl")
|
||||
|
@ -108,6 +122,8 @@ class Assiduite(db.Model):
|
|||
etudiant=etud,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
user_id=user_id,
|
||||
est_just=est_just,
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
@ -122,6 +138,7 @@ class Assiduite(db.Model):
|
|||
moduleimpl_id: int = None,
|
||||
description: str = None,
|
||||
entry_date: datetime = None,
|
||||
est_just: bool = False,
|
||||
) -> object or int:
|
||||
"""Créer une nouvelle assiduité pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
|
@ -134,6 +151,7 @@ class Assiduite(db.Model):
|
|||
moduleimpl_id=moduleimpl_id,
|
||||
desc=description,
|
||||
entry_date=entry_date,
|
||||
est_just=est_just,
|
||||
)
|
||||
|
||||
return nouv_assiduite
|
||||
|
@ -172,6 +190,13 @@ class Justificatif(db.Model):
|
|||
|
||||
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
|
||||
user_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("user.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
|
||||
raison = db.Column(db.Text())
|
||||
|
||||
# Archive_id -> sco_archives_justificatifs.py
|
||||
|
@ -194,6 +219,7 @@ class Justificatif(db.Model):
|
|||
"raison": self.raison,
|
||||
"fichier": self.fichier,
|
||||
"entry_date": self.entry_date,
|
||||
"user_id": self.user_id,
|
||||
}
|
||||
return data
|
||||
|
||||
|
@ -206,15 +232,9 @@ class Justificatif(db.Model):
|
|||
etat: EtatJustificatif,
|
||||
raison: str = None,
|
||||
entry_date: datetime = None,
|
||||
user_id: int = None,
|
||||
) -> object or int:
|
||||
"""Créer un nouveau justificatif pour l'étudiant"""
|
||||
# Vérification de non duplication des périodes
|
||||
justificatifs: list[Justificatif] = etud.justificatifs
|
||||
if is_period_conflicting(date_debut, date_fin, justificatifs, Justificatif):
|
||||
raise ScoValueError(
|
||||
"Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)"
|
||||
)
|
||||
|
||||
nouv_justificatif = Justificatif(
|
||||
date_debut=date_debut,
|
||||
date_fin=date_fin,
|
||||
|
@ -222,8 +242,8 @@ class Justificatif(db.Model):
|
|||
etudiant=etud,
|
||||
raison=raison,
|
||||
entry_date=entry_date,
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
return nouv_justificatif
|
||||
|
||||
@classmethod
|
||||
|
@ -275,3 +295,42 @@ def is_period_conflicting(
|
|||
).count()
|
||||
|
||||
return count > 0
|
||||
|
||||
|
||||
def compute_assiduites_justified(
|
||||
justificatifs: Justificatif = Justificatif, reset: bool = False
|
||||
) -> list[int]:
|
||||
"""Calcule et modifie les champs "est_just" de chaque assiduité lié à l'étud
|
||||
retourne la liste des assiduite_id justifiées
|
||||
|
||||
Si reset alors : met à false toutes les assiduités non justifiées par les justificatifs donnés
|
||||
"""
|
||||
|
||||
list_assiduites_id: set[int] = set()
|
||||
for justi in justificatifs:
|
||||
assiduites: Assiduite = (
|
||||
Assiduite.query.join(Justificatif, Assiduite.etudid == Justificatif.etudid)
|
||||
.filter(Assiduite.etat != EtatAssiduite.PRESENT)
|
||||
.filter(
|
||||
Assiduite.date_debut <= justi.date_fin,
|
||||
Assiduite.date_fin >= justi.date_debut,
|
||||
)
|
||||
)
|
||||
|
||||
for assi in assiduites:
|
||||
assi.est_just = True
|
||||
list_assiduites_id.add(assi.id)
|
||||
db.session.add(assi)
|
||||
|
||||
if reset:
|
||||
un_justified: Assiduite = (
|
||||
Assiduite.query.filter(Assiduite.id.not_in(list_assiduites_id))
|
||||
.join(Justificatif, Assiduite.etudid == Justificatif.etudid)
|
||||
.filter(Assiduite.etat != EtatAssiduite.PRESENT)
|
||||
)
|
||||
for assi in un_justified:
|
||||
assi.est_just = False
|
||||
db.session.add(assi)
|
||||
|
||||
db.session.commit()
|
||||
return
|
||||
|
|
|
@ -1079,7 +1079,7 @@ def get_assiduites_count_in_interval(etudid, date_debut_iso, date_fin_iso):
|
|||
nb_abs: dict = calculator.to_dict()["demi"]
|
||||
|
||||
abs_just: list[Assiduite] = scass.get_all_justified(
|
||||
justificatifs, date_debut, date_fin
|
||||
etudid, date_debut, date_fin
|
||||
)
|
||||
|
||||
calculator.reset()
|
||||
|
|
|
@ -4,11 +4,73 @@ Gestion de l'archivage des justificatifs
|
|||
Ecrit par Matthias HARTMANN
|
||||
"""
|
||||
import os
|
||||
from datetime import datetime
|
||||
from shutil import rmtree
|
||||
|
||||
from app.models import Identite
|
||||
from app.scodoc.sco_archives import BaseArchiver
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_utils import is_iso_formated
|
||||
|
||||
|
||||
class Trace:
|
||||
"""gestionnaire de la trace des fichiers justificatifs"""
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
self.path: str = path + "/_trace.csv"
|
||||
self.content: dict[str, list[datetime, datetime]] = {}
|
||||
self.import_from_file()
|
||||
|
||||
def import_from_file(self):
|
||||
"""import trace from file"""
|
||||
if os.path.isfile(self.path):
|
||||
with open(self.path, "r", encoding="utf-8") as file:
|
||||
for line in file.readlines():
|
||||
csv = line.split(",")
|
||||
fname: str = csv[0]
|
||||
entry_date: datetime = is_iso_formated(csv[1], True)
|
||||
delete_date: datetime = is_iso_formated(csv[2], True)
|
||||
|
||||
self.content[fname] = [entry_date, delete_date]
|
||||
|
||||
def set_trace(self, *fnames: str, mode: str = "entry"):
|
||||
"""Ajoute une trace du fichier donné
|
||||
mode : entry / delete
|
||||
"""
|
||||
modes: list[str] = ["entry", "delete"]
|
||||
for fname in fnames:
|
||||
if fname in modes:
|
||||
continue
|
||||
traced: list[datetime, datetime] = self.content.get(fname, False)
|
||||
if not traced:
|
||||
self.content[fname] = [None, None]
|
||||
traced = self.content[fname]
|
||||
|
||||
traced[modes.index(mode)] = datetime.now()
|
||||
self.save_trace()
|
||||
|
||||
def save_trace(self):
|
||||
"""Enregistre la trace dans le fichier _trace.csv"""
|
||||
lines: list[str] = []
|
||||
for fname, traced in self.content.items():
|
||||
date_fin: datetime or None = traced[1].isoformat() if traced[1] else "None"
|
||||
|
||||
lines.append(f"{fname},{traced[0].isoformat()},{date_fin}")
|
||||
with open(self.path, "w", encoding="utf-8") as file:
|
||||
file.write("\n".join(lines))
|
||||
|
||||
def get_trace(self, fnames: list[str] = ()) -> dict[str, list[datetime, datetime]]:
|
||||
"""Récupère la trace pour les noms de fichiers.
|
||||
si aucun nom n'est donné, récupère tous les fichiers"""
|
||||
|
||||
if fnames is None or len(fnames) == 0:
|
||||
return self.content
|
||||
|
||||
traced: dict = {}
|
||||
for fname in fnames:
|
||||
traced[fname] = self.content.get(fname, None)
|
||||
|
||||
return traced
|
||||
|
||||
|
||||
class JustificatifArchiver(BaseArchiver):
|
||||
|
@ -21,6 +83,7 @@ class JustificatifArchiver(BaseArchiver):
|
|||
justificatif
|
||||
└── <dept_id>
|
||||
└── <etudid/oid>
|
||||
├── [_trace.csv]
|
||||
└── <archive_id>
|
||||
├── [_description.txt]
|
||||
└── [<filename.ext>]
|
||||
|
@ -52,11 +115,22 @@ class JustificatifArchiver(BaseArchiver):
|
|||
|
||||
fname: str = self.store(archive_id, filename, data)
|
||||
|
||||
trace = Trace(self.get_obj_dir(etudid))
|
||||
trace.set_trace(fname, "entry")
|
||||
|
||||
return self.get_archive_name(archive_id), fname
|
||||
|
||||
def delete_justificatif(self, etudid: int, archive_name: str, filename: str = None):
|
||||
def delete_justificatif(
|
||||
self,
|
||||
etudid: int,
|
||||
archive_name: str,
|
||||
filename: str = None,
|
||||
has_trace: bool = True,
|
||||
):
|
||||
"""
|
||||
Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné
|
||||
|
||||
Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) dans la trace de l'étudiant
|
||||
"""
|
||||
self._set_dept(etudid)
|
||||
if str(etudid) not in self.list_oids():
|
||||
|
@ -73,9 +147,16 @@ class JustificatifArchiver(BaseArchiver):
|
|||
path: str = os.path.join(self.get_obj_dir(etudid), archive_id, filename)
|
||||
|
||||
if os.path.isfile(path):
|
||||
if has_trace:
|
||||
trace = Trace(self.get_obj_dir(etudid))
|
||||
trace.set_trace(filename, "delete")
|
||||
os.remove(path)
|
||||
|
||||
else:
|
||||
if has_trace:
|
||||
trace = Trace(self.get_obj_dir(etudid))
|
||||
trace.set_trace(*self.list_archive(archive_id), mode="delete")
|
||||
|
||||
self.delete_archive(
|
||||
os.path.join(
|
||||
self.get_obj_dir(etudid),
|
||||
|
@ -116,6 +197,7 @@ class JustificatifArchiver(BaseArchiver):
|
|||
def remove_dept_archive(self, dept_id: int = None):
|
||||
"""
|
||||
Supprime toutes les archives d'un département (ou de tous les départements)
|
||||
⚠ Supprime aussi les fichiers de trace ⚠
|
||||
"""
|
||||
self.set_dept_id(1)
|
||||
self.initialize()
|
||||
|
@ -124,3 +206,10 @@ class JustificatifArchiver(BaseArchiver):
|
|||
rmtree(self.root, ignore_errors=True)
|
||||
else:
|
||||
rmtree(os.path.join(self.root, str(dept_id)), ignore_errors=True)
|
||||
|
||||
def get_trace(
|
||||
self, etudid: int, *fnames: str
|
||||
) -> dict[str, list[datetime, datetime]]:
|
||||
"""Récupère la trace des justificatifs de l'étudiant"""
|
||||
trace = Trace(self.get_obj_dir(etudid))
|
||||
return trace.get_trace(fnames)
|
||||
|
|
|
@ -193,6 +193,10 @@ def get_assiduites_stats(
|
|||
assiduites = filter_by_module_impl(assiduites, filtered[key])
|
||||
elif key == "formsemestre":
|
||||
assiduites = filter_by_formsemestre(assiduites, filtered[key])
|
||||
elif key == "est_just":
|
||||
assiduites = filter_assiduites_by_est_just(assiduites, filtered[key])
|
||||
elif key == "user_id":
|
||||
assiduites = filter_assiduites_by_user_id(assiduites, filtered[key])
|
||||
if (deb, fin) != (None, None):
|
||||
assiduites = filter_by_date(assiduites, Assiduite, deb, fin)
|
||||
|
||||
|
@ -219,6 +223,22 @@ def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
|
|||
return assiduites.filter(Assiduite.etat.in_(etats))
|
||||
|
||||
|
||||
def filter_assiduites_by_est_just(
|
||||
assiduites: Assiduite, est_just: bool
|
||||
) -> Justificatif:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction de s'ils sont justifiés
|
||||
"""
|
||||
return assiduites.filter_by(est_just=est_just)
|
||||
|
||||
|
||||
def filter_assiduites_by_user_id(assiduites: Assiduite, user_id: int) -> Justificatif:
|
||||
"""
|
||||
Filtrage d'une collection d'assiduites en fonction de l'user_id
|
||||
"""
|
||||
return assiduites.filter_by(user_id=user_id)
|
||||
|
||||
|
||||
def filter_by_date(
|
||||
collection: Assiduite or Justificatif,
|
||||
collection_cls: Assiduite or Justificatif,
|
||||
|
@ -291,8 +311,8 @@ def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemest
|
|||
def justifies(justi: Justificatif, obj: bool = False) -> list[int]:
|
||||
"""
|
||||
Retourne la liste des assiduite_id qui sont justifié par la justification
|
||||
Une assiduité est justifiée si elle est STRICTEMENT comprise dans la plage du justificatif
|
||||
et que l'état du justificatif est "validé"
|
||||
Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT comprise dans la plage du justificatif
|
||||
et que l'état du justificatif est "valide"
|
||||
renvoie des id si obj == False, sinon les Assiduités
|
||||
"""
|
||||
|
||||
|
@ -303,10 +323,8 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int]:
|
|||
Assiduite.query.join(Justificatif, Assiduite.etudid == Justificatif.etudid)
|
||||
.filter(Assiduite.etat != scu.EtatAssiduite.PRESENT)
|
||||
.filter(
|
||||
Assiduite.date_debut >= justi.date_debut,
|
||||
Assiduite.date_debut <= justi.date_fin,
|
||||
Assiduite.date_fin >= justi.date_debut,
|
||||
Assiduite.date_fin <= justi.date_fin,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -317,12 +335,10 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int]:
|
|||
|
||||
|
||||
def get_all_justified(
|
||||
justificatifs: Justificatif, date_deb: datetime = None, date_fin: datetime = None
|
||||
etudid: int, date_deb: datetime = None, date_fin: datetime = None
|
||||
) -> list[Assiduite]:
|
||||
"""Retourne toutes les assiduités justifiées par les justificatifs donnés"""
|
||||
"""Retourne toutes les assiduités justifiées sur une période"""
|
||||
|
||||
# TODO: Forcer le filtrage des assiduités en fonction d'une période
|
||||
# => Cas d'un justificatif en bordure de période
|
||||
if date_deb is None:
|
||||
date_deb = datetime.min
|
||||
if date_fin is None:
|
||||
|
@ -330,10 +346,11 @@ def get_all_justified(
|
|||
|
||||
date_deb = scu.localize_datetime(date_deb)
|
||||
date_fin = scu.localize_datetime(date_fin)
|
||||
|
||||
assiduites: list[Assiduite] = []
|
||||
|
||||
for justi in justificatifs:
|
||||
assis: list[Assiduite] = justifies(justi, obj=True)
|
||||
assiduites.extend(assis)
|
||||
return list(assiduites)
|
||||
justified = Assiduite.query.filter_by(est_just=True, etudid=etudid)
|
||||
after = filter_by_date(
|
||||
justified,
|
||||
Assiduite,
|
||||
date_deb,
|
||||
date_fin,
|
||||
)
|
||||
return after
|
||||
|
|
|
@ -222,7 +222,10 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
|
|||
|
||||
new_date: datetime.datetime = date
|
||||
if new_date.tzinfo is None:
|
||||
new_date = timezone("Europe/Paris").localize(date)
|
||||
try:
|
||||
new_date = timezone("Europe/Paris").localize(date)
|
||||
except OverflowError:
|
||||
new_date = timezone("UTC").localize(date)
|
||||
return new_date
|
||||
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
<span class="formation_module_ue">(<a title="UE de rattachement">{{mod.ue.acronyme}}</a>)</span>,
|
||||
{% endif %}
|
||||
|
||||
- parcours <b>{{ mod.get_cursus()|map(attribute="code")|join("</b>, <b>")|default('tronc commun',
|
||||
- parcours <b>{{ mod.get_parcours()|map(attribute="code")|join("</b>, <b>")|default('tronc commun',
|
||||
true)|safe
|
||||
}}</b>
|
||||
{% if mod.heures_cours or mod.heures_td or mod.heures_tp %}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
"""assiduites ajout user_id,est_just
|
||||
|
||||
Revision ID: b555390780b2
|
||||
Revises: dbcf2175e87f
|
||||
Create Date: 2023-02-22 18:44:22.643275
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "b555390780b2"
|
||||
down_revision = "dbcf2175e87f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column(
|
||||
"assiduites",
|
||||
sa.Column(
|
||||
"user_id",
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
),
|
||||
)
|
||||
op.add_column(
|
||||
"assiduites",
|
||||
sa.Column("est_just", sa.Boolean(), server_default="false", nullable=False),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_assiduites_user_id"), "assiduites", ["user_id"], unique=False
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_assiduites_user_id",
|
||||
"assiduites",
|
||||
"user",
|
||||
["user_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
op.add_column(
|
||||
"justificatifs",
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_justificatifs_user_id"), "justificatifs", ["user_id"], unique=False
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_justificatifs_user_id",
|
||||
"justificatifs",
|
||||
"user",
|
||||
["user_id"],
|
||||
["id"],
|
||||
ondelete="SET NULL",
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_constraint("fk_justificatifs_user_id", "justificatifs", type_="foreignkey")
|
||||
op.drop_index(op.f("ix_justificatifs_user_id"), table_name="justificatifs")
|
||||
op.drop_column("justificatifs", "user_id")
|
||||
op.drop_constraint("fk_assiduites_user_id", "assiduites", type_="foreignkey")
|
||||
op.drop_index(op.f("ix_assiduites_user_id"), table_name="assiduites")
|
||||
op.drop_column("assiduites", "est_just")
|
||||
op.drop_column("assiduites", "user_id")
|
||||
# ### end Alembic commands ###
|
|
@ -24,6 +24,8 @@ ASSIDUITES_FIELDS = {
|
|||
"etat": str,
|
||||
"desc": str,
|
||||
"entry_date": str,
|
||||
"user_id": str,
|
||||
"est_just": bool,
|
||||
}
|
||||
|
||||
CREATE_FIELD = {"assiduite_id": int}
|
||||
|
@ -47,7 +49,7 @@ def check_fields(data: dict, fields: dict = None):
|
|||
fields = ASSIDUITES_FIELDS
|
||||
assert set(data.keys()) == set(fields.keys())
|
||||
for key in data:
|
||||
if key in ("moduleimpl_id", "desc"):
|
||||
if key in ("moduleimpl_id", "desc", "user_id"):
|
||||
assert isinstance(data[key], fields[key]) or data[key] is None
|
||||
else:
|
||||
assert isinstance(data[key], fields[key])
|
||||
|
|
|
@ -30,9 +30,10 @@ JUSTIFICATIFS_FIELDS = {
|
|||
"raison": str,
|
||||
"entry_date": str,
|
||||
"fichier": str,
|
||||
"user_id": int,
|
||||
}
|
||||
|
||||
CREATE_FIELD = {"justif_id": int}
|
||||
CREATE_FIELD = {"justif_id": int, "couverture": list}
|
||||
BATCH_FIELD = {"errors": dict, "success": dict}
|
||||
|
||||
TO_REMOVE = []
|
||||
|
@ -51,7 +52,7 @@ def check_fields(data, fields: dict = None):
|
|||
fields = JUSTIFICATIFS_FIELDS
|
||||
assert set(data.keys()) == set(fields.keys())
|
||||
for key in data:
|
||||
if key in ("raison", "fichier"):
|
||||
if key in ("raison", "fichier", "user_id"):
|
||||
assert isinstance(data[key], fields[key]) or data[key] is None
|
||||
else:
|
||||
assert isinstance(data[key], fields[key])
|
||||
|
@ -182,14 +183,6 @@ def test_route_create(api_headers):
|
|||
# Mauvais fonctionnement
|
||||
check_failure_post(f"/justificatif/{FAUX}/create", api_headers, [data])
|
||||
|
||||
res = POST_JSON(f"/justificatif/{ETUDID}/create", [data], api_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["errors"]) == 1
|
||||
assert (
|
||||
res["errors"]["0"]
|
||||
== "Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)"
|
||||
)
|
||||
|
||||
res = POST_JSON(
|
||||
f"/justificatif/{ETUDID}/create",
|
||||
[create_data("absent", "03")],
|
||||
|
@ -218,7 +211,6 @@ def test_route_create(api_headers):
|
|||
# Mauvais Fonctionnement
|
||||
|
||||
data2 = [
|
||||
create_data("modifie", "01"),
|
||||
create_data(None, "25"),
|
||||
create_data("blabla", 26),
|
||||
create_data("valide", 32),
|
||||
|
@ -226,16 +218,12 @@ def test_route_create(api_headers):
|
|||
|
||||
res = POST_JSON(f"/justificatif/{ETUDID}/create", data2, api_headers)
|
||||
check_fields(res, BATCH_FIELD)
|
||||
assert len(res["errors"]) == 4
|
||||
assert len(res["errors"]) == 3
|
||||
|
||||
assert res["errors"]["0"] == "param 'etat': manquant"
|
||||
assert res["errors"]["1"] == "param 'etat': invalide"
|
||||
assert (
|
||||
res["errors"]["0"]
|
||||
== "Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)"
|
||||
)
|
||||
assert res["errors"]["1"] == "param 'etat': manquant"
|
||||
assert res["errors"]["2"] == "param 'etat': invalide"
|
||||
assert (
|
||||
res["errors"]["3"]
|
||||
res["errors"]["2"]
|
||||
== "param 'date_debut': format invalide, param 'date_fin': format invalide"
|
||||
)
|
||||
|
||||
|
@ -246,11 +234,11 @@ def test_route_edit(api_headers):
|
|||
|
||||
data = {"etat": "modifie", "raison": "test"}
|
||||
res = POST_JSON(f"/justificatif/{TO_REMOVE[0]}/edit", data, api_headers)
|
||||
assert res == {"OK": True}
|
||||
assert isinstance(res, dict) and "couverture" in res.keys()
|
||||
|
||||
data["raison"] = None
|
||||
res = POST_JSON(f"/justificatif/{TO_REMOVE[1]}/edit", data, api_headers)
|
||||
assert res == {"OK": True}
|
||||
assert isinstance(res, dict) and "couverture" in res.keys()
|
||||
|
||||
# Mauvais fonctionnement
|
||||
|
||||
|
|
|
@ -294,20 +294,6 @@ def ajouter_justificatifs(etud):
|
|||
"fin": "2023-01-03T12:00+01:00",
|
||||
"raison": "Description",
|
||||
}
|
||||
|
||||
try:
|
||||
Justificatif.create_justificatif(
|
||||
etud,
|
||||
scu.is_iso_formated(test_assiduite["deb"], True),
|
||||
scu.is_iso_formated(test_assiduite["fin"], True),
|
||||
test_assiduite["etat"],
|
||||
test_assiduite["raison"],
|
||||
)
|
||||
except ScoValueError as excp:
|
||||
assert (
|
||||
excp.args[0]
|
||||
== "Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)"
|
||||
)
|
||||
return justificatifs
|
||||
|
||||
|
||||
|
|
|
@ -40,6 +40,12 @@ def downgrade_module(
|
|||
_remove_justificatifs(dept_etudid)
|
||||
_remove_justificatifs_archive(dept_id)
|
||||
|
||||
if dept is None:
|
||||
if assiduites:
|
||||
db.session.execute("ALTER SEQUENCE assiduites_id_seq RESTART WITH 1")
|
||||
if justificatifs:
|
||||
db.session.execute("ALTER SEQUENCE justificatifs_id_seq RESTART WITH 1")
|
||||
|
||||
db.session.commit()
|
||||
|
||||
print(
|
||||
|
|
|
@ -7,6 +7,8 @@ from datetime import date, datetime, time, timedelta
|
|||
from json import dump, dumps
|
||||
from sqlalchemy import not_
|
||||
|
||||
from flask import g
|
||||
|
||||
from app import db
|
||||
from app.models import (
|
||||
Absence,
|
||||
|
@ -16,6 +18,7 @@ from app.models import (
|
|||
Justificatif,
|
||||
ModuleImplInscription,
|
||||
)
|
||||
from app.models.assiduites import compute_assiduites_justified
|
||||
from app.profiler import Profiler
|
||||
from app.scodoc.sco_utils import (
|
||||
EtatAssiduite,
|
||||
|
@ -54,6 +57,7 @@ class _Merger:
|
|||
self.etudid = abs_.etudid
|
||||
self.est_abs = est_abs
|
||||
self.raison = abs_.description
|
||||
self.entry_date = abs_.entry_date
|
||||
|
||||
def merge(self, abs_: Absence) -> bool:
|
||||
"""Fusionne les absences"""
|
||||
|
@ -97,6 +101,7 @@ class _Merger:
|
|||
date_fin=date_fin,
|
||||
etat=EtatJustificatif.VALIDE,
|
||||
raison=self.raison,
|
||||
entry_date=self.entry_date,
|
||||
)
|
||||
return retour
|
||||
|
||||
|
@ -111,6 +116,7 @@ class _Merger:
|
|||
etat=EtatAssiduite.ABSENT,
|
||||
moduleimpl_id=self.moduleimpl,
|
||||
description=self.raison,
|
||||
entry_date=self.entry_date,
|
||||
)
|
||||
return retour
|
||||
|
||||
|
@ -291,6 +297,13 @@ def migrate_abs_to_assiduites(
|
|||
|
||||
db.session.commit()
|
||||
|
||||
justifs: Justificatif = Justificatif.query
|
||||
|
||||
if dept is not None:
|
||||
justifs.filter(Justificatif.etudid.in_(etuds_id))
|
||||
|
||||
compute_assiduites_justified(justifs)
|
||||
|
||||
print_progress_bar(
|
||||
absences_len,
|
||||
absences_len,
|
||||
|
|
Loading…
Reference in New Issue
Block a user