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:
iziram 2023-02-22 22:33:13 +01:00
parent 4c648212dd
commit b73a02ac67
15 changed files with 393 additions and 103 deletions

View File

@ -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

View File

@ -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()

View File

@ -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"])

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 %}

View File

@ -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 ###

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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,