1
0
forked from ScoDoc/ScoDoc

Merge branch 'modif' of https://scodoc.org/git/iziram/ScoDoc into iziram-rev

This commit is contained in:
Emmanuel Viennet 2023-02-03 15:45:24 +01:00
commit 2fc978e515
21 changed files with 677 additions and 285 deletions

View File

@ -40,14 +40,7 @@ def assiduite(assiduite_id: int = None):
}
"""
query = Assiduite.query.filter_by(id=assiduite_id)
# if g.scodoc_dept:
# query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
assiduite_query = query.first_or_404()
data = assiduite_query.to_dict()
return jsonify(_change_etat(data))
return scu.get_model_api_object(Assiduite, assiduite_id)
@bp.route("/assiduites/<int:etudid>/count", defaults={"with_query": False})
@ -164,8 +157,8 @@ def assiduites(etudid: int = None, with_query: bool = False):
data_set: list[dict] = []
for ass in assiduites_query.all():
data = ass.to_dict()
data_set.append(_change_etat(data))
data = ass.to_dict(format_api=True)
data_set.append(data)
return jsonify(data_set)
@ -202,8 +195,8 @@ def assiduites_formsemestre(formsemestre_id: int, with_query: bool = False):
data_set: list[dict] = []
for ass in assiduites_query.all():
data = ass.to_dict()
data_set.append(_change_etat(data))
data = ass.to_dict(format_api=True)
data_set.append(data)
return jsonify(data_set)
@ -307,11 +300,10 @@ def _create_singular(
etat = data.get("etat", None)
if etat is None:
errors.append("param 'etat': manquant")
elif etat not in scu.ETATS_ASSIDUITE:
elif not scu.EtatAssiduite.contains(etat):
errors.append("param 'etat': invalide")
data = _change_etat(data, False)
etat = data.get("etat", None)
etat = scu.EtatAssiduite.get(etat)
# cas 2 : date_debut
date_debut = data.get("date_debut", None)
@ -418,7 +410,7 @@ def _delete_singular(assiduite_id: int, database):
@scodoc
@permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange)
def assiduite_cedit(assiduite_id: int):
def assiduite_edit(assiduite_id: int):
"""
Edition d'une assiduité à partir de son id
La requête doit avoir un content type "application/json":
@ -438,11 +430,11 @@ def assiduite_cedit(assiduite_id: int):
# Cas 1 : Etat
if data.get("etat") is not None:
data = _change_etat(data, False)
if data.get("etat") is None:
etat = scu.EtatAssiduite.get(data.get("etat"))
if etat is None:
errors.append("param 'etat': invalide")
else:
assiduite_unique.etat = data.get("etat")
assiduite_unique.etat = etat
# Cas 2 : Moduleimpl_id
moduleimpl_id = data.get("moduleimpl_id", False)
@ -478,13 +470,6 @@ def assiduite_cedit(assiduite_id: int):
# -- Utils --
def _change_etat(data: dict, from_int: bool = True):
"""change dans un json la valeur du champs état"""
if from_int:
data["etat"] = scu.ETAT_ASSIDUITE_NAME.get(data["etat"])
else:
data["etat"] = scu.ETATS_ASSIDUITE.get(data["etat"])
return data
def _count_manager(requested) -> tuple[str, dict]:

View File

@ -5,22 +5,22 @@
##############################################################################
"""ScoDoc 9 API : Assiduités
"""
import os
from datetime import datetime
from flask import g, jsonify, request
from flask_login import login_required
import app.scodoc.sco_assiduites as scass
import app.scodoc.sco_utils as scu
from app import db
from app.api import api_bp as bp
from app.api import api_web_bp
from app.scodoc.sco_exceptions import ScoValueError
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 flask import g, jsonify, request
from flask_login import login_required
from app.scodoc.sco_utils import json_error
@ -35,9 +35,8 @@ from app.scodoc.sco_utils import json_error
# return jsonify("done")
# Partie Modèle
# TODO: justificatif
@bp.route("/justificatif/<int:justif_id>")
@api_web_bp.route("/assiduite/<int:justif_id>")
@api_web_bp.route("/justificatif/<int:justif_id>")
@scodoc
@permission_required(Permission.ScoView)
def justificatif(justif_id: int = None):
@ -54,19 +53,12 @@ def justificatif(justif_id: int = None):
"raison": "une raison",
"entry_date": "2022-10-31T08:00+01:00",
}
"""
query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique = query.first_or_404()
data = justificatif_unique.to_dict()
return jsonify(_change_etat(data))
return scu.get_model_api_object(Justificatif, justif_id)
# TODO: justificatifs[-query]
@bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
@bp.route("/justificatifs/<int:etudid>/query", defaults={"with_query": True})
@api_web_bp.route("/justificatifs/<int:etudid>", defaults={"with_query": False})
@ -110,13 +102,12 @@ def justificatifs(etudid: int = None, with_query: bool = False):
data_set: list[dict] = []
for just in justificatifs_query.all():
data = just.to_dict()
data_set.append(_change_etat(data))
data = just.to_dict(format_api=True)
data_set.append(data)
return jsonify(data_set)
# TODO: justificatif-create
@bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
@api_web_bp.route("/justificatif/<int:etudid>/create", methods=["POST"])
@scodoc
@ -173,11 +164,10 @@ def _create_singular(
etat = data.get("etat", None)
if etat is None:
errors.append("param 'etat': manquant")
elif etat not in scu.ETATS_JUSTIFICATIF:
elif not scu.EtatJustificatif.contains(etat):
errors.append("param 'etat': invalide")
data = _change_etat(data, False)
etat = data.get("etat", None)
etat = scu.EtatJustificatif.get(etat)
# cas 2 : date_debut
date_debut = data.get("date_debut", None)
@ -224,7 +214,6 @@ def _create_singular(
)
# TODO: justificatif-edit
@bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
@api_web_bp.route("/justificatif/<int:justif_id>/edit", methods=["POST"])
@login_required
@ -235,9 +224,12 @@ def justif_edit(justif_id: int):
"""
Edition d'un justificatif à partir de son id
La requête doit avoir un content type "application/json":
{
"etat"?: str,
"raison"?: str
"date_debut"?: str
"date_fin"?: str
}
"""
justificatif_unique: Justificatif = Justificatif.query.filter_by(
@ -250,17 +242,58 @@ def justif_edit(justif_id: int):
# Cas 1 : Etat
if data.get("etat") is not None:
data = _change_etat(data, False)
if data.get("etat") is None:
etat = scu.EtatJustificatif.get(data.get("etat"))
if etat is None:
errors.append("param 'etat': invalide")
else:
justificatif_unique.etat = data.get("etat")
justificatif_unique.etat = etat
# Cas 2 : raison
raison = data.get("raison", False)
if raison is not False:
justificatif_unique.raison = raison
deb, fin = None, None
# cas 3 : date_debut
date_debut = data.get("date_debut", False)
if date_debut is not False:
if date_debut is None:
errors.append("param 'date_debut': manquant")
deb = scu.is_iso_formated(date_debut, convert=True)
if deb is None:
errors.append("param 'date_debut': format invalide")
if justificatif_unique.date_fin >= deb:
errors.append("param 'date_debut': date de début située après date de fin ")
# cas 4 : date_fin
date_fin = data.get("date_fin", False)
if date_fin is not False:
if date_fin is None:
errors.append("param 'date_fin': manquant")
fin = scu.is_iso_formated(date_fin, convert=True)
if fin is None:
errors.append("param 'date_fin': format invalide")
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
justificatifs_list: list[Justificatif] = Justificatif.query.filter_by(
etuid=justificatif_unique.etudid
).all()
if is_period_conflicting(deb, fin, justificatifs_list):
errors.append(
"Modification de la plage horaire impossible: conflit avec les autres justificatifs"
)
justificatif_unique.date_debut = deb
justificatif_unique.date_fin = fin
if errors:
err: str = ", ".join(errors)
return json_error(404, err)
@ -270,7 +303,6 @@ def justif_edit(justif_id: int):
return jsonify({"OK": True})
# TODO: justificatif-delete
@bp.route("/justificatif/delete", methods=["POST"])
@api_web_bp.route("/justificatif/delete", methods=["POST"])
@login_required
@ -312,12 +344,18 @@ def _delete_singular(justif_id: int, database):
).first()
if justificatif_unique is None:
return (404, "Justificatif non existant")
archive_name: str = justificatif_unique.fichier
if archive_name is not None:
archiver: JustificatifArchiver = JustificatifArchiver()
archiver.delete_justificatif(justificatif_unique.etudid, archive_name)
database.session.delete(justificatif_unique)
return (200, "OK")
# Partie archivage
# TODO: justificatif-import
@bp.route("/justificatif/import/<int:justif_id>", methods=["POST"])
@api_web_bp.route("/justificatif/import/<int:justif_id>", methods=["POST"])
@scodoc
@ -359,12 +397,11 @@ def justif_import(justif_id: int = None):
return jsonify({"response": "imported"})
except ScoValueError as err:
return json_error(404, err.args[1])
return json_error(404, err.args[0])
# TODO: justificatif-export
@bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["GET"])
@api_web_bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["GET"])
@bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["POST"])
@api_web_bp.route("/justificatif/export/<int:justif_id>/<filename>", methods=["POST"])
@scodoc
@login_required
@permission_required(Permission.ScoView)
@ -391,10 +428,9 @@ def justif_export(justif_id: int = None, filename: str = None):
archive_name, justificatif_unique.etudid, filename
)
except ScoValueError as err:
return json_error(404, err.args[1])
return json_error(404, err.args[0])
# TODO: justificatif-remove
@bp.route("/justificatif/remove/<int:justif_id>", methods=["POST"])
@api_web_bp.route("/justificatif/remove/<int:justif_id>", methods=["POST"])
@scodoc
@ -404,7 +440,7 @@ def justif_export(justif_id: int = None, filename: str = None):
def justif_remove(justif_id: int = None):
"""
Supression d'un fichier ou d'une archive
# TOTALK: Doc, expliquer les noms coté server
{
"remove": <"all"/"list">
@ -454,12 +490,11 @@ def justif_remove(justif_id: int = None):
db.session.commit()
except ScoValueError as err:
return json_error(404, err.args[1])
return json_error(404, err.args[0])
return jsonify({"response": "removed"})
# TODO: justificatif-list
@bp.route("/justificatif/list/<int:justif_id>", methods=["GET"])
@api_web_bp.route("/justificatif/list/<int:justif_id>", methods=["GET"])
@scodoc
@ -492,16 +527,29 @@ def justif_list(justif_id: int = None):
# Partie justification
# TODO: justificatif-justified
@bp.route("/justificatif/justified/<int:justif_id>", methods=["GET"])
@api_web_bp.route("/justificatif/justified/<int:justif_id>", methods=["GET"])
@scodoc
@login_required
@permission_required(Permission.ScoView)
# @permission_required(Permission.ScoAssiduiteChange)
def justif_justified(justif_id: int = None):
"""
Liste assiduite_id justifiées par le justificatif
"""
query = Justificatif.query.filter_by(id=justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404()
assiduites_list: list[int] = scass.justifies(justificatif_unique)
return jsonify(assiduites_list)
# -- Utils --
def _change_etat(data: dict, from_int: bool = True):
"""change dans un json la valeur du champs état"""
if from_int:
data["etat"] = scu.ETAT_JUSTIFICATIF_NAME.get(data["etat"])
else:
data["etat"] = scu.ETATS_JUSTIFICATIF.get(data["etat"])
return data
def _filter_manager(requested, justificatifs_query):

View File

@ -33,10 +33,7 @@ import pandas as pd
from app import db
from app import models
from app.models import (
DispenseUE,
FormSemestre,
FormSemestreInscription,
Identite,
Module,
ModuleImpl,
ModuleUECoef,
@ -218,31 +215,6 @@ def notes_sem_load_cube(formsemestre: FormSemestre) -> tuple:
)
def load_dispense_ues(
formsemestre: FormSemestre, etudids: pd.Index, ues: list[UniteEns]
) -> set[tuple[int, int]]:
"""Construit l'ensemble des
etudids = modimpl_inscr_df.index, # les etudids
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
Résultat: set de (etudid, ue_id).
"""
dispense_ues = set()
ue_sem_by_code = {ue.ue_code: ue for ue in ues}
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
# puis filtre sur inscrits et code d'UE UE
for dispense_ue in DispenseUE.query.join(
Identite, FormSemestreInscription
).filter_by(formsemestre_id=formsemestre.id):
if dispense_ue.etudid in etudids:
# UE dans le semestre avec même code ?
ue = ue_sem_by_code.get(dispense_ue.ue.ue_code)
if ue is not None:
dispense_ues.add((dispense_ue.etudid, ue.id))
return dispense_ues
def compute_ue_moys_apc(
sem_cube: np.array,
etuds: list,

View File

@ -72,7 +72,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
modimpl.module.ue.type != UE_SPORT
for modimpl in self.formsemestre.modimpls_sorted
]
self.dispense_ues = moy_ue.load_dispense_ues(
self.dispense_ues = DispenseUE.load_formsemestre_dispense_ues_set(
self.formsemestre, self.modimpl_inscr_df.index, self.ues
)
self.etud_moy_ue = moy_ue.compute_ue_moys_apc(

View File

@ -51,14 +51,18 @@ class Assiduite(db.Model):
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
def to_dict(self) -> dict:
def to_dict(self, format_api=True) -> dict:
etat = self.etat
if format_api:
etat = EtatJustificatif.inverse().get(self.etat).name
data = {
"assiduite_id": self.assiduite_id,
"etudid": self.etudid,
"moduleimpl_id": self.moduleimpl_id,
"date_debut": self.date_debut,
"date_fin": self.date_fin,
"etat": self.etat,
"etat": etat,
"desc": self.desc,
"entry_date": self.entry_date,
}
@ -78,17 +82,8 @@ class Assiduite(db.Model):
# Vérification de non duplication des périodes
assiduites: list[Assiduite] = etud.assiduites.all()
date_debut = localize_datetime(date_debut)
date_fin = localize_datetime(date_fin)
assiduites = [
ass
for ass in assiduites
if is_period_overlapping(
(date_debut, date_fin),
(ass.date_debut, ass.date_fin),
)
]
if len(assiduites) != 0:
assiduites: list[Justificatif] = etud.assiduites.all()
if is_period_conflicting(date_debut, date_fin, assiduites):
raise ScoValueError(
"Duplication des assiduités (la période rentrée rentre en conflit avec une assiduité enregistrée)"
)
@ -156,13 +151,20 @@ class Justificatif(db.Model):
# Archive_id -> sco_archives_justificatifs.py
fichier = db.Column(db.Text())
def to_dict(self) -> dict:
def to_dict(self, format_api: bool = False) -> dict:
"""transformation de l'objet en dictionnaire sérialisable"""
etat = self.etat
if format_api:
etat = EtatJustificatif.inverse().get(self.etat).name
data = {
"justif_id": self.justif_id,
"etudid": self.etudid,
"date_debut": self.date_debut,
"date_fin": self.date_fin,
"etat": self.etat,
"etat": etat,
"raison": self.raison,
"fichier": self.fichier,
"entry_date": self.entry_date,
@ -181,23 +183,12 @@ class Justificatif(db.Model):
"""Créer un nouveau justificatif pour l'étudiant"""
# Vérification de non duplication des périodes
justificatifs: list[Justificatif] = etud.justificatifs.all()
date_debut = localize_datetime(date_debut)
date_fin = localize_datetime(date_fin)
justificatifs = [
just
for just in justificatifs
if is_period_overlapping(
(date_debut, date_fin),
(just.date_debut, just.date_fin),
)
]
if len(justificatifs) != 0:
if is_period_conflicting(date_debut, date_fin, justificatifs):
raise ScoValueError(
"Duplication des justificatifs (la période rentrée rentre en conflit avec un justificatif enregistré)"
)
nouv_assiduite = Justificatif(
nouv_justificatif = Justificatif(
date_debut=date_debut,
date_fin=date_fin,
etat=etat,
@ -205,4 +196,28 @@ class Justificatif(db.Model):
raison=raison,
)
return nouv_assiduite
return nouv_justificatif
def is_period_conflicting(
date_debut: datetime,
date_fin: datetime,
collection: list[Assiduite or Justificatif],
) -> bool:
"""
Vérifie si une date n'entre pas en collision
avec les justificatifs ou assiduites déjà présentes
"""
date_debut = localize_datetime(date_debut)
date_fin = localize_datetime(date_fin)
unified = [
uni
for uni in collection
if is_period_overlapping(
(date_debut, date_fin),
(uni.date_debut, uni.date_fin),
)
]
return len(unified) != 0

View File

@ -256,12 +256,23 @@ class UniteEns(db.Model):
class DispenseUE(db.Model):
"""Dispense d'UE
Utilisé en PCC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
Utilisé en APC (BUT) pour indiquer les étudiants redoublants avec une UE capitalisée
qu'ils ne refont pas.
La dispense d'UE n'est PAS une validation:
- elle n'est pas affectée par les décisions de jury (pas effacée)
- elle est associée à un formsemestre
- elle ne permet pas la délivrance d'ECTS ou du diplôme.
On utilise cette dispense et non une "inscription" par souci d'efficacité:
en général, la grande majorité des étudiants suivront toutes les UEs de leur parcours,
la dispense étant une exception.
"""
__table_args__ = (db.UniqueConstraint("ue_id", "etudid"),)
__table_args__ = (db.UniqueConstraint("formsemestre_id", "ue_id", "etudid"),)
id = db.Column(db.Integer, primary_key=True)
formsemestre_id = formsemestre_id = db.Column(
db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, nullable=True
)
ue_id = db.Column(
db.Integer,
db.ForeignKey(UniteEns.id, ondelete="CASCADE"),
@ -280,3 +291,25 @@ class DispenseUE(db.Model):
def __repr__(self) -> str:
return f"""<{self.__class__.__name__} {self.id} etud={
repr(self.etud)} ue={repr(self.ue)}>"""
@classmethod
def load_formsemestre_dispense_ues_set(
cls, formsemestre: "FormSemestre", etudids: pd.Index, ues: list[UniteEns]
) -> set[tuple[int, int]]:
"""Construit l'ensemble des
etudids = modimpl_inscr_df.index, # les etudids
ue_ids : modimpl_coefs_df.index, # les UE du formsemestre sans les UE bonus sport
Résultat: set de (etudid, ue_id).
"""
# Prend toutes les dispenses obtenues par des étudiants de ce formsemestre,
# puis filtre sur inscrits et ues
ue_ids = {ue.id for ue in ues}
dispense_ues = {
(dispense_ue.etudid, dispense_ue.ue_id)
for dispense_ue in DispenseUE.query.filter_by(
formsemestre_id=formsemestre.id
)
if dispense_ue.etudid in etudids and dispense_ue.ue_id in ue_ids
}
return dispense_ues

View File

@ -89,7 +89,7 @@ class BaseArchiver(object):
self.archive_type = archive_type
self.initialized = False
self.root = None
self.dept_id = getattr(g, "scodoc_dept_id")
self.dept_id = None
def set_dept_id(self, dept_id: int):
"set dept"
@ -115,6 +115,8 @@ class BaseArchiver(object):
finally:
scu.GSL.release()
self.initialized = True
if self.dept_id is None:
self.dept_id = getattr(g, "scodoc_dept_id")
def get_obj_dir(self, oid: int):
"""

View File

@ -19,9 +19,6 @@ class JustificatifArchiver(BaseArchiver):
[_description.txt]
[<filename.ext>]
TODO:
- Faire fonction suppression fichier unique dans archive
"""
def __init__(self):
@ -38,6 +35,7 @@ class JustificatifArchiver(BaseArchiver):
"""
Ajoute un fichier dans une archive "justificatif" pour l'etudid donné
Retourne l'archive_name utilisé
TODO: renvoie archive_name + filename
"""
self._set_dept(etudid)
if archive_name is None:
@ -104,9 +102,8 @@ class JustificatifArchiver(BaseArchiver):
)
def _set_dept(self, etudid: int):
if g.scodoc_dept is None or g.scodoc_dept_id is None:
etud: Identite = Identite.query.filter_by(id=etudid).first()
dept: Departement = Departement.query.filter_by(id=etud.dept_id).first()
g.scodoc_dept = dept.acronym
g.scodoc_dept_id = dept.id
"""
Mets à jour le dept_id de l'archiver en fonction du département de l'étudiant
"""
etud: Identite = Identite.query.filter_by(id=etudid).first()
self.set_dept_id(etud.dept_id)

View File

@ -85,7 +85,7 @@ def filter_assiduites_by_etat(assiduites: Assiduite, etat: str) -> Assiduite:
Filtrage d'une collection d'assiduites en fonction de leur état
"""
etats: list[str] = list(etat.split(","))
etats = [scu.ETATS_ASSIDUITE.get(e, -1) for e in etats]
etats = [scu.EtatAssiduite.get(e, -1) for e in etats]
return assiduites.filter(Assiduite.etat.in_(etats))
@ -117,7 +117,7 @@ def filter_justificatifs_by_etat(
Filtrage d'une collection de justificatifs en fonction de leur état
"""
etats: list[str] = list(etat.split(","))
etats = [scu.ETATS_JUSTIFICATIF.get(e, -1) for e in etats]
etats = [scu.EtatJustificatif.get(e, -1) for e in etats]
return justificatifs.filter(Justificatif.etat.in_(etats))
@ -172,3 +172,31 @@ def filter_by_formsemestre(assiduites_query: Assiduite, formsemestre: FormSemest
Assiduite.date_debut >= formsemestre.date_debut
)
return assiduites_query.filter(Assiduite.date_fin <= formsemestre.date_fin)
def justifies(justi: Justificatif) -> 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é"
"""
justified: list[int] = []
if justi.etat != scu.EtatJustificatif.VALIDE:
return justified
assiduites_query: Assiduite = Assiduite.query.join(
Justificatif, Assiduite.etudid == Justificatif.etudid
).filter(Assiduite.etat != scu.EtatAssiduite.PRESENT)
assiduites_query = filter_assiduites_by_date(
assiduites_query, justi.date_debut, True
)
assiduites_query = filter_assiduites_by_date(
assiduites_query, justi.date_fin, False
)
justified = [assi.id for assi in assiduites_query.all()]
return justified

View File

@ -36,7 +36,7 @@ from flask_login import current_user
from app import db, log
from app.models import ModuleImpl, ScolarNews
from app.models import Evaluation, ModuleImpl, ScolarNews
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb

View File

@ -93,7 +93,7 @@ _formsemestreEditor = ndb.EditableTable(
)
def get_formsemestre(formsemestre_id, raise_soft_exc=False):
def get_formsemestre(formsemestre_id: int):
"list ONE formsemestre"
if formsemestre_id is None:
raise ValueError("get_formsemestre: id manquant")
@ -105,10 +105,8 @@ def get_formsemestre(formsemestre_id, raise_soft_exc=False):
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
if not sems:
log(f"get_formsemestre: invalid formsemestre_id ({formsemestre_id})")
if raise_soft_exc:
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
else:
raise ValueError(f"semestre {formsemestre_id} inconnu !")
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
g.stored_get_formsemestre[formsemestre_id] = sems[0]
return sems[0]

View File

@ -1629,7 +1629,9 @@ def do_formsemestre_delete(formsemestre_id):
req = """DELETE FROM notes_formsemestre_etapes
WHERE formsemestre_id=%(formsemestre_id)s"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
# --- Dispenses d'UE
req = """DELETE FROM "dispenseUE" WHERE formsemestre_id=%(formsemestre_id)s"""
cursor.execute(req, {"formsemestre_id": formsemestre_id})
# --- Destruction du semestre
sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)

View File

@ -523,7 +523,8 @@ def retreive_formsemestre_from_request() -> int:
# Element HTML decrivant un semestre (barre de menu et infos)
def formsemestre_page_title(formsemestre_id=None):
"""Element HTML decrivant un semestre (barre de menu et infos)
Cherche dans la requete si un semestre est défini (formsemestre_id ou moduleimpl ou evaluation ou group)
Cherche dans la requete si un semestre est défini
via (formsemestre_id ou moduleimpl ou evaluation ou group)
"""
formsemestre_id = (
formsemestre_id
@ -540,15 +541,13 @@ def formsemestre_page_title(formsemestre_id=None):
return ""
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
h = render_template(
return render_template(
"formsemestre_page_title.j2",
formsemestre=formsemestre,
scu=scu,
sem_menu_bar=formsemestre_status_menubar(formsemestre),
)
return h
def fill_formsemestre(sem):
"""Add some useful fields to help display formsemestres"""
@ -768,8 +767,7 @@ def formsemestre_description_table(
caption=title,
html_caption=title,
html_class="table_leftalign formsemestre_description",
base_url="%s?formsemestre_id=%s&with_evals=%s"
% (request.base_url, formsemestre_id, with_evals),
base_url=f"{request.base_url}?formsemestre_id={formsemestre_id}&with_evals={with_evals}",
page_title=title,
html_title=html_sco_header.html_sem_header(
"Description du semestre", with_page_header=False
@ -923,7 +921,7 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
f"""<h4><a
href="{
url_for("scolar.edit_partition_form",
formsemestre_id=formsemestre.id,
formsemestre_id=formsemestre.id,
scodoc_dept=g.scodoc_dept,
)
}">Ajouter une partition</a></h4>"""
@ -980,14 +978,14 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
),
f"""<table>
<tr><td class="fichetitre2">Formation: </td><td>
<a href="{url_for('notes.ue_table',
<a href="{url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=sem.formation.id)}"
class="discretelink" title="Formation {
formation.acronyme}, v{formation.version}">{formation.titre}</a>
""",
]
if sem.semestre_id >= 0:
H.append(", %s %s" % (parcours.SESSION_NAME, sem.semestre_id))
H.append(f", {parcours.SESSION_NAME} {sem.semestre_id}")
if sem.modalite:
H.append(f"&nbsp;en {sem.modalite}")
if sem.etapes:
@ -1091,7 +1089,8 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
elif datetime.date.today() > formsemestre.date_fin:
if formsemestre.etat:
H.append(
"""<span class="formsemestre_status_warning">semestre du passé non verrouillé</span>"""
"""<span
class="formsemestre_status_warning">semestre terminé mais non verrouillé</span>"""
)
else:
H.append(
@ -1101,7 +1100,8 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
if sco_preferences.get_preference("bul_show_all_evals", formsemestre_id):
H.append(
"""<div class="formsemestre_status_warning">Toutes évaluations (même incomplètes) visibles</div>"""
"""<div class="formsemestre_status_warning"
>Toutes évaluations (même incomplètes) visibles</div>"""
)
if nt.expr_diagnostics:
@ -1215,6 +1215,11 @@ def formsemestre_tableau_modules(
prev_ue_id = None
for modimpl in modimpls:
mod: Module = Module.query.get(modimpl["module_id"])
moduleimpl_status_url = url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=modimpl["moduleimpl_id"],
)
mod_descr = "Module " + (mod.titre or "")
if mod.is_apc():
coef_descr = ", ".join(
@ -1240,7 +1245,7 @@ def formsemestre_tableau_modules(
prev_ue_id = ue["ue_id"]
titre = ue["titre"]
if use_ue_coefs:
titre += " <b>(coef. %s)</b>" % (ue["coefficient"] or 0.0)
titre += f""" <b>(coef. {ue["coefficient"] or 0.0})</b>"""
H.append(
f"""<tr class="formsemestre_status_ue"><td colspan="4">
<span class="status_ue_acro">{ue["acronyme"]}</span>
@ -1280,23 +1285,18 @@ def formsemestre_tableau_modules(
H.append(f'<tr class="formsemestre_status{fontorange}">')
H.append(
f"""<td class="formsemestre_status_code""><a
href="{url_for('notes.moduleimpl_status',
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl['moduleimpl_id'])}"
title="{mod_descr}" class="stdlink">{mod.code}</a></td>"""
)
H.append(
f"""<td class="scotext"><a href="{
url_for( "notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl["moduleimpl_id"]
) }" title="{mod_descr}" class="formsemestre_status_link">{mod.abbrev or mod.titre or ""}</a>
f"""
<td class="formsemestre_status_code""><a
href="{moduleimpl_status_url}"
title="{mod_descr}" class="stdlink">{mod.code}</a></td>
<td class="scotext"><a href="{moduleimpl_status_url}" title="{mod_descr}"
class="formsemestre_status_link">{mod.abbrev or mod.titre or ""}</a>
</td>
<td class="formsemestre_status_inscrits">{len(mod_inscrits)}</td>
<td class="resp scotext">
<a class="discretelink" href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl["moduleimpl_id"]
) }" title="{mod_ens}">{ sco_users.user_info(modimpl["responsable_id"])["prenomnom"] }</a>
<a class="discretelink" href="{moduleimpl_status_url}" title="{mod_ens}">{
sco_users.user_info(modimpl["responsable_id"])["prenomnom"]
}</a>
</td>
<td>
"""
@ -1331,18 +1331,21 @@ def formsemestre_tableau_modules(
)
if nb_evals != 0:
H.append(
'<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">%s prévues, %s ok</a>'
% (modimpl["moduleimpl_id"], nb_evals, etat["nb_evals_completes"])
f"""<a href="{moduleimpl_status_url}"
class="formsemestre_status_link">{nb_evals} prévues,
{etat["nb_evals_completes"]} ok</a>"""
)
if etat["nb_evals_en_cours"] > 0:
H.append(
', <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il manque des notes">%s en cours</a></span>'
% (modimpl["moduleimpl_id"], etat["nb_evals_en_cours"])
f""", <span><a class="redlink" href="{moduleimpl_status_url}"
title="Il manque des notes">{
etat["nb_evals_en_cours"]
} en cours</a></span>"""
)
if etat["attente"]:
H.append(
' <span><a class="redlink" href="moduleimpl_status?moduleimpl_id=%s" title="Il y a des notes en attente">[en attente]</a></span>'
% modimpl["moduleimpl_id"]
f""" <span><a class="redlink" href="{moduleimpl_status_url}"
title="Il y a des notes en attente">[en attente]</a></span>"""
)
elif mod.module_type == ModuleType.MALUS:
nb_malus_notes = sum(
@ -1352,10 +1355,10 @@ def formsemestre_tableau_modules(
]
)
H.append(
"""<td class="malus">
<a href="moduleimpl_status?moduleimpl_id=%s" class="formsemestre_status_link">malus (%d notes)</a>
f"""<td class="malus">
<a href="{moduleimpl_status_url}" class="formsemestre_status_link">malus
({nb_malus_notes} notes)</a>
"""
% (modimpl["moduleimpl_id"], nb_malus_notes)
)
else:
raise ValueError(f"Invalid module_type {mod.module_type}") # a bug

View File

@ -300,9 +300,7 @@ def get_group_infos(group_id, etat=None): # was _getlisteetud
cnx = ndb.GetDBConnexion()
group = get_group(group_id)
sem = sco_formsemestre.get_formsemestre(
group["formsemestre_id"], raise_soft_exc=True
)
sem = sco_formsemestre.get_formsemestre(group["formsemestre_id"])
members = get_group_members(group_id, etat=etat)
# add human readable description of state:

View File

@ -7,7 +7,7 @@ from flask import g
from flask_login import current_user
from app.auth.models import User
from app.models import FormSemestre
import app.scodoc.notesdb as ndb
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
@ -164,18 +164,14 @@ def check_access_diretud(formsemestre_id, required_permission=Permission.ScoImpl
return True, ""
def can_change_groups(formsemestre_id):
def can_change_groups(formsemestre_id: int) -> bool:
"Vrai si l'utilisateur peut changer les groupes dans ce semestre"
from app.scodoc import sco_formsemestre
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
if not sem["etat"]:
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.etat:
return False # semestre verrouillé
if current_user.has_permission(Permission.ScoEtudChangeGroups):
return True # admin, chef dept
if current_user.id in sem["responsables"]:
return True
return False
return True # typiquement admin, chef dept
return formsemestre.est_responsable(current_user)
def can_handle_passwd(user: User, allow_admindepts=False) -> bool:

View File

@ -32,7 +32,7 @@ import base64
import bisect
import copy
import datetime
from enum import IntEnum
from enum import IntEnum, Enum
import io
import json
from hashlib import md5
@ -50,17 +50,17 @@ from PIL import Image as PILImage
import pydot
import requests
import dateutil.parser as dtparser
import flask
from flask import g, request
from flask import flash, url_for, make_response, jsonify
from werkzeug.http import HTTP_STATUS_CODES
from config import Config
from app import log
from app import log, db
from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_codes_parcours import NOTES_TOLERANCE, CODES_EXPL
from app.scodoc import sco_xml
from app.scodoc.intervals import intervalmap
import sco_version
@ -88,7 +88,43 @@ ETATS_INSCRIPTION = {
}
class EtatAssiduite(IntEnum):
def get_model_api_object(model_cls: db.Model, model_id: int):
from app.models import Identite
query = model_cls.query.filter_by(id=model_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
unique: model_cls = query.first_or_404()
return jsonify(unique.to_dict(format_api=True))
class BiDirectionalEnum(Enum):
"""Permet la recherche inverse d'un enum
Condition : les clés et les valeurs doivent être uniques
les clés doivent être en MAJUSCULES
"""
@classmethod
def contains(cls, attr: str):
return attr.upper() in cls._member_names_
@classmethod
def get(cls, attr: str, default: any = None):
val = None
try:
val = cls[attr.upper()]
except (KeyError, AttributeError):
val = default
return val
@classmethod
def inverse(cls):
"""Retourne un dictionnaire représentant la map inverse de l'Enum"""
return cls._value2member_map_
class EtatAssiduite(int, BiDirectionalEnum):
"""Code des états d'assiduité"""
# Stockés en BD ne pas modifier
@ -110,7 +146,7 @@ ETATS_ASSIDUITE = {
}
class EtatJustificatif(IntEnum):
class EtatJustificatif(int, BiDirectionalEnum):
"""Code des états des justificatifs"""
# Stockés en BD ne pas modifier
@ -121,21 +157,6 @@ class EtatJustificatif(IntEnum):
MODIFIE = 3
ETAT_JUSTIFICATIF_NAME = {
EtatJustificatif.VALIDE: "validé",
EtatJustificatif.NON_VALIDE: "non validé",
EtatJustificatif.ATTENTE: "en attente",
EtatJustificatif.MODIFIE: "modifié",
}
ETATS_JUSTIFICATIF = {
"validé": EtatJustificatif.VALIDE,
"non vaidé": EtatJustificatif.NON_VALIDE,
"en attente": EtatJustificatif.ATTENTE,
"modifié": EtatJustificatif.MODIFIE,
}
def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None:
"""
Vérifie si une date est au format iso
@ -147,7 +168,6 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No
Retourne None sinon
"""
import dateutil.parser as dtparser
try:
date: datetime.datetime = dtparser.isoparse(date)

View File

@ -1621,10 +1621,24 @@ def etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
ue = UniteEns.query.get_or_404(ue_id)
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if ue.formation.is_apc():
if DispenseUE.query.filter_by(etudid=etudid, ue_id=ue_id).count() == 0:
disp = DispenseUE(ue_id=ue_id, etudid=etudid)
if (
DispenseUE.query.filter_by(
formsemestre_id=formsemestre_id, etudid=etudid, ue_id=ue_id
).count()
== 0
):
disp = DispenseUE(
formsemestre_id=formsemestre_id, ue_id=ue_id, etudid=etudid
)
db.session.add(disp)
db.session.commit()
log(f"etud_desinscrit_ue {etud} {ue}")
Scolog.logdb(
method="etud_desinscrit_ue",
etudid=etud.id,
msg=f"Désinscription de l'UE {ue.acronyme} de {formsemestre.titre_annee()}",
commit=True,
)
sco_cache.invalidate_formsemestre(formsemestre_id=formsemestre.id)
else:
sco_moduleimpl_inscriptions.do_etud_desinscrit_ue_classic(

View File

@ -10,58 +10,86 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'dbcf2175e87f'
down_revision = '5c7b208355df'
revision = "dbcf2175e87f"
down_revision = "5c7b208355df"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('justificatifs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('etudid', sa.Integer(), nullable=False),
sa.Column('etat', sa.Integer(), nullable=False),
sa.Column('entry_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.Column('raison', sa.Text(), nullable=True),
sa.Column('fichier', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
op.create_table(
"justificatifs",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column(
"date_debut",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"date_fin",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column("etudid", sa.Integer(), nullable=False),
sa.Column("etat", sa.Integer(), nullable=False),
sa.Column(
"entry_date",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.Column("raison", sa.Text(), nullable=True),
sa.Column("fichier", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f('ix_justificatifs_etudid'), 'justificatifs', ['etudid'], unique=False)
op.create_table('assiduites',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('date_debut', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('date_fin', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('moduleimpl_id', sa.Integer(), nullable=True),
sa.Column('etudid', sa.Integer(), nullable=False),
sa.Column('etat', sa.Integer(), nullable=False),
sa.Column('desc', sa.Text(), nullable=True),
sa.Column('entry_date', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True),
sa.ForeignKeyConstraint(['etudid'], ['identite.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['moduleimpl_id'], ['notes_moduleimpl.id'], ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id')
op.create_index(
op.f("ix_justificatifs_etudid"), "justificatifs", ["etudid"], unique=False
)
op.create_table(
"assiduites",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column(
"date_debut",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column(
"date_fin",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=False,
),
sa.Column("moduleimpl_id", sa.Integer(), nullable=True),
sa.Column("etudid", sa.Integer(), nullable=False),
sa.Column("etat", sa.Integer(), nullable=False),
sa.Column("desc", sa.Text(), nullable=True),
sa.Column(
"entry_date",
sa.DateTime(timezone=True),
server_default=sa.text("now()"),
nullable=True,
),
sa.ForeignKeyConstraint(["etudid"], ["identite.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(
["moduleimpl_id"], ["notes_moduleimpl.id"], ondelete="SET NULL"
),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_assiduites_etudid"), "assiduites", ["etudid"], unique=False
)
op.create_index(op.f('ix_assiduites_etudid'), 'assiduites', ['etudid'], unique=False)
op.drop_constraint('dispenseUE_formsemestre_id_ue_id_etudid_key', 'dispenseUE', type_='unique')
op.drop_index('ix_dispenseUE_formsemestre_id', table_name='dispenseUE')
op.create_unique_constraint(None, 'dispenseUE', ['ue_id', 'etudid'])
op.drop_constraint('dispenseUE_formsemestre_id_fkey', 'dispenseUE', type_='foreignkey')
op.drop_column('dispenseUE', 'formsemestre_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('dispenseUE', sa.Column('formsemestre_id', sa.INTEGER(), autoincrement=False, nullable=True))
op.create_foreign_key('dispenseUE_formsemestre_id_fkey', 'dispenseUE', 'notes_formsemestre', ['formsemestre_id'], ['id'])
op.drop_constraint(None, 'dispenseUE', type_='unique')
op.create_index('ix_dispenseUE_formsemestre_id', 'dispenseUE', ['formsemestre_id'], unique=False)
op.create_unique_constraint('dispenseUE_formsemestre_id_ue_id_etudid_key', 'dispenseUE', ['formsemestre_id', 'ue_id', 'etudid'])
op.drop_index(op.f('ix_assiduites_etudid'), table_name='assiduites')
op.drop_table('assiduites')
op.drop_index(op.f('ix_justificatifs_etudid'), table_name='justificatifs')
op.drop_table('justificatifs')
op.drop_index(op.f("ix_assiduites_etudid"), table_name="assiduites")
op.drop_table("assiduites")
op.drop_index(op.f("ix_justificatifs_etudid"), table_name="justificatifs")
op.drop_table("justificatifs")
# ### end Alembic commands ###

View File

@ -1,5 +1,5 @@
"""
Test de l'api Assiduité
Test de l'api justificatif
Ecrit par HARTMANN Matthias
@ -121,7 +121,7 @@ def test_route_create(api_headers):
# -== Unique ==-
# Bon fonctionnement
data = create_data("validé", "01")
data = create_data("valide", "01")
res = POST_JSON(f"/justificatif/{ETUDID}/create", [data], api_headers)
check_fields(res, BATCH_FIELD)
@ -129,7 +129,7 @@ def test_route_create(api_headers):
TO_REMOVE.append(res["success"]["0"]["justif_id"])
data2 = create_data("modifié", "02", "raison")
data2 = create_data("modifie", "02", "raison")
res = POST_JSON(f"/justificatif/{ETUDID}/create", [data2], api_headers)
check_fields(res, BATCH_FIELD)
assert len(res["success"]) == 1
@ -160,7 +160,7 @@ def test_route_create(api_headers):
# Bon Fonctionnement
etats = ["validé", "modifé", "non validé", "en attente"]
etats = ["valide", "modifie", "non_valide", "attente"]
data = [
create_data(etats[d % 4], 10 + d, "raison" if d % 2 else None)
for d in range(randint(3, 5))
@ -175,10 +175,10 @@ def test_route_create(api_headers):
# Mauvais Fonctionnement
data2 = [
create_data("modifié", "01"),
create_data("modifie", "01"),
create_data(None, "25"),
create_data("blabla", 26),
create_data("validé", 32),
create_data("valide", 32),
]
res = POST_JSON(f"/justificatif/{ETUDID}/create", data2, api_headers)
@ -201,7 +201,7 @@ def test_route_edit(api_headers):
# Bon fonctionnement
data = {"etat": "modifié", "raison": "test"}
data = {"etat": "modifie", "raison": "test"}
res = POST_JSON(f"/justificatif/{TO_REMOVE[0]}/edit", data, api_headers)
assert res == {"OK": True}
@ -209,6 +209,8 @@ def test_route_edit(api_headers):
res = POST_JSON(f"/justificatif/{TO_REMOVE[1]}/edit", data, api_headers)
assert res == {"OK": True}
# TODO: Modification date deb / fin
# Mauvais fonctionnement
check_failure_post(f"/justificatif/{FAUX}/edit", api_headers, data)
@ -288,7 +290,7 @@ def send_file(justif_id: int, filename: str, headers):
def check_failure_send(
justif_id: int,
headers,
filename: str = "/opt/scodoc/tests/api/test_api_justificatif.txt",
filename: str = "tests/api/test_api_justificatif.txt",
err: str = None,
):
try:
@ -305,12 +307,13 @@ def test_import_justificatif(api_headers):
# Bon fonctionnement
filename: str = "/opt/scodoc/tests/api/test_api_justificatif.txt"
filename: str = "tests/api/test_api_justificatif.txt"
resp: dict = send_file(1, filename, api_headers)
assert "response" in resp
assert resp["response"] == "imported"
filename: str = "/opt/scodoc/tests/api/test_api_justificatif2.txt"
filename: str = "tests/api/test_api_justificatif2.txt"
resp: dict = send_file(1, filename, api_headers)
assert "response" in resp
assert resp["response"] == "imported"
@ -339,13 +342,32 @@ def test_list_justificatifs(api_headers):
check_failure_get(f"/justificatif/list/{FAUX}", api_headers)
def post_export(id: int, fname: str, api_headers):
url: str = API_URL + f"/justificatif/export/{id}/{fname}"
res = requests.post(url, headers=api_headers)
return res
def test_export(api_headers):
# Bon fonctionnement
assert post_export(1, "test_api_justificatif.txt", api_headers).status_code == 200
# Mauvais fonctionnement
assert (
post_export(FAUX, "test_api_justificatif.txt", api_headers).status_code == 404
)
assert post_export(1, "blabla.txt", api_headers).status_code == 404
assert post_export(2, "blabla.txt", api_headers).status_code == 404
def test_remove_justificatif(api_headers):
# Bon fonctionnement
filename: str = "/opt/scodoc/tests/api/test_api_justificatif.txt"
filename: str = "tests/api/test_api_justificatif.txt"
send_file(2, filename, api_headers)
filename: str = "/opt/scodoc/tests/api/test_api_justificatif2.txt"
filename: str = "tests/api/test_api_justificatif2.txt"
send_file(2, filename, api_headers)
res: dict = POST_JSON("/justificatif/remove/1", {"remove": "all"}, api_headers)
@ -372,3 +394,15 @@ def test_remove_justificatif(api_headers):
check_failure_post("/justificatif/remove/2", api_headers, {})
check_failure_post(f"/justificatif/remove/{FAUX}", api_headers, {"remove": "all"})
check_failure_post("/justificatif/remove/1", api_headers, {"remove": "all"})
def test_justified(api_headers):
# Bon fonctionnement
res: list = GET("/justificatif/justified/1", api_headers)
assert isinstance(res, list)
# Mauvais fonctionnement
check_failure_get(f"/justificatif/justified/{FAUX}", api_headers)

View File

@ -14,7 +14,7 @@ from app import db
from app.scodoc import sco_formsemestre
import app.scodoc.sco_assiduites as scass
from app.models import Assiduite, Identite, FormSemestre, ModuleImpl
from app.models import Assiduite, Justificatif, Identite, FormSemestre, ModuleImpl
from app.scodoc.sco_exceptions import ScoValueError
import app.scodoc.sco_utils as scu
@ -115,13 +115,226 @@ def test_general(test_client):
etud_faux = Identite.query.filter_by(id=etud_faux_dict["id"]).first()
ajouter_assiduites(etuds, moduleimpls, etud_faux)
verifier_comptage_et_filtrage(
justificatifs: list[Justificatif] = ajouter_justificatifs(etuds[0])
verifier_comptage_et_filtrage_assiduites(
etuds, moduleimpls, (formsemestre_1, formsemestre_2, formsemestre_3)
)
editer_supprimer_assiduiter(etuds, moduleimpls)
verifier_filtrage_justificatifs(etuds[0], justificatifs)
editer_supprimer_assiduites(etuds, moduleimpls)
editer_supprimer_justificatif(etuds[0])
def editer_supprimer_assiduiter(etuds: list[Identite], moduleimpls: list[int]):
def ajouter_justificatifs(etud):
obj_justificatifs = [
{
"etat": scu.EtatJustificatif.ATTENTE,
"deb": "2022-09-03T08:00+01:00",
"fin": "2022-09-03T09:59:59+01:00",
"raison": None,
},
{
"etat": scu.EtatJustificatif.VALIDE,
"deb": "2023-01-03T07:00+01:00",
"fin": "2023-01-03T11:00+01:00",
"raison": None,
},
{
"etat": scu.EtatJustificatif.VALIDE,
"deb": "2022-09-03T10:00:00+01:00",
"fin": "2022-09-03T12:00+01:00",
"raison": None,
},
{
"etat": scu.EtatJustificatif.NON_VALIDE,
"deb": "2022-09-03T14:00:00+01:00",
"fin": "2022-09-03T15:00+01:00",
"raison": "Description",
},
{
"etat": scu.EtatJustificatif.MODIFIE,
"deb": "2023-01-03T11:30+01:00",
"fin": "2023-01-03T12:00+01:00",
"raison": None,
},
]
justificatifs = [
Justificatif.create_justificatif(
etud,
scu.is_iso_formated(just["deb"], True),
scu.is_iso_formated(just["fin"], True),
just["etat"],
just["raison"],
)
for just in obj_justificatifs
]
# Vérification de la création des justificatifs
assert [
justi for justi in justificatifs if not isinstance(justi, Justificatif)
] == [], "La création des justificatifs de base n'est pas OK"
# Vérification de la gestion des erreurs
test_assiduite = {
"etat": scu.EtatJustificatif.ATTENTE,
"deb": "2023-01-03T11:00:01+01:00",
"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
def verifier_filtrage_justificatifs(etud: Identite, justificatifs: list[Justificatif]):
"""
- vérifier le filtrage des justificatifs (etat, debut, fin)
"""
# Vérification du filtrage classique
# Etat
assert (
scass.filter_justificatifs_by_etat(etud.justificatifs, "valide").count() == 2
), "Filtrage de l'état 'valide' mauvais"
assert (
scass.filter_justificatifs_by_etat(etud.justificatifs, "attente").count() == 1
), f"Filtrage de l'état 'attente' mauvais"
assert (
scass.filter_justificatifs_by_etat(etud.justificatifs, "modifie").count() == 1
), "Filtrage de l'état 'modifie' mauvais"
assert (
scass.filter_justificatifs_by_etat(etud.justificatifs, "non_valide").count()
== 1
), "Filtrage de l'état 'non_valide' mauvais"
assert (
scass.filter_justificatifs_by_etat(etud.justificatifs, "valide,modifie").count()
== 3
), "Filtrage de l'état 'valide,modifie' mauvais"
assert (
scass.filter_justificatifs_by_etat(
etud.justificatifs, "valide,modifie,attente"
).count()
== 4
), "Filtrage de l'état 'valide,modifie,attente' mauvais"
assert (
scass.filter_justificatifs_by_etat(
etud.justificatifs, "valide,modifie,attente,non_valide"
).count()
== 5
), "Filtrage de l'état 'valide,modifie,attente,_non_valide' mauvais"
assert (
scass.filter_justificatifs_by_etat(etud.justificatifs, "autre").count() == 0
), "Filtrage de l'état 'autre' mauvais"
# Date début
date = scu.localize_datetime("2022-09-01T10:00+01:00")
assert (
scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count()
== 5
), "Filtrage 'Date début' mauvais 1"
date = scu.localize_datetime("2022-09-03T08:00:00+01:00")
assert (
scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count()
== 5
), "Filtrage 'Date début' mauvais 2"
date = scu.localize_datetime("2022-09-03T09:00:00+01:00")
assert (
scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count()
== 4
), "Filtrage 'Date début' mauvais 3"
date = scu.localize_datetime("2022-09-03T09:00:02+01:00")
assert (
scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=True).count()
== 4
), "Filtrage 'Date début' mauvais 4"
# Date fin
date = scu.localize_datetime("2022-09-01T10:00+01:00")
assert (
scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count()
== 0
), "Filtrage 'Date fin' mauvais 1"
date = scu.localize_datetime("2022-09-03T10:00:00+01:00")
assert (
scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count()
== 1
), "Filtrage 'Date fin' mauvais 2"
date = scu.localize_datetime("2022-09-03T10:00:01+01:00")
assert (
scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count()
== 1
), "Filtrage 'Date fin' mauvais 3"
date = scu.localize_datetime("2023-01-04T13:00:01+01:00")
assert (
scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count()
== 5
), "Filtrage 'Date fin' mauvais 4"
date = scu.localize_datetime("2023-01-03T11:00:01+01:00")
assert (
scass.filter_justificatifs_by_date(etud.justificatifs, date, sup=False).count()
== 4
), "Filtrage 'Date fin' mauvais 5"
# Justifications des assiduites
assert len(scass.justifies(justificatifs[2])) == 1, "Justifications mauvais"
assert len(scass.justifies(justificatifs[0])) == 0, f"Justifications mauvais"
def editer_supprimer_justificatif(etud: Identite):
"""
Troisième Partie:
- Vérification de l'édition des justificatifs
- Vérification de la suppression des justificatifs
"""
justi: Justificatif = etud.justificatifs.first()
# Modification de l'état
justi.etat = scu.EtatJustificatif.MODIFIE
db.session.add(justi)
# Modification du moduleimpl
justi.date_debut = scu.localize_datetime("2023-02-03T11:00:01+01:00")
justi.fin = scu.localize_datetime("2023-02-03T12:00:01+01:00")
db.session.add(justi)
db.session.commit()
# Vérification du changement
assert (
scass.filter_justificatifs_by_etat(etud.justificatifs, "modifie").count() == 2
), "Edition de justificatif mauvais"
assert (
scass.filter_justificatifs_by_date(
etud.justificatifs, scu.localize_datetime("2023-02-03T11:00:00+01:00")
).count()
== 1
), "Edition de justificatif mauvais"
# Supression d'une assiduité
db.session.delete(justi)
db.session.commit()
assert etud.justificatifs.count() == 4, "Supression de justificatif mauvais"
def editer_supprimer_assiduites(etuds: list[Identite], moduleimpls: list[int]):
"""
Troisième Partie:
- Vérification de l'édition des assiduitées
@ -217,8 +430,8 @@ def ajouter_assiduites(
assiduites = [
Assiduite.create_assiduite(
etud,
ass["deb"],
ass["fin"],
scu.is_iso_formated(ass["deb"], True),
scu.is_iso_formated(ass["fin"], True),
ass["etat"],
ass["moduleimpl"],
ass["desc"],
@ -244,8 +457,8 @@ def ajouter_assiduites(
try:
Assiduite.create_assiduite(
etuds[0],
test_assiduite["deb"],
test_assiduite["fin"],
scu.is_iso_formated(test_assiduite["deb"], True),
scu.is_iso_formated(test_assiduite["fin"], True),
test_assiduite["etat"],
test_assiduite["moduleimpl"],
test_assiduite["desc"],
@ -258,8 +471,8 @@ def ajouter_assiduites(
try:
Assiduite.create_assiduite(
etud_faux,
test_assiduite["deb"],
test_assiduite["fin"],
scu.is_iso_formated(test_assiduite["deb"], True),
scu.is_iso_formated(test_assiduite["fin"], True),
test_assiduite["etat"],
test_assiduite["moduleimpl"],
test_assiduite["desc"],
@ -268,7 +481,7 @@ def ajouter_assiduites(
assert excp.args[0] == "L'étudiant n'est pas inscrit au moduleimpl"
def verifier_comptage_et_filtrage(
def verifier_comptage_et_filtrage_assiduites(
etuds: list[Identite], moduleimpls: list[int], formsemestres: tuple[int]
):
"""

View File

@ -123,7 +123,13 @@ def test_ue_moy(test_client):
modimpl.module.ue.type != UE_SPORT for modimpl in formsemestre.modimpls_sorted
]
etud_moy_ue = moy_ue.compute_ue_moys_apc(
sem_cube, etuds, modimpls, modimpl_inscr_df, modimpl_coefs_df, modimpl_mask
sem_cube,
etuds,
modimpls,
modimpl_inscr_df,
modimpl_coefs_df,
modimpl_mask,
set(),
)
assert etud_moy_ue[ue1.id][etudid] == n1
assert etud_moy_ue[ue2.id][etudid] == n1