Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into edit_roles

This commit is contained in:
Emmanuel Viennet 2023-09-24 19:05:59 +02:00
commit d8fbedb96d
128 changed files with 3646 additions and 2226 deletions

View File

@ -27,7 +27,7 @@ from app.models import (
Justificatif, Justificatif,
) )
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from app.models.assiduites import get_assiduites_justif from app.models.assiduites import get_assiduites_justif, get_justifs_from_date
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import json_error
@ -559,6 +559,7 @@ def _create_singular(
data: dict, data: dict,
etud: Identite, etud: Identite,
) -> tuple[int, object]: ) -> tuple[int, object]:
"""TODO: documenter"""
errors: list[str] = [] errors: list[str] = []
# -- vérifications de l'objet json -- # -- vérifications de l'objet json --
@ -601,9 +602,12 @@ def _create_singular(
moduleimpl_id = data.get("moduleimpl_id", False) moduleimpl_id = data.get("moduleimpl_id", False)
moduleimpl: ModuleImpl = None moduleimpl: ModuleImpl = None
if moduleimpl_id not in [False, None]: if moduleimpl_id not in [False, None, "", "-1"]:
if moduleimpl_id != "autre": if moduleimpl_id != "autre":
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() try:
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first()
except ValueError:
moduleimpl = None
if moduleimpl is None: if moduleimpl is None:
errors.append("param 'moduleimpl_id': invalide") errors.append("param 'moduleimpl_id': invalide")
else: else:
@ -725,7 +729,6 @@ def assiduite_edit(assiduite_id: int):
assiduite_unique.etudiant.id, assiduite_unique.etudiant.id,
msg=f"assiduite: modif {assiduite_unique}", msg=f"assiduite: modif {assiduite_unique}",
) )
db.session.add(assiduite_unique)
db.session.commit() db.session.commit()
scass.simple_invalidate_cache(assiduite_unique.to_dict()) scass.simple_invalidate_cache(assiduite_unique.to_dict())
@ -810,7 +813,7 @@ def _edit_singular(assiduite_unique, data):
moduleimpl: ModuleImpl = None moduleimpl: ModuleImpl = None
if moduleimpl_id is not False: if moduleimpl_id is not False:
if moduleimpl_id is not None: if moduleimpl_id not in [None, "", "-1"]:
if moduleimpl_id == "autre": if moduleimpl_id == "autre":
assiduite_unique.moduleimpl_id = None assiduite_unique.moduleimpl_id = None
external_data = ( external_data = (
@ -823,7 +826,13 @@ def _edit_singular(assiduite_unique, data):
assiduite_unique.external_data = external_data assiduite_unique.external_data = external_data
else: else:
moduleimpl = ModuleImpl.query.filter_by(id=int(moduleimpl_id)).first() try:
moduleimpl = ModuleImpl.query.filter_by(
id=int(moduleimpl_id)
).first()
except ValueError:
moduleimpl = None
if moduleimpl is None: if moduleimpl is None:
errors.append("param 'moduleimpl_id': invalide") errors.append("param 'moduleimpl_id': invalide")
else: else:
@ -834,20 +843,28 @@ def _edit_singular(assiduite_unique, data):
else: else:
assiduite_unique.moduleimpl_id = moduleimpl_id assiduite_unique.moduleimpl_id = moduleimpl_id
else: else:
assiduite_unique.moduleimpl_id = moduleimpl_id assiduite_unique.moduleimpl_id = None
# Cas 3 : desc # Cas 3 : desc
desc = data.get("desc", False) desc = data.get("desc", False)
if desc is not False: if desc is not False:
assiduite_unique.desc = desc assiduite_unique.description = desc
# Cas 4 : est_just # Cas 4 : est_just
est_just = data.get("est_just") if assiduite_unique.etat == scu.EtatAssiduite.PRESENT:
if est_just is not None: assiduite_unique.est_just = False
if not isinstance(est_just, bool): else:
errors.append("param 'est_just' : booléen non reconnu") assiduite_unique.est_just = (
else: len(
assiduite_unique.est_just = est_just get_justifs_from_date(
assiduite_unique.etudiant.id,
assiduite_unique.date_debut,
assiduite_unique.date_fin,
valid=True,
)
)
> 0
)
if errors: if errors:
err: str = ", ".join(errors) err: str = ", ".join(errors)
@ -1015,6 +1032,19 @@ def _filter_manager(requested, assiduites_query: Query) -> Query:
if user_id is not False: if user_id is not False:
assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id) assiduites_query: Query = scass.filter_by_user_id(assiduites_query, user_id)
order = requested.args.get("order", None)
if order is not None:
assiduites_query: Query = assiduites_query.order_by(Assiduite.date_debut.desc())
courant = requested.args.get("courant", None)
if courant is not None:
annee: int = scu.annee_scolaire()
assiduites_query: Query = assiduites_query.filter(
Assiduite.date_debut >= scu.date_debut_anne_scolaire(annee),
Assiduite.date_fin <= scu.date_fin_anne_scolaire(annee),
)
return assiduites_query return assiduites_query

View File

@ -359,7 +359,7 @@ def bulletin(
with_img_signatures_pdf: bool = True, with_img_signatures_pdf: bool = True,
): ):
""" """
Retourne le bulletin d'un étudiant en fonction de son id et d'un semestre donné Retourne le bulletin d'un étudiant dans un formsemestre.
formsemestre_id : l'id d'un formsemestre formsemestre_id : l'id d'un formsemestre
code_type : "etudid", "nip" ou "ine" code_type : "etudid", "nip" ou "ine"
@ -376,7 +376,7 @@ def bulletin(
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404() formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first_or_404()
dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404() dept = Departement.query.filter_by(id=formsemestre.dept_id).first_or_404()
if g.scodoc_dept and dept.acronym != g.scodoc_dept: if g.scodoc_dept and dept.acronym != g.scodoc_dept:
return json_error(404, "formsemestre inexistant", as_response=True) return json_error(404, "formsemestre inexistant")
app.set_sco_dept(dept.acronym) app.set_sco_dept(dept.acronym)
if code_type == "nip": if code_type == "nip":
@ -399,7 +399,7 @@ def bulletin(
formsemestre, formsemestre,
etud, etud,
version=version, version=version,
format="pdf", fmt="pdf",
with_img_signatures_pdf=with_img_signatures_pdf, with_img_signatures_pdf=with_img_signatures_pdf,
) )
return pdf_response return pdf_response

View File

@ -8,8 +8,9 @@
from datetime import datetime from datetime import datetime
from flask_json import as_json from flask_json import as_json
from flask import g, jsonify, request from flask import g, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_sqlalchemy.query import Query
import app.scodoc.sco_assiduites as scass import app.scodoc.sco_assiduites as scass
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -18,7 +19,13 @@ from app.api import api_bp as bp
from app.api import api_web_bp from app.api import api_web_bp
from app.api import get_model_api_object, tools from app.api import get_model_api_object, tools
from app.decorators import permission_required, scodoc from app.decorators import permission_required, scodoc
from app.models import Identite, Justificatif, Departement, FormSemestre from app.models import (
Identite,
Justificatif,
Departement,
FormSemestre,
FormSemestreInscription,
)
from app.models.assiduites import ( from app.models.assiduites import (
compute_assiduites_justified, compute_assiduites_justified,
) )
@ -26,7 +33,7 @@ from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import json_error
from flask_sqlalchemy.query import Query from app.scodoc.sco_groups import get_group_members
# Partie Modèle # Partie Modèle
@ -130,6 +137,8 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
@api_web_bp.route( @api_web_bp.route(
"/justificatifs/dept/<int:dept_id>/query", defaults={"with_query": True} "/justificatifs/dept/<int:dept_id>/query", defaults={"with_query": True}
) )
@bp.route("/justificatifs/dept/<int:dept_id>", defaults={"with_query": False})
@bp.route("/justificatifs/dept/<int:dept_id>/query", defaults={"with_query": True})
@login_required @login_required
@scodoc @scodoc
@as_json @as_json
@ -143,9 +152,77 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
if with_query: if with_query:
justificatifs_query = _filter_manager(request, justificatifs_query) justificatifs_query = _filter_manager(request, justificatifs_query)
data_set: list[dict] = [] data_set: list[dict] = []
for just in justificatifs_query.all(): for just in justificatifs_query:
data = just.to_dict(format_api=True) data_set.append(_set_sems_and_groupe(just))
return data_set
def _set_sems_and_groupe(justi: Justificatif) -> dict:
from app.scodoc.sco_groups import get_etud_groups
data = justi.to_dict(format_api=True)
formsemestre: FormSemestre = (
FormSemestre.query.join(
FormSemestreInscription,
FormSemestre.id == FormSemestreInscription.formsemestre_id,
)
.filter(
justi.date_debut <= FormSemestre.date_fin,
justi.date_fin >= FormSemestre.date_debut,
FormSemestreInscription.etudid == justi.etudid,
)
.first()
)
if formsemestre:
data["formsemestre"] = {
"id": formsemestre.id,
"title": formsemestre.session_id(),
}
return data
@bp.route(
"/justificatifs/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
)
@api_web_bp.route(
"/justificatifs/formsemestre/<int:formsemestre_id>", defaults={"with_query": False}
)
@bp.route(
"/justificatifs/formsemestre/<int:formsemestre_id>/query",
defaults={"with_query": True},
)
@api_web_bp.route(
"/justificatifs/formsemestre/<int:formsemestre_id>/query",
defaults={"with_query": True},
)
@login_required
@scodoc
@as_json
@permission_required(Permission.ScoView)
def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
"""Retourne tous les justificatifs du formsemestre"""
formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
if formsemestre is None:
return json_error(404, "le paramètre 'formsemestre_id' n'existe pas")
justificatifs_query = scass.filter_by_formsemestre(
Justificatif.query, Justificatif, formsemestre
)
if with_query:
justificatifs_query = _filter_manager(request, justificatifs_query)
data_set: list[dict] = []
for justi in justificatifs_query.all():
data = justi.to_dict(format_api=True)
data_set.append(data) data_set.append(data)
return data_set return data_set
@ -380,7 +457,7 @@ def justif_edit(justif_id: int):
"après": compute_assiduites_justified( "après": compute_assiduites_justified(
justificatif_unique.etudid, justificatif_unique.etudid,
[justificatif_unique], [justificatif_unique],
False, True,
), ),
} }
} }
@ -436,7 +513,7 @@ def _delete_singular(justif_id: int, database):
if archive_name is not None: if archive_name is not None:
archiver: JustificatifArchiver = JustificatifArchiver() archiver: JustificatifArchiver = JustificatifArchiver()
try: try:
archiver.delete_justificatif(justificatif_unique.etudid, archive_name) archiver.delete_justificatif(justificatif_unique.etudiant, archive_name)
except ValueError: except ValueError:
pass pass
@ -481,7 +558,7 @@ def justif_import(justif_id: int = None):
try: try:
fname: str fname: str
archive_name, fname = archiver.save_justificatif( archive_name, fname = archiver.save_justificatif(
etudid=justificatif_unique.etudid, justificatif_unique.etudiant,
filename=file.filename, filename=file.filename,
data=file.stream.read(), data=file.stream.read(),
archive_name=archive_name, archive_name=archive_name,
@ -512,7 +589,7 @@ def justif_export(justif_id: int = None, filename: str = None):
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificaitf = query.first_or_404() justificatif_unique: Justificatif = query.first_or_404()
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
if archive_name is None: if archive_name is None:
@ -522,7 +599,7 @@ def justif_export(justif_id: int = None, filename: str = None):
try: try:
return archiver.get_justificatif_file( return archiver.get_justificatif_file(
archive_name, justificatif_unique.etudid, filename archive_name, justificatif_unique.etudiant, filename
) )
except ScoValueError as err: except ScoValueError as err:
return json_error(404, err.args[0]) return json_error(404, err.args[0])
@ -564,10 +641,10 @@ def justif_remove(justif_id: int = None):
if remove is None or remove not in ("all", "list"): if remove is None or remove not in ("all", "list"):
return json_error(404, "param 'remove': Valeur invalide") return json_error(404, "param 'remove': Valeur invalide")
archiver: JustificatifArchiver = JustificatifArchiver() archiver: JustificatifArchiver = JustificatifArchiver()
etudid: int = justificatif_unique.etudid etud = justificatif_unique.etudiant
try: try:
if remove == "all": if remove == "all":
archiver.delete_justificatif(etudid=etudid, archive_name=archive_name) archiver.delete_justificatif(etud, archive_name=archive_name)
justificatif_unique.fichier = None justificatif_unique.fichier = None
db.session.add(justificatif_unique) db.session.add(justificatif_unique)
db.session.commit() db.session.commit()
@ -575,13 +652,13 @@ def justif_remove(justif_id: int = None):
else: else:
for fname in data.get("filenames", []): for fname in data.get("filenames", []):
archiver.delete_justificatif( archiver.delete_justificatif(
etudid=etudid, etud,
archive_name=archive_name, archive_name=archive_name,
filename=fname, filename=fname,
) )
if len(archiver.list_justificatifs(archive_name, etudid)) == 0: if len(archiver.list_justificatifs(archive_name, etud)) == 0:
archiver.delete_justificatif(etudid, archive_name) archiver.delete_justificatif(etud, archive_name)
justificatif_unique.fichier = None justificatif_unique.fichier = None
db.session.add(justificatif_unique) db.session.add(justificatif_unique)
db.session.commit() db.session.commit()
@ -616,16 +693,16 @@ def justif_list(justif_id: int = None):
archiver: JustificatifArchiver = JustificatifArchiver() archiver: JustificatifArchiver = JustificatifArchiver()
if archive_name is not None: if archive_name is not None:
filenames = archiver.list_justificatifs( filenames = archiver.list_justificatifs(
archive_name, justificatif_unique.etudid archive_name, justificatif_unique.etudiant
) )
retour = {"total": len(filenames), "filenames": []} retour = {"total": len(filenames), "filenames": []}
for fi in filenames: for filename in filenames:
if int(fi[1]) == current_user.id or current_user.has_permission( if int(filename[1]) == current_user.id or current_user.has_permission(
Permission.ScoJustifView Permission.ScoJustifView
): ):
retour["filenames"].append(fi[0]) retour["filenames"].append(filename[0])
return retour return retour
@ -688,12 +765,41 @@ def _filter_manager(requested, justificatifs_query):
# cas 5 : formsemestre_id # cas 5 : formsemestre_id
formsemestre_id = requested.args.get("formsemestre_id") formsemestre_id = requested.args.get("formsemestre_id")
if formsemestre_id is not None: if formsemestre_id not in [None, "", -1]:
formsemestre: FormSemestre = None formsemestre: FormSemestre = None
formsemestre_id = int(formsemestre_id) try:
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() formsemestre_id = int(formsemestre_id)
justificatifs_query = scass.filter_by_formsemestre( formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
justificatifs_query, Justificatif, formsemestre justificatifs_query = scass.filter_by_formsemestre(
justificatifs_query, Justificatif, formsemestre
)
except ValueError:
formsemestre = None
order = requested.args.get("order", None)
if order is not None:
justificatifs_query: Query = justificatifs_query.order_by(
Justificatif.date_debut.desc()
) )
courant = requested.args.get("courant", None)
if courant is not None:
annee: int = scu.annee_scolaire()
justificatifs_query: Query = justificatifs_query.filter(
Justificatif.date_debut >= scu.date_debut_anne_scolaire(annee),
Justificatif.date_fin <= scu.date_fin_anne_scolaire(annee),
)
group_id = requested.args.get("group_id", None)
if group_id is not None:
try:
group_id = int(group_id)
etudids: list[int] = [etu["etudid"] for etu in get_group_members(group_id)]
justificatifs_query = justificatifs_query.filter(
Justificatif.etudid.in_(etudids)
)
except ValueError:
group_id = None
return justificatifs_query return justificatifs_query

View File

@ -67,3 +67,28 @@ def moduleimpl(moduleimpl_id: int):
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404() modimpl: ModuleImpl = query.first_or_404()
return modimpl.to_dict(convert_objects=True) return modimpl.to_dict(convert_objects=True)
@bp.route("/moduleimpl/<int:moduleimpl_id>/inscriptions")
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/inscriptions")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def moduleimpl_inscriptions(moduleimpl_id: int):
"""Liste des inscriptions à ce moduleimpl
Exemple de résultat :
[
{
"id": 1,
"etudid": 666,
"moduleimpl_id": 1234,
},
...
]
"""
query = ModuleImpl.query.filter_by(id=moduleimpl_id)
if g.scodoc_dept:
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
return [i.to_dict() for i in modimpl.inscriptions]

View File

@ -306,6 +306,13 @@ class User(UserMixin, db.Model):
role, dept = UserRole.role_dept_from_string(r_d) role, dept = UserRole.role_dept_from_string(r_d)
self.add_role(role, dept) self.add_role(role, dept)
# Set cas_id using regexp if configured:
exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
if exp and self.email_institutionnel:
cas_id = ScoDocSiteConfig.extract_cas_id(self.email_institutionnel)
if cas_id is not None:
self.cas_id = cas_id
def get_token(self, expires_in=3600): def get_token(self, expires_in=3600):
"Un jeton pour cet user. Stocké en base, non commité." "Un jeton pour cet user. Stocké en base, non commité."
now = datetime.utcnow() now = datetime.utcnow()

View File

@ -512,10 +512,10 @@ class BulletinBUT:
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id) d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
# --- Decision Jury # --- Decision Jury
infos, dpv = sco_bulletins.etud_descr_situation_semestre( infos, _ = sco_bulletins.etud_descr_situation_semestre(
etud.id, etud.id,
self.res.formsemestre, self.res.formsemestre,
format="html", fmt="html",
show_date_inscr=self.prefs["bul_show_date_inscr"], show_date_inscr=self.prefs["bul_show_date_inscr"],
show_decisions=self.prefs["bul_show_decision"], show_decisions=self.prefs["bul_show_decision"],
show_uevalid=self.prefs["bul_show_uevalid"], show_uevalid=self.prefs["bul_show_uevalid"],

View File

@ -69,13 +69,13 @@ def bulletin_but(formsemestre_id: int, etudid: int = None, fmt="html"):
if fmt == "pdf": if fmt == "pdf":
bul: dict = bulletins_sem.bulletin_etud_complet(etud) bul: dict = bulletins_sem.bulletin_etud_complet(etud)
else: # la même chose avec un peu moins d'infos else: # la même chose avec un peu moins d'infos
bul: dict = bulletins_sem.bulletin_etud(etud) bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True)
decision_ues = ( decision_ues = (
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]} {x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
if "semestre" in bul and "decision_ue" in bul["semestre"] if "semestre" in bul and "decision_ue" in bul["semestre"]
else {} else {}
) )
if not "ues" in bul: if "ues" not in bul:
raise ScoValueError("Aucune UE à afficher") raise ScoValueError("Aucune UE à afficher")
cursus = cursus_but.EtudCursusBUT(etud, formsemestre.formation) cursus = cursus_but.EtudCursusBUT(etud, formsemestre.formation)
refcomp = formsemestre.formation.referentiel_competence refcomp = formsemestre.formation.referentiel_competence

View File

@ -50,7 +50,7 @@ def make_bulletin_but_court_pdf(
try: try:
PDFLOCK.acquire() PDFLOCK.acquire()
bul_generator = BulletinGeneratorBUTCourt(**locals()) bul_generator = BulletinGeneratorBUTCourt(**locals())
bul_pdf = bul_generator.generate(format="pdf") bul_pdf = bul_generator.generate(fmt="pdf")
finally: finally:
PDFLOCK.release() PDFLOCK.release()
return bul_pdf return bul_pdf
@ -499,14 +499,15 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
def boite_decisions_jury(self): def boite_decisions_jury(self):
"""La boite en bas à droite avec jury""" """La boite en bas à droite avec jury"""
txt = f"""ECTS acquis en BUT : <b>{self.ects_total:g}</b><br/>""" txt = f"""ECTS acquis en BUT : <b>{self.ects_total:g}</b><br/>"""
if self.bul["semestre"]["decision_annee"]: if self.bul["semestre"].get("decision_annee", None):
txt += f""" txt += f"""
Jury tenu le { Décision saisie le {
datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y") datetime.datetime.fromisoformat(self.bul["semestre"]["decision_annee"]["date"]).strftime("%d/%m/%Y")
}, année BUT <b>{self.bul["semestre"]["decision_annee"]["code"]}</b>. }, année BUT{self.bul["semestre"]["decision_annee"]["ordre"]}
<b>{self.bul["semestre"]["decision_annee"]["code"]}</b>.
<br/> <br/>
""" """
if self.bul["semestre"]["autorisation_inscription"]: if self.bul["semestre"].get("autorisation_inscription", None):
txt += ( txt += (
"<br/>Autorisé à s'inscrire en <b>" "<br/>Autorisé à s'inscrire en <b>"
+ ", ".join( + ", ".join(

View File

@ -14,7 +14,7 @@ La génération du bulletin PDF suit le chemin suivant:
- sco_bulletins_generator.make_formsemestre_bulletin_etud() - sco_bulletins_generator.make_formsemestre_bulletin_etud()
- instance de BulletinGeneratorStandardBUT - instance de BulletinGeneratorStandardBUT
- BulletinGeneratorStandardBUT.generate(format="pdf") - BulletinGeneratorStandardBUT.generate(fmt="pdf")
sco_bulletins_generator.BulletinGenerator.generate() sco_bulletins_generator.BulletinGenerator.generate()
.generate_pdf() .generate_pdf()
.bul_table() (ci-dessous) .bul_table() (ci-dessous)
@ -24,6 +24,7 @@ from reportlab.lib.colors import blue
from reportlab.lib.units import cm, mm from reportlab.lib.units import cm, mm
from reportlab.platypus import Paragraph, Spacer from reportlab.platypus import Paragraph, Spacer
from app.models import ScoDocSiteConfig
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc import gen_tables from app.scodoc import gen_tables
from app.scodoc.codes_cursus import UE_SPORT from app.scodoc.codes_cursus import UE_SPORT
@ -48,6 +49,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
- en HTML: une chaine - en HTML: une chaine
- en PDF: une liste d'objets PLATYPUS (eg instance de Table). - en PDF: une liste d'objets PLATYPUS (eg instance de Table).
""" """
if fmt == "pdf" and ScoDocSiteConfig.is_bul_pdf_disabled():
return [Paragraph("<p>Export des PDF interdit par l'administrateur</p>")]
tables_infos = [ tables_infos = [
# ---- TABLE SYNTHESE UES # ---- TABLE SYNTHESE UES
self.but_table_synthese_ues(), self.but_table_synthese_ues(),
@ -71,7 +74,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
html_class_ignore_default=True, html_class_ignore_default=True,
html_with_td_classes=True, html_with_td_classes=True,
) )
table_objects = table.gen(format=fmt) table_objects = table.gen(fmt=fmt)
objects += table_objects objects += table_objects
# objects += [KeepInFrame(0, 0, table_objects, mode="shrink")] # objects += [KeepInFrame(0, 0, table_objects, mode="shrink")]
if i != 2: if i != 2:

View File

@ -258,7 +258,7 @@ def bulletin_but_xml_compat(
infos, dpv = sco_bulletins.etud_descr_situation_semestre( infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etudid, etudid,
formsemestre, formsemestre,
format="xml", fmt="xml",
show_uevalid=sco_preferences.get_preference( show_uevalid=sco_preferences.get_preference(
"bul_show_uevalid", formsemestre_id "bul_show_uevalid", formsemestre_id
), ),

View File

@ -61,14 +61,12 @@ DecisionsProposeesUE: décisions de jury sur une UE du BUT
from datetime import datetime from datetime import datetime
import html import html
import re import re
from typing import Union
import numpy as np import numpy as np
from flask import flash, g, url_for from flask import flash, g, url_for
from app import db from app import db
from app import log from app import log
from app.but import cursus_but
from app.but.cursus_but import EtudCursusBUT from app.but.cursus_but import EtudCursusBUT
from app.but.rcue import RegroupementCoherentUE from app.but.rcue import RegroupementCoherentUE
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
@ -150,7 +148,7 @@ class DecisionsProposees:
def __init__( def __init__(
self, self,
etud: Identite = None, etud: Identite = None,
code: Union[str, list[str]] = None, code: str | list[str] | None = None,
explanation="", explanation="",
code_valide=None, code_valide=None,
include_communs=True, include_communs=True,

View File

@ -94,7 +94,7 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
}, },
xls_style_base=xls_style_base, xls_style_base=xls_style_base,
) )
return tab.make_page(format=fmt, javascripts=["js/etud_info.js"], init_qtip=True) return tab.make_page(fmt=fmt, javascripts=["js/etud_info.js"], init_qtip=True)
def pvjury_table_but( def pvjury_table_but(

View File

@ -6,7 +6,6 @@
"""Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs """Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs
""" """
from typing import Union
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
@ -205,7 +204,7 @@ class RegroupementCoherentUE:
self.moy_rcue > codes_cursus.BUT_BARRE_RCUE self.moy_rcue > codes_cursus.BUT_BARRE_RCUE
) )
def code_valide(self) -> Union[ApcValidationRCUE, None]: def code_valide(self) -> ApcValidationRCUE | None:
"Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None" "Si ce RCUE est ADM, CMP ou ADJ, la validation. Sinon, None"
validation = self.query_validations().first() validation = self.query_validations().first()
if (validation is not None) and ( if (validation is not None) and (

View File

@ -344,8 +344,12 @@ def compute_ue_moys_classic(
pd.Series( pd.Series(
[val] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index [val] * len(modimpl_inscr_df.index), index=modimpl_inscr_df.index
), ),
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index), pd.DataFrame(
pd.DataFrame(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index), columns=[ue.id for ue in ues], index=modimpl_inscr_df.index, dtype=float
),
pd.DataFrame(
columns=[ue.id for ue in ues], index=modimpl_inscr_df.index, dtype=float
),
) )
# Restreint aux modules sélectionnés: # Restreint aux modules sélectionnés:
sem_matrix = sem_matrix[:, modimpl_mask] sem_matrix = sem_matrix[:, modimpl_mask]
@ -400,6 +404,7 @@ def compute_ue_moys_classic(
}, },
index=modimpl_inscr_df.index, index=modimpl_inscr_df.index,
columns=[ue.id for ue in ues], columns=[ue.id for ue in ues],
dtype=float,
) )
# remplace NaN par zéros dans les moyennes d'UE # remplace NaN par zéros dans les moyennes d'UE
etud_moy_ue_df_no_nan = etud_moy_ue_df.fillna(0.0, inplace=False) etud_moy_ue_df_no_nan = etud_moy_ue_df.fillna(0.0, inplace=False)
@ -415,6 +420,7 @@ def compute_ue_moys_classic(
coefs.sum(axis=2).T, coefs.sum(axis=2).T,
index=modimpl_inscr_df.index, # etudids index=modimpl_inscr_df.index, # etudids
columns=[ue.id for ue in ues], columns=[ue.id for ue in ues],
dtype=float,
) )
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN) with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_gen = np.sum( etud_moy_gen = np.sum(

View File

@ -186,7 +186,10 @@ def scodoc7func(func):
arg_names = argspec.args arg_names = argspec.args
for arg_name in arg_names: # pour chaque arg de la fonction vue for arg_name in arg_names: # pour chaque arg de la fonction vue
# peut produire une KeyError s'il manque un argument attendu: # peut produire une KeyError s'il manque un argument attendu:
v = req_args[arg_name] try:
v = req_args[arg_name]
except KeyError as exc:
raise ScoValueError(f"argument {arg_name} manquant") from exc
# try to convert all arguments to INTEGERS # try to convert all arguments to INTEGERS
# necessary for db ids and boolean values # necessary for db ids and boolean values
try: try:

View File

@ -76,7 +76,7 @@ class TimeField(StringField):
class ConfigAssiduitesForm(FlaskForm): class ConfigAssiduitesForm(FlaskForm):
"Formulaire paramétrage Module Assiduités" "Formulaire paramétrage Module Assiduité"
morning_time = TimeField("Début de la journée") morning_time = TimeField("Début de la journée")
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)") lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")

View File

@ -30,8 +30,17 @@ Formulaire configuration CAS
""" """
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import BooleanField, SubmitField from wtforms import BooleanField, SubmitField, ValidationError
from wtforms.fields.simple import FileField, StringField from wtforms.fields.simple import FileField, StringField
from wtforms.validators import Optional
from app.models import ScoDocSiteConfig
def check_cas_uid_from_mail_regexp(form, field):
"Vérifie la regexp fournie pur l'extraction du CAS id"
if not ScoDocSiteConfig.cas_uid_from_mail_regexp_is_valid(field.data):
raise ValidationError("expression régulière invalide")
class ConfigCASForm(FlaskForm): class ConfigCASForm(FlaskForm):
@ -50,7 +59,8 @@ class ConfigCASForm(FlaskForm):
) )
cas_login_route = StringField( cas_login_route = StringField(
label="Route du login CAS", label="Route du login CAS",
description="""ajouté à l'URL du serveur: exemple <tt>/cas</tt> (si commence par <tt>/</tt>, part de la racine)""", description="""ajouté à l'URL du serveur: exemple <tt>/cas</tt>
(si commence par <tt>/</tt>, part de la racine)""",
default="/cas", default="/cas",
) )
cas_logout_route = StringField( cas_logout_route = StringField(
@ -70,6 +80,18 @@ class ConfigCASForm(FlaskForm):
comptes utilisateurs.""", comptes utilisateurs.""",
) )
cas_uid_from_mail_regexp = StringField(
label="Expression pour extraire l'identifiant utilisateur",
description="""regexp python appliquée au mail institutionnel de l'utilisateur,
dont le premier groupe doit donner l'identifiant CAS.
Si non fournie, le super-admin devra saisir cet identifiant pour chaque compte.
Par exemple, <tt>(.*)@</tt> indique que le mail sans le domaine (donc toute
la partie avant le <tt>@</tt>) est l'identifiant.
Pour prendre le mail complet, utiliser <tt>(.*)</tt>.
""",
validators=[Optional(), check_cas_uid_from_mail_regexp],
)
cas_ssl_verify = BooleanField("Vérification du certificat SSL") cas_ssl_verify = BooleanField("Vérification du certificat SSL")
cas_ssl_certificate_file = FileField( cas_ssl_certificate_file = FileField(
label="Certificat (PEM)", label="Certificat (PEM)",

View File

@ -76,6 +76,7 @@ class ScoDocConfigurationForm(FlaskForm):
Attention: si ce champ peut aussi être défini dans chaque département.""", Attention: si ce champ peut aussi être défini dans chaque département.""",
validators=[Optional(), Email()], validators=[Optional(), Email()],
) )
disable_bul_pdf = BooleanField("empêcher les exports des bulletins en PDF")
submit_scodoc = SubmitField("Valider") submit_scodoc = SubmitField("Valider")
cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True}) cancel_scodoc = SubmitField("Annuler", render_kw={"formnovalidate": True})
@ -94,6 +95,7 @@ def configuration():
"month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(), "month_debut_annee_scolaire": ScoDocSiteConfig.get_month_debut_annee_scolaire(),
"month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(), "month_debut_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"), "email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
"disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(),
} }
) )
if request.method == "POST" and ( if request.method == "POST" and (
@ -139,6 +141,13 @@ def configuration():
) )
if ScoDocSiteConfig.set("email_from_addr", form_scodoc.data["email_from_addr"]): if ScoDocSiteConfig.set("email_from_addr", form_scodoc.data["email_from_addr"]):
flash("Adresse email origine enregistrée") flash("Adresse email origine enregistrée")
if ScoDocSiteConfig.disable_bul_pdf(
enabled=form_scodoc.data["disable_bul_pdf"]
):
flash(
"Exports PDF "
+ ("désactivés" if form_scodoc.data["disable_bul_pdf"] else "réactivés")
)
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
return render_template( return render_template(

View File

@ -123,7 +123,7 @@ class Assiduite(db.Model):
user_id: int = None, user_id: int = None,
est_just: bool = False, est_just: bool = False,
external_data: dict = None, external_data: dict = None,
) -> object or int: ) -> "Assiduite":
"""Créer une nouvelle assiduité pour l'étudiant""" """Créer une nouvelle assiduité pour l'étudiant"""
# Vérification de non duplication des périodes # Vérification de non duplication des périodes
assiduites: Query = etud.assiduites assiduites: Query = etud.assiduites
@ -134,7 +134,10 @@ class Assiduite(db.Model):
if not est_just: if not est_just:
est_just = ( est_just = (
len(_get_assiduites_justif(etud.etudid, date_debut, date_fin)) > 0 len(
get_justifs_from_date(etud.etudid, date_debut, date_fin, valid=True)
)
> 0
) )
if moduleimpl is not None: if moduleimpl is not None:
@ -153,7 +156,7 @@ class Assiduite(db.Model):
external_data=external_data, external_data=external_data,
) )
else: else:
raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl") raise ScoValueError("L'étudiant n'est pas inscrit au module")
else: else:
nouv_assiduite = Assiduite( nouv_assiduite = Assiduite(
date_debut=date_debut, date_debut=date_debut,
@ -282,7 +285,7 @@ class Justificatif(db.Model):
entry_date: datetime = None, entry_date: datetime = None,
user_id: int = None, user_id: int = None,
external_data: dict = None, external_data: dict = None,
) -> object or int: ) -> "Justificatif":
"""Créer un nouveau justificatif pour l'étudiant""" """Créer un nouveau justificatif pour l'étudiant"""
nouv_justificatif = Justificatif( nouv_justificatif = Justificatif(
date_debut=date_debut, date_debut=date_debut,
@ -310,7 +313,7 @@ def is_period_conflicting(
date_debut: datetime, date_debut: datetime,
date_fin: datetime, date_fin: datetime,
collection: Query, collection: Query,
collection_cls: Assiduite or Justificatif, collection_cls: Assiduite | Justificatif,
) -> bool: ) -> bool:
""" """
Vérifie si une date n'entre pas en collision Vérifie si une date n'entre pas en collision
@ -350,14 +353,26 @@ def compute_assiduites_justified(
if justificatifs is None: if justificatifs is None:
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all() justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid).all()
justificatifs = [j for j in justificatifs if j.etat == EtatJustificatif.VALIDE]
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid)
assiduites_justifiees: list[int] = [] assiduites_justifiees: list[int] = []
for assi in assiduites: for assi in assiduites:
if assi.etat == EtatAssiduite.PRESENT:
continue
assi_justificatifs = Justificatif.query.filter(
Justificatif.etudid == assi.etudid,
Justificatif.date_debut <= assi.date_debut,
Justificatif.date_fin >= assi.date_fin,
Justificatif.etat == EtatJustificatif.VALIDE,
).all()
if any( if any(
assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin assi.date_debut >= j.date_debut and assi.date_fin <= j.date_fin
for j in justificatifs for j in justificatifs + assi_justificatifs
): ):
assi.est_just = True assi.est_just = True
assiduites_justifiees.append(assi.assiduite_id) assiduites_justifiees.append(assi.assiduite_id)
@ -371,16 +386,23 @@ def compute_assiduites_justified(
def get_assiduites_justif(assiduite_id: int, long: bool): def get_assiduites_justif(assiduite_id: int, long: bool):
assi: Assiduite = Assiduite.query.get_or_404(assiduite_id) assi: Assiduite = Assiduite.query.get_or_404(assiduite_id)
return _get_assiduites_justif(assi.etudid, assi.date_debut, assi.date_fin, long) return get_justifs_from_date(assi.etudid, assi.date_debut, assi.date_fin, long)
def _get_assiduites_justif( def get_justifs_from_date(
etudid: int, date_debut: datetime, date_fin: datetime, long: bool = False etudid: int,
date_debut: datetime,
date_fin: datetime,
long: bool = False,
valid: bool = False,
): ):
justifs: Justificatif = Justificatif.query.filter( justifs: Query = Justificatif.query.filter(
Justificatif.etudid == etudid, Justificatif.etudid == etudid,
Justificatif.date_debut <= date_debut, Justificatif.date_debut <= date_debut,
Justificatif.date_fin >= date_fin, Justificatif.date_fin >= date_fin,
) )
if valid:
justifs = justifs.filter(Justificatif.etat == EtatJustificatif.VALIDE)
return [j.justif_id if not long else j.to_dict(True) for j in justifs] return [j.justif_id if not long else j.to_dict(True) for j in justifs]

View File

@ -214,10 +214,12 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
decisions["decision_rcue"] = [] decisions["decision_rcue"] = []
decisions["descr_decisions_rcue"] = "" decisions["descr_decisions_rcue"] = ""
decisions["descr_decisions_niveaux"] = "" decisions["descr_decisions_niveaux"] = ""
# --- Année: prend la validation pour l'année scolaire de ce semestre # --- Année: prend la validation pour l'année scolaire et l'ordre de ce semestre
annee_but = (formsemestre.semestre_id + 1) // 2
validation = ApcValidationAnnee.query.filter_by( validation = ApcValidationAnnee.query.filter_by(
etudid=etud.id, etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(), annee_scolaire=formsemestre.annee_scolaire(),
ordre=annee_but,
referentiel_competence_id=formsemestre.formation.referentiel_competence_id, referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
).first() ).first()
if validation: if validation:

View File

@ -5,6 +5,7 @@
import json import json
import urllib.parse import urllib.parse
import re
from flask import flash from flask import flash
from app import current_app, db, log from app import current_app, db, log
@ -95,6 +96,7 @@ class ScoDocSiteConfig(db.Model):
"enable_entreprises": bool, "enable_entreprises": bool,
"month_debut_annee_scolaire": int, "month_debut_annee_scolaire": int,
"month_debut_periode2": int, "month_debut_periode2": int,
"disable_bul_pdf": bool,
# CAS # CAS
"cas_enable": bool, "cas_enable": bool,
"cas_server": str, "cas_server": str,
@ -102,7 +104,8 @@ class ScoDocSiteConfig(db.Model):
"cas_logout_route": str, "cas_logout_route": str,
"cas_validate_route": str, "cas_validate_route": str,
"cas_attribute_id": str, "cas_attribute_id": str,
# Assiduités "cas_uid_from_mail_regexp": str,
# Assiduité
"morning_time": str, "morning_time": str,
"lunch_time": str, "lunch_time": str,
"afternoon_time": str, "afternoon_time": str,
@ -235,6 +238,12 @@ class ScoDocSiteConfig(db.Model):
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first() cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
return cfg is not None and cfg.value return cfg is not None and cfg.value
@classmethod
def is_bul_pdf_disabled(cls) -> bool:
"""True si on interdit les exports PDF des bulltins"""
cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first()
return cfg is not None and cfg.value
@classmethod @classmethod
def enable_entreprises(cls, enabled=True) -> bool: def enable_entreprises(cls, enabled=True) -> bool:
"""Active (ou déactive) le module entreprises. True si changement.""" """Active (ou déactive) le module entreprises. True si changement."""
@ -251,6 +260,22 @@ class ScoDocSiteConfig(db.Model):
return True return True
return False return False
@classmethod
def disable_bul_pdf(cls, enabled=True) -> bool:
"""Interedit (ou autorise) les exports PDF. True si changement."""
if enabled != ScoDocSiteConfig.is_bul_pdf_disabled():
cfg = ScoDocSiteConfig.query.filter_by(name="disable_bul_pdf").first()
if cfg is None:
cfg = ScoDocSiteConfig(
name="disable_bul_pdf", value="on" if enabled else ""
)
else:
cfg.value = "on" if enabled else ""
db.session.add(cfg)
db.session.commit()
return True
return False
@classmethod @classmethod
def get(cls, name: str, default: str = "") -> str: def get(cls, name: str, default: str = "") -> str:
"Get configuration param; empty string or specified default if unset" "Get configuration param; empty string or specified default if unset"
@ -360,7 +385,7 @@ class ScoDocSiteConfig(db.Model):
cls.set("personalized_links", "") cls.set("personalized_links", "")
raise ScoValueError( raise ScoValueError(
"Attention: liens personnalisés erronés: ils ont été effacés." "Attention: liens personnalisés erronés: ils ont été effacés."
) ) from exc
return [PersonalizedLink(**item) for item in links_dict] return [PersonalizedLink(**item) for item in links_dict]
@classmethod @classmethod
@ -372,6 +397,59 @@ class ScoDocSiteConfig(db.Model):
data_links = json.dumps(links_dict) data_links = json.dumps(links_dict)
cls.set("personalized_links", data_links) cls.set("personalized_links", data_links)
@classmethod
def extract_cas_id(cls, email_addr: str) -> str | None:
"Extract cas_id from maill, using regexp in config. None if not possible."
exp = cls.get("cas_uid_from_mail_regexp")
if not exp or not email_addr:
return None
try:
match = re.search(exp, email_addr)
except re.error:
log("error extracting CAS id from '{email_addr}' using regexp '{exp}'")
return None
if not match:
log("no match extracting CAS id from '{email_addr}' using regexp '{exp}'")
return None
try:
cas_id = match.group(1)
except IndexError:
log(
"no group found extracting CAS id from '{email_addr}' using regexp '{exp}'"
)
return None
return cas_id
@classmethod
def cas_uid_from_mail_regexp_is_valid(cls, exp: str) -> bool:
"True si l'expression régulière semble valide"
# check that it compiles
try:
pattern = re.compile(exp)
except re.error:
return False
# and returns at least one group on a simple cannonical address
match = pattern.search("emmanuel@exemple.fr")
return len(match.groups()) > 0
@classmethod
def assi_get_rounded_time(cls, label: str, default: str) -> float:
"Donne l'heure stockée dans la config globale sous label, en float arrondi au quart d'heure"
return _round_time_str_to_quarter(cls.get(label, default))
def _round_time_str_to_quarter(string: str) -> float:
"""Prend une heure iso '12:20:23', et la converti en un nombre d'heures
en arrondissant au quart d'heure: (les secondes sont ignorées)
"12:20:00" -> 12.25
"12:29:00" -> 12.25
"12:30:00" -> 12.5
"""
parts = [*map(float, string.split(":"))]
hour = parts[0]
minutes = round(parts[1] / 60 * 4) / 4
return hour + minutes
class PersonalizedLink: class PersonalizedLink:
def __init__(self, title: str = "", url: str = "", with_args: bool = False): def __init__(self, title: str = "", url: str = "", with_args: bool = False):

View File

@ -74,9 +74,11 @@ class Identite(db.Model):
) )
# Relations avec les assiduites et les justificatifs # Relations avec les assiduites et les justificatifs
assiduites = db.relationship("Assiduite", back_populates="etudiant", lazy="dynamic") assiduites = db.relationship(
"Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
)
justificatifs = db.relationship( justificatifs = db.relationship(
"Justificatif", back_populates="etudiant", lazy="dynamic" "Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
) )
def __repr__(self): def __repr__(self):

View File

@ -536,7 +536,9 @@ def check_convert_evaluation_args(moduleimpl: ModuleImpl, data: dict):
raise ScoValueError("invalid note_max value (must be positive or null)") raise ScoValueError("invalid note_max value (must be positive or null)")
data["note_max"] = note_max data["note_max"] = note_max
# --- coefficient # --- coefficient
coef = data.get("coefficient", 1.0) or 1.0 coef = data.get("coefficient", None)
if coef is None:
coef = 1.0
try: try:
coef = float(coef) coef = float(coef)
except ValueError as exc: except ValueError as exc:

View File

@ -18,6 +18,7 @@ from flask_login import current_user
from flask import flash, g, url_for from flask import flash, g, url_for
from sqlalchemy.sql import text from sqlalchemy.sql import text
from sqlalchemy import func
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import db, log from app import db, log
@ -138,6 +139,7 @@ class FormSemestre(db.Model):
secondary="notes_formsemestre_responsables", secondary="notes_formsemestre_responsables",
lazy=True, lazy=True,
backref=db.backref("formsemestres", lazy=True), backref=db.backref("formsemestres", lazy=True),
order_by=func.upper(User.nom),
) )
partitions = db.relationship( partitions = db.relationship(
"Partition", "Partition",
@ -195,6 +197,7 @@ class FormSemestre(db.Model):
""" """
d = dict(self.__dict__) d = dict(self.__dict__)
d.pop("_sa_instance_state", None) d.pop("_sa_instance_state", None)
d.pop("groups_auto_assignment_data", None)
# ScoDoc7 output_formators: (backward compat) # ScoDoc7 output_formators: (backward compat)
d["formsemestre_id"] = self.id d["formsemestre_id"] = self.id
d["titre_num"] = self.titre_num() d["titre_num"] = self.titre_num()
@ -226,6 +229,7 @@ class FormSemestre(db.Model):
""" """
d = dict(self.__dict__) d = dict(self.__dict__)
d.pop("_sa_instance_state", None) d.pop("_sa_instance_state", None)
d.pop("groups_auto_assignment_data", None)
d["annee_scolaire"] = self.annee_scolaire() d["annee_scolaire"] = self.annee_scolaire()
if self.date_debut: if self.date_debut:
d["date_debut"] = self.date_debut.strftime("%d/%m/%Y") d["date_debut"] = self.date_debut.strftime("%d/%m/%Y")
@ -767,6 +771,15 @@ class FormSemestre(db.Model):
etuds.sort(key=lambda e: e.sort_key) etuds.sort(key=lambda e: e.sort_key)
return etuds return etuds
def get_partitions_list(self, with_default=True) -> list[Partition]:
"""Liste des partitions pour ce semestre (list of dicts),
triées par numéro, avec la partition par défaut en fin de liste.
"""
partitions = [p for p in self.partitions if p.partition_name is not None]
if with_default:
partitions += [p for p in self.partitions if p.partition_name is None]
return partitions
@cached_property @cached_property
def etudids_actifs(self) -> set: def etudids_actifs(self) -> set:
"Set des etudids inscrits non démissionnaires et non défaillants" "Set des etudids inscrits non démissionnaires et non défaillants"

View File

@ -12,6 +12,7 @@ from sqlalchemy.exc import IntegrityError
from app import db, log from app import db, log
from app.models import Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN from app.models import Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN
from app.models.etudiants import Identite
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
@ -50,7 +51,7 @@ class Partition(db.Model):
backref=db.backref("partition", lazy=True), backref=db.backref("partition", lazy=True),
lazy="dynamic", lazy="dynamic",
cascade="all, delete-orphan", cascade="all, delete-orphan",
order_by="GroupDescr.numero", order_by="GroupDescr.numero, GroupDescr.group_name",
) )
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -240,6 +241,21 @@ class GroupDescr(db.Model):
d["partition"] = self.partition.to_dict(with_groups=False) d["partition"] = self.partition.to_dict(with_groups=False)
return d return d
def get_nb_inscrits(self) -> int:
"""Nombre inscrits à ce group et au formsemestre.
C'est nécessaire car lors d'une désinscription, on conserve l'appartenance
aux groupes pour facilier une éventuelle -inscription.
"""
from app.models.formsemestre import FormSemestreInscription
return (
Identite.query.join(group_membership)
.filter_by(group_id=self.id)
.join(FormSemestreInscription)
.filter_by(formsemestre_id=self.partition.formsemestre.id)
.count()
)
@classmethod @classmethod
def check_name( def check_name(
cls, partition: "Partition", group_name: str, existing=False, default=False cls, partition: "Partition", group_name: str, existing=False, default=False

View File

@ -219,6 +219,14 @@ class ModuleImplInscription(db.Model):
backref=db.backref("inscriptions", cascade="all, delete-orphan"), backref=db.backref("inscriptions", cascade="all, delete-orphan"),
) )
def to_dict(self) -> dict:
"dict repr."
return {
"id": self.id,
"etudid": self.etudid,
"moduleimpl_id": self.moduleimpl_id,
}
@classmethod @classmethod
def etud_modimpls_in_ue( def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int cls, formsemestre_id: int, etudid: int, ue_id: int

View File

@ -182,8 +182,8 @@ class Module(db.Model):
Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée. Les coefs nuls (zéro) ne sont pas stockés: la relation est supprimée.
""" """
if self.formation.has_locked_sems(self.ue.semestre_idx): if self.formation.has_locked_sems(self.ue.semestre_idx):
current_app.logguer.info( current_app.logger.info(
f"set_ue_coef_dict: locked formation, ignoring request" "set_ue_coef_dict: locked formation, ignoring request"
) )
raise ScoValueError("Formation verrouillée") raise ScoValueError("Formation verrouillée")
changed = False changed = False
@ -213,8 +213,8 @@ class Module(db.Model):
def update_ue_coef_dict(self, ue_coef_dict: dict): def update_ue_coef_dict(self, ue_coef_dict: dict):
"""update coefs vers UE (ajoute aux existants)""" """update coefs vers UE (ajoute aux existants)"""
if self.formation.has_locked_sems(self.ue.semestre_idx): if self.formation.has_locked_sems(self.ue.semestre_idx):
current_app.logguer.info( current_app.logger.info(
f"update_ue_coef_dict: locked formation, ignoring request" "update_ue_coef_dict: locked formation, ignoring request"
) )
raise ScoValueError("Formation verrouillée") raise ScoValueError("Formation verrouillée")
current = self.get_ue_coef_dict() current = self.get_ue_coef_dict()
@ -232,7 +232,7 @@ class Module(db.Model):
def delete_ue_coef(self, ue): def delete_ue_coef(self, ue):
"""delete coef""" """delete coef"""
if self.formation.has_locked_sems(self.ue.semestre_idx): if self.formation.has_locked_sems(self.ue.semestre_idx):
current_app.logguer.info( current_app.logger.info(
"delete_ue_coef: locked formation, ignoring request" "delete_ue_coef: locked formation, ignoring request"
) )
raise ScoValueError("Formation verrouillée") raise ScoValueError("Formation verrouillée")

View File

@ -297,23 +297,23 @@ class GenTable:
"list of titles" "list of titles"
return [self.titles.get(cid, "") for cid in self.columns_ids] return [self.titles.get(cid, "") for cid in self.columns_ids]
def gen(self, format="html", columns_ids=None): def gen(self, fmt="html", columns_ids=None):
"""Build representation of the table in the specified format. """Build representation of the table in the specified format.
See make_page() for more sophisticated output. See make_page() for more sophisticated output.
""" """
if format == "html": if fmt == "html":
return self.html() return self.html()
elif format == "xls" or format == "xlsx": elif fmt == "xls" or fmt == "xlsx":
return self.excel() return self.excel()
elif format == "text" or format == "csv": elif fmt == "text" or fmt == "csv":
return self.text() return self.text()
elif format == "pdf": elif fmt == "pdf":
return self.pdf() return self.pdf()
elif format == "xml": elif fmt == "xml":
return self.xml() return self.xml()
elif format == "json": elif fmt == "json":
return self.json() return self.json()
raise ValueError(f"GenTable: invalid format: {format}") raise ValueError(f"GenTable: invalid format: {fmt}")
def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""): def _gen_html_row(self, row, line_num=0, elem="td", css_classes=""):
"row is a dict, returns a string <tr...>...</tr>" "row is a dict, returns a string <tr...>...</tr>"
@ -477,15 +477,13 @@ class GenTable:
H.append('<span class="gt_export_icons">') H.append('<span class="gt_export_icons">')
if self.xls_link: if self.xls_link:
H.append( H.append(
' <a href="%s&format=xls">%s</a>' ' <a href="%s&fmt=xls">%s</a>' % (self.base_url, scu.ICON_XLS)
% (self.base_url, scu.ICON_XLS)
) )
if self.xls_link and self.pdf_link: if self.xls_link and self.pdf_link:
H.append("&nbsp;") H.append("&nbsp;")
if self.pdf_link: if self.pdf_link:
H.append( H.append(
' <a href="%s&format=pdf">%s</a>' ' <a href="%s&fmt=pdf">%s</a>' % (self.base_url, scu.ICON_PDF)
% (self.base_url, scu.ICON_PDF)
) )
H.append("</span>") H.append("</span>")
H.append("</p>") H.append("</p>")
@ -653,7 +651,7 @@ class GenTable:
def make_page( def make_page(
self, self,
title="", title="",
format="html", fmt="html",
page_title="", page_title="",
filename=None, filename=None,
javascripts=[], javascripts=[],
@ -670,7 +668,7 @@ class GenTable:
filename = self.filename filename = self.filename
page_title = page_title or self.page_title page_title = page_title or self.page_title
html_title = self.html_title or title html_title = self.html_title or title
if format == "html": if fmt == "html":
H = [] H = []
if with_html_headers: if with_html_headers:
H.append( H.append(
@ -687,7 +685,7 @@ class GenTable:
if with_html_headers: if with_html_headers:
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
return "\n".join(H) return "\n".join(H)
elif format == "pdf": elif fmt == "pdf":
pdf_objs = self.pdf() pdf_objs = self.pdf()
pdf_doc = sco_pdf.pdf_basic_page( pdf_doc = sco_pdf.pdf_basic_page(
pdf_objs, title=title, preferences=self.preferences pdf_objs, title=title, preferences=self.preferences
@ -701,7 +699,7 @@ class GenTable:
) )
else: else:
return pdf_doc return pdf_doc
elif format == "xls" or format == "xlsx": # dans les 2 cas retourne du xlsx elif fmt == "xls" or fmt == "xlsx": # dans les 2 cas retourne du xlsx
xls = self.excel() xls = self.excel()
if publish: if publish:
return scu.send_file( return scu.send_file(
@ -712,9 +710,9 @@ class GenTable:
) )
else: else:
return xls return xls
elif format == "text": elif fmt == "text":
return self.text() return self.text()
elif format == "csv": elif fmt == "csv":
return scu.send_file( return scu.send_file(
self.text(), self.text(),
filename, filename,
@ -722,14 +720,14 @@ class GenTable:
mime=scu.CSV_MIMETYPE, mime=scu.CSV_MIMETYPE,
attached=True, attached=True,
) )
elif format == "xml": elif fmt == "xml":
xml = self.xml() xml = self.xml()
if publish: if publish:
return scu.send_file( return scu.send_file(
xml, filename, suffix=".xml", mime=scu.XML_MIMETYPE xml, filename, suffix=".xml", mime=scu.XML_MIMETYPE
) )
return xml return xml
elif format == "json": elif fmt == "json":
js = self.json() js = self.json()
if publish: if publish:
return scu.send_file( return scu.send_file(
@ -737,7 +735,7 @@ class GenTable:
) )
return js return js
else: else:
log("make_page: format=%s" % format) log(f"make_page: format={fmt}")
raise ValueError("_make_page: invalid format") raise ValueError("_make_page: invalid format")
@ -771,19 +769,18 @@ if __name__ == "__main__":
columns_ids=("nom", "age"), columns_ids=("nom", "age"),
) )
print("--- HTML:") print("--- HTML:")
print(table.gen(format="html")) print(table.gen(fmt="html"))
print("\n--- XML:") print("\n--- XML:")
print(table.gen(format="xml")) print(table.gen(fmt="xml"))
print("\n--- JSON:") print("\n--- JSON:")
print(table.gen(format="json")) print(table.gen(fmt="json"))
# Test pdf: # Test pdf:
import io import io
from reportlab.platypus import KeepInFrame from app.scodoc import sco_preferences
from app.scodoc import sco_preferences, sco_pdf
preferences = sco_preferences.SemPreferences() preferences = sco_preferences.SemPreferences()
table.preferences = preferences table.preferences = preferences
objects = table.gen(format="pdf") objects = table.gen(fmt="pdf")
objects = [KeepInFrame(0, 0, objects, mode="shrink")] objects = [KeepInFrame(0, 0, objects, mode="shrink")]
doc = io.BytesIO() doc = io.BytesIO()
document = sco_pdf.BaseDocTemplate(doc) document = sco_pdf.BaseDocTemplate(doc)
@ -796,6 +793,6 @@ if __name__ == "__main__":
data = doc.getvalue() data = doc.getvalue()
with open("/tmp/gen_table.pdf", "wb") as f: with open("/tmp/gen_table.pdf", "wb") as f:
f.write(data) f.write(data)
p = table.make_page(format="pdf") p = table.make_page(fmt="pdf")
with open("toto.pdf", "wb") as f: with open("toto.pdf", "wb") as f:
f.write(p) f.write(p)

View File

@ -58,7 +58,7 @@ def sidebar_common():
] ]
if current_user.has_permission(Permission.ScoAbsChange): if current_user.has_permission(Permission.ScoAbsChange):
H.append( H.append(
f""" <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduités</a> <br> """ f""" <a href="{scu.AssiduitesURL()}" class="sidebar">Assiduité</a> <br> """
) )
if current_user.has_permission( if current_user.has_permission(
Permission.ScoUsersAdmin Permission.ScoUsersAdmin

View File

@ -47,7 +47,6 @@
nommé _description.txt qui est une description (humaine, format libre) de l'archive. nommé _description.txt qui est une description (humaine, format libre) de l'archive.
""" """
from typing import Union
import datetime import datetime
import glob import glob
import json import json
@ -81,7 +80,7 @@ from app.scodoc import sco_pv_pdf
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
class BaseArchiver(object): class BaseArchiver:
def __init__(self, archive_type=""): def __init__(self, archive_type=""):
self.archive_type = archive_type self.archive_type = archive_type
self.initialized = False self.initialized = False
@ -92,14 +91,17 @@ class BaseArchiver(object):
"set dept" "set dept"
self.dept_id = dept_id self.dept_id = dept_id
def initialize(self): def initialize(self, dept_id: int = None):
"""Fixe le département et initialise les répertoires au besoin."""
# Set departement (à chaque fois car peut changer d'une utilisation à l'autre)
self.dept_id = getattr(g, "scodoc_dept_id") if dept_id is None else dept_id
if self.initialized: if self.initialized:
return return
dirs = [Config.SCODOC_VAR_DIR, "archives"] dirs = [Config.SCODOC_VAR_DIR, "archives"]
if self.archive_type: if self.archive_type:
dirs.append(self.archive_type) dirs.append(self.archive_type)
self.root = os.path.join(*dirs) self.root = os.path.join(*dirs) # /opt/scodoc-data/archives/<type>
log("initialized archiver, path=" + self.root) log("initialized archiver, path=" + self.root)
path = dirs[0] path = dirs[0]
for directory in dirs[1:]: for directory in dirs[1:]:
@ -112,15 +114,13 @@ class BaseArchiver(object):
finally: finally:
scu.GSL.release() scu.GSL.release()
self.initialized = True self.initialized = True
if self.dept_id is None:
self.dept_id = getattr(g, "scodoc_dept_id")
def get_obj_dir(self, oid: int): def get_obj_dir(self, oid: int, dept_id: int = None):
""" """
:return: path to directory of archives for this object (eg formsemestre_id or etudid). :return: path to directory of archives for this object (eg formsemestre_id or etudid).
If directory does not yet exist, create it. If directory does not yet exist, create it.
""" """
self.initialize() self.initialize(dept_id)
dept_dir = os.path.join(self.root, str(self.dept_id)) dept_dir = os.path.join(self.root, str(self.dept_id))
try: try:
scu.GSL.acquire() scu.GSL.acquire()
@ -141,21 +141,21 @@ class BaseArchiver(object):
scu.GSL.release() scu.GSL.release()
return obj_dir return obj_dir
def list_oids(self): def list_oids(self, dept_id: int = None):
""" """
:return: list of archive oids :return: list of archive oids
""" """
self.initialize() self.initialize(dept_id)
base = os.path.join(self.root, str(self.dept_id)) + os.path.sep base = os.path.join(self.root, str(self.dept_id)) + os.path.sep
dirs = glob.glob(base + "*") dirs = glob.glob(base + "*")
return [os.path.split(x)[1] for x in dirs] return [os.path.split(x)[1] for x in dirs]
def list_obj_archives(self, oid: int): def list_obj_archives(self, oid: int, dept_id: int = None):
"""Returns """Returns
:return: list of archive identifiers for this object (paths to non empty dirs) :return: list of archive identifiers for this object (paths to non empty dirs)
""" """
self.initialize() self.initialize(dept_id)
base = self.get_obj_dir(oid) + os.path.sep base = self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep
dirs = glob.glob( dirs = glob.glob(
base base
+ "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]" + "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]"
@ -165,9 +165,9 @@ class BaseArchiver(object):
dirs.sort() dirs.sort()
return dirs return dirs
def delete_archive(self, archive_id: str): def delete_archive(self, archive_id: str, dept_id: int = None):
"""Delete (forever) this archive""" """Delete (forever) this archive"""
self.initialize() self.initialize(dept_id)
try: try:
scu.GSL.acquire() scu.GSL.acquire()
shutil.rmtree(archive_id, ignore_errors=True) shutil.rmtree(archive_id, ignore_errors=True)
@ -180,9 +180,9 @@ class BaseArchiver(object):
*[int(x) for x in os.path.split(archive_id)[1].split("-")] *[int(x) for x in os.path.split(archive_id)[1].split("-")]
) )
def list_archive(self, archive_id: str) -> str: def list_archive(self, archive_id: str, dept_id: int = None) -> str:
"""Return list of filenames (without path) in archive""" """Return list of filenames (without path) in archive"""
self.initialize() self.initialize(dept_id)
try: try:
scu.GSL.acquire() scu.GSL.acquire()
files = os.listdir(archive_id) files = os.listdir(archive_id)
@ -201,12 +201,12 @@ class BaseArchiver(object):
"^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name "^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$", archive_name
) )
def get_id_from_name(self, oid, archive_name: str): def get_id_from_name(self, oid, archive_name: str, dept_id: int = None):
"""returns archive id (check that name is valid)""" """returns archive id (check that name is valid)"""
self.initialize() self.initialize(dept_id)
if not self.is_valid_archive_name(archive_name): if not self.is_valid_archive_name(archive_name):
raise ScoValueError(f"Archive {archive_name} introuvable") raise ScoValueError(f"Archive {archive_name} introuvable")
archive_id = os.path.join(self.get_obj_dir(oid), archive_name) archive_id = os.path.join(self.get_obj_dir(oid, dept_id=dept_id), archive_name)
if not os.path.isdir(archive_id): if not os.path.isdir(archive_id):
log( log(
f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}" f"invalid archive name: {archive_name}, oid={oid}, archive_id={archive_id}"
@ -214,9 +214,9 @@ class BaseArchiver(object):
raise ScoValueError(f"Archive {archive_name} introuvable") raise ScoValueError(f"Archive {archive_name} introuvable")
return archive_id return archive_id
def get_archive_description(self, archive_id: str) -> str: def get_archive_description(self, archive_id: str, dept_id: int = None) -> str:
"""Return description of archive""" """Return description of archive"""
self.initialize() self.initialize(dept_id)
filename = os.path.join(archive_id, "_description.txt") filename = os.path.join(archive_id, "_description.txt")
try: try:
with open(filename, encoding=scu.SCO_ENCODING) as f: with open(filename, encoding=scu.SCO_ENCODING) as f:
@ -229,11 +229,11 @@ class BaseArchiver(object):
return descr return descr
def create_obj_archive(self, oid: int, description: str): def create_obj_archive(self, oid: int, description: str, dept_id: int = None):
"""Creates a new archive for this object and returns its id.""" """Creates a new archive for this object and returns its id."""
# id suffixé par YYYY-MM-DD-hh-mm-ss # id suffixé par YYYY-MM-DD-hh-mm-ss
archive_id = ( archive_id = (
self.get_obj_dir(oid) self.get_obj_dir(oid, dept_id=dept_id)
+ os.path.sep + os.path.sep
+ "-".join([f"{x:02d}" for x in time.localtime()[:6]]) + "-".join([f"{x:02d}" for x in time.localtime()[:6]])
) )
@ -248,7 +248,13 @@ class BaseArchiver(object):
self.store(archive_id, "_description.txt", description) self.store(archive_id, "_description.txt", description)
return archive_id return archive_id
def store(self, archive_id: str, filename: str, data: Union[str, bytes]): def store(
self,
archive_id: str,
filename: str,
data: str | bytes,
dept_id: int = None,
):
"""Store data in archive, under given filename. """Store data in archive, under given filename.
Filename may be modified (sanitized): return used filename Filename may be modified (sanitized): return used filename
The file is created or replaced. The file is created or replaced.
@ -256,7 +262,7 @@ class BaseArchiver(object):
""" """
if isinstance(data, str): if isinstance(data, str):
data = data.encode(scu.SCO_ENCODING) data = data.encode(scu.SCO_ENCODING)
self.initialize() self.initialize(dept_id)
filename = scu.sanitize_filename(filename) filename = scu.sanitize_filename(filename)
log(f"storing {filename} ({len(data)} bytes) in {archive_id}") log(f"storing {filename} ({len(data)} bytes) in {archive_id}")
try: try:
@ -264,27 +270,36 @@ class BaseArchiver(object):
fname = os.path.join(archive_id, filename) fname = os.path.join(archive_id, filename)
with open(fname, "wb") as f: with open(fname, "wb") as f:
f.write(data) f.write(data)
except FileNotFoundError as exc:
raise ScoValueError(
f"Erreur stockage archive (dossier inexistant, chemin {fname})"
) from exc
finally: finally:
scu.GSL.release() scu.GSL.release()
return filename return filename
def get(self, archive_id: str, filename: str): def get(self, archive_id: str, filename: str, dept_id: int = None):
"""Retreive data""" """Retreive data"""
self.initialize() self.initialize(dept_id)
if not scu.is_valid_filename(filename): if not scu.is_valid_filename(filename):
log(f"""Archiver.get: invalid filename '{filename}'""") log(f"""Archiver.get: invalid filename '{filename}'""")
raise ScoValueError("archive introuvable (déjà supprimée ?)") raise ScoValueError("archive introuvable (déjà supprimée ?)")
fname = os.path.join(archive_id, filename) fname = os.path.join(archive_id, filename)
log(f"reading archive file {fname}") log(f"reading archive file {fname}")
with open(fname, "rb") as f: try:
data = f.read() with open(fname, "rb") as f:
data = f.read()
except FileNotFoundError as exc:
raise ScoValueError(
f"Erreur lecture archive (inexistant, chemin {fname})"
) from exc
return data return data
def get_archived_file(self, oid, archive_name, filename): def get_archived_file(self, oid, archive_name, filename, dept_id: int = None):
"""Recupère les donnees du fichier indiqué et envoie au client. """Recupère les donnees du fichier indiqué et envoie au client.
Returns: Response Returns: Response
""" """
archive_id = self.get_id_from_name(oid, archive_name) archive_id = self.get_id_from_name(oid, archive_name, dept_id=dept_id)
data = self.get(archive_id, filename) data = self.get(archive_id, filename)
mime = mimetypes.guess_type(filename)[0] mime = mimetypes.guess_type(filename)[0]
if mime is None: if mime is None:
@ -298,7 +313,7 @@ class SemsArchiver(BaseArchiver):
BaseArchiver.__init__(self, archive_type="") BaseArchiver.__init__(self, archive_type="")
PVArchive = SemsArchiver() PV_ARCHIVER = SemsArchiver()
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
@ -332,8 +347,10 @@ def do_formsemestre_archive(
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre) res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem_archive_id = formsemestre_id sem_archive_id = formsemestre_id
archive_id = PVArchive.create_obj_archive(sem_archive_id, description) archive_id = PV_ARCHIVER.create_obj_archive(
date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M") sem_archive_id, description, formsemestre.dept_id
)
date = PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
if not group_ids: if not group_ids:
# tous les inscrits du semestre # tous les inscrits du semestre
@ -347,7 +364,12 @@ def do_formsemestre_archive(
# Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes) # Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes)
data, _ = gen_formsemestre_recapcomplet_excel(res, include_evaluations=True) data, _ = gen_formsemestre_recapcomplet_excel(res, include_evaluations=True)
if data: if data:
PVArchive.store(archive_id, "Tableau_moyennes" + scu.XLSX_SUFFIX, data) PV_ARCHIVER.store(
archive_id,
"Tableau_moyennes" + scu.XLSX_SUFFIX,
data,
dept_id=formsemestre.dept_id,
)
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes) # Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
table_html, _, _ = gen_formsemestre_recapcomplet_html_table( table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
formsemestre, res, include_evaluations=True formsemestre, res, include_evaluations=True
@ -367,33 +389,43 @@ def do_formsemestre_archive(
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
) )
PVArchive.store(archive_id, "Tableau_moyennes.html", data) PV_ARCHIVER.store(
archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id
)
# Bulletins en JSON # Bulletins en JSON
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True) data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder) data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
if data: if data:
PVArchive.store(archive_id, "Bulletins.json", data_js) PV_ARCHIVER.store(
archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id
)
# Décisions de jury, en XLS # Décisions de jury, en XLS
if formsemestre.formation.is_apc(): if formsemestre.formation.is_apc():
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls") response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls")
data = response.get_data() data = response.get_data()
else: # formations classiques else: # formations classiques
data = sco_pv_forms.formsemestre_pvjury( data = sco_pv_forms.formsemestre_pvjury(
formsemestre_id, format="xls", publish=False formsemestre_id, fmt="xls", publish=False
) )
if data: if data:
PVArchive.store( PV_ARCHIVER.store(
archive_id, archive_id,
"Decisions_Jury" + scu.XLSX_SUFFIX, "Decisions_Jury" + scu.XLSX_SUFFIX,
data, data,
dept_id=formsemestre.dept_id,
) )
# Classeur bulletins (PDF) # Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=bul_version formsemestre_id, version=bul_version
) )
if data: if data:
PVArchive.store(archive_id, "Bulletins.pdf", data) PV_ARCHIVER.store(
archive_id,
"Bulletins.pdf",
data,
dept_id=formsemestre.dept_id,
)
# Lettres individuelles (PDF): # Lettres individuelles (PDF):
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles( data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
formsemestre_id, formsemestre_id,
@ -403,7 +435,12 @@ def do_formsemestre_archive(
signature=signature, signature=signature,
) )
if data: if data:
PVArchive.store(archive_id, f"CourriersDecisions{groups_filename}.pdf", data) PV_ARCHIVER.store(
archive_id,
f"CourriersDecisions{groups_filename}.pdf",
data,
dept_id=formsemestre.dept_id,
)
# PV de jury (PDF): # PV de jury (PDF):
data = sco_pv_pdf.pvjury_pdf( data = sco_pv_pdf.pvjury_pdf(
@ -419,7 +456,12 @@ def do_formsemestre_archive(
anonymous=anonymous, anonymous=anonymous,
) )
if data: if data:
PVArchive.store(archive_id, f"PV_Jury{groups_filename}.pdf", data) PV_ARCHIVER.store(
archive_id,
f"PV_Jury{groups_filename}.pdf",
data,
dept_id=formsemestre.dept_id,
)
def formsemestre_archive(formsemestre_id, group_ids: list[int] = None): def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
@ -558,14 +600,21 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
def formsemestre_list_archives(formsemestre_id): def formsemestre_list_archives(formsemestre_id):
"""Page listing archives""" """Page listing archives"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre_id sem_archive_id = formsemestre_id
L = [] L = []
for archive_id in PVArchive.list_obj_archives(sem_archive_id): for archive_id in PV_ARCHIVER.list_obj_archives(
sem_archive_id, dept_id=formsemestre.dept_id
):
a = { a = {
"archive_id": archive_id, "archive_id": archive_id,
"description": PVArchive.get_archive_description(archive_id), "description": PV_ARCHIVER.get_archive_description(
"date": PVArchive.get_archive_date(archive_id), archive_id, dept_id=formsemestre.dept_id
"content": PVArchive.list_archive(archive_id), ),
"date": PV_ARCHIVER.get_archive_date(archive_id),
"content": PV_ARCHIVER.list_archive(
archive_id, dept_id=formsemestre.dept_id
),
} }
L.append(a) L.append(a)
@ -575,7 +624,7 @@ def formsemestre_list_archives(formsemestre_id):
else: else:
H.append("<ul>") H.append("<ul>")
for a in L: for a in L:
archive_name = PVArchive.get_archive_name(a["archive_id"]) archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
H.append( H.append(
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>' '<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
% ( % (
@ -602,7 +651,9 @@ def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
"""Send file to client.""" """Send file to client."""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
sem_archive_id = formsemestre.id sem_archive_id = formsemestre.id
return PVArchive.get_archived_file(sem_archive_id, archive_name, filename) return PV_ARCHIVER.get_archived_file(
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id
)
def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False): def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False):
@ -617,7 +668,9 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=
) )
) )
sem_archive_id = formsemestre_id sem_archive_id = formsemestre_id
archive_id = PVArchive.get_id_from_name(sem_archive_id, archive_name) archive_id = PV_ARCHIVER.get_id_from_name(
sem_archive_id, archive_name, dept_id=formsemestre.dept_id
)
dest_url = url_for( dest_url = url_for(
"notes.formsemestre_list_archives", "notes.formsemestre_list_archives",
@ -628,7 +681,7 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
f"""<h2>Confirmer la suppression de l'archive du { f"""<h2>Confirmer la suppression de l'archive du {
PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M") PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M")
} ?</h2> } ?</h2>
<p>La suppression sera définitive.</p> <p>La suppression sera définitive.</p>
""", """,
@ -640,6 +693,6 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=
}, },
) )
PVArchive.delete_archive(archive_id) PV_ARCHIVER.delete_archive(archive_id, dept_id=formsemestre.dept_id)
flash("Archive supprimée") flash("Archive supprimée")
return flask.redirect(dest_url) return flask.redirect(dest_url)

View File

@ -52,7 +52,8 @@ class EtudsArchiver(sco_archives.BaseArchiver):
sco_archives.BaseArchiver.__init__(self, archive_type="docetuds") sco_archives.BaseArchiver.__init__(self, archive_type="docetuds")
EtudsArchive = EtudsArchiver() # Global au processus, attention !
ETUDS_ARCHIVER = EtudsArchiver()
def can_edit_etud_archive(authuser): def can_edit_etud_archive(authuser):
@ -60,21 +61,21 @@ def can_edit_etud_archive(authuser):
return authuser.has_permission(Permission.ScoEtudAddAnnotations) return authuser.has_permission(Permission.ScoEtudAddAnnotations)
def etud_list_archives_html(etudid): def etud_list_archives_html(etud: Identite):
"""HTML snippet listing archives""" """HTML snippet listing archives"""
can_edit = can_edit_etud_archive(current_user) can_edit = can_edit_etud_archive(current_user)
etuds = sco_etud.get_etud_info(etudid=etudid) etud_archive_id = etud.id
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etudid
L = [] L = []
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): for archive_id in ETUDS_ARCHIVER.list_obj_archives(
etud_archive_id, dept_id=etud.dept_id
):
a = { a = {
"archive_id": archive_id, "archive_id": archive_id,
"description": EtudsArchive.get_archive_description(archive_id), "description": ETUDS_ARCHIVER.get_archive_description(
"date": EtudsArchive.get_archive_date(archive_id), archive_id, dept_id=etud.dept_id
"content": EtudsArchive.list_archive(archive_id), ),
"date": ETUDS_ARCHIVER.get_archive_date(archive_id),
"content": ETUDS_ARCHIVER.list_archive(archive_id, dept_id=etud.dept_id),
} }
L.append(a) L.append(a)
delete_icon = scu.icontag( delete_icon = scu.icontag(
@ -85,7 +86,7 @@ def etud_list_archives_html(etudid):
) )
H = ['<div class="etudarchive"><ul>'] H = ['<div class="etudarchive"><ul>']
for a in L: for a in L:
archive_name = EtudsArchive.get_archive_name(a["archive_id"]) archive_name = ETUDS_ARCHIVER.get_archive_name(a["archive_id"])
H.append( H.append(
"""<li><span class ="etudarchive_descr" title="%s">%s</span>""" """<li><span class ="etudarchive_descr" title="%s">%s</span>"""
% (a["date"].strftime("%d/%m/%Y %H:%M"), a["description"]) % (a["date"].strftime("%d/%m/%Y %H:%M"), a["description"])
@ -93,14 +94,14 @@ def etud_list_archives_html(etudid):
for filename in a["content"]: for filename in a["content"]:
H.append( H.append(
"""<a class="stdlink etudarchive_link" href="etud_get_archived_file?etudid=%s&archive_name=%s&filename=%s">%s</a>""" """<a class="stdlink etudarchive_link" href="etud_get_archived_file?etudid=%s&archive_name=%s&filename=%s">%s</a>"""
% (etudid, archive_name, filename, filename) % (etud.id, archive_name, filename, filename)
) )
if not a["content"]: if not a["content"]:
H.append("<em>aucun fichier !</em>") H.append("<em>aucun fichier !</em>")
if can_edit: if can_edit:
H.append( H.append(
'<span class="deletudarchive"><a class="smallbutton" href="etud_delete_archive?etudid=%s&archive_name=%s">%s</a></span>' '<span class="deletudarchive"><a class="smallbutton" href="etud_delete_archive?etudid=%s&archive_name=%s">%s</a></span>'
% (etudid, archive_name, delete_icon) % (etud.id, archive_name, delete_icon)
) )
else: else:
H.append('<span class="deletudarchive">' + delete_disabled_icon + "</span>") H.append('<span class="deletudarchive">' + delete_disabled_icon + "</span>")
@ -108,7 +109,7 @@ def etud_list_archives_html(etudid):
if can_edit: if can_edit:
H.append( H.append(
'<li class="addetudarchive"><a class="stdlink" href="etud_upload_file_form?etudid=%s">ajouter un fichier</a></li>' '<li class="addetudarchive"><a class="stdlink" href="etud_upload_file_form?etudid=%s">ajouter un fichier</a></li>'
% etudid % etud.id
) )
H.append("</ul></div>") H.append("</ul></div>")
return "".join(H) return "".join(H)
@ -121,12 +122,13 @@ def add_archives_info_to_etud_list(etuds):
for etud in etuds: for etud in etuds:
l = [] l = []
etud_archive_id = etud["etudid"] etud_archive_id = etud["etudid"]
for archive_id in EtudsArchive.list_obj_archives(etud_archive_id): # Here, ETUDS_ARCHIVER will use g.dept_id
for archive_id in ETUDS_ARCHIVER.list_obj_archives(etud_archive_id):
l.append( l.append(
"%s (%s)" "%s (%s)"
% ( % (
EtudsArchive.get_archive_description(archive_id), ETUDS_ARCHIVER.get_archive_description(archive_id),
EtudsArchive.list_archive(archive_id)[0], ETUDS_ARCHIVER.list_archive(archive_id)[0],
) )
) )
etud["etudarchive"] = ", ".join(l) etud["etudarchive"] = ", ".join(l)
@ -197,8 +199,8 @@ def _store_etud_file_to_new_archive(
filesize = len(data) filesize = len(data)
if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE: if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE:
return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})" return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})"
archive_id = EtudsArchive.create_obj_archive(etud_archive_id, description) archive_id = ETUDS_ARCHIVER.create_obj_archive(etud_archive_id, description)
EtudsArchive.store(archive_id, filename, data) ETUDS_ARCHIVER.store(archive_id, filename, data)
return True, "ok" return True, "ok"
@ -212,14 +214,16 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
raise ScoValueError("étudiant inexistant") raise ScoValueError("étudiant inexistant")
etud = etuds[0] etud = etuds[0]
etud_archive_id = etud["etudid"] etud_archive_id = etud["etudid"]
archive_id = EtudsArchive.get_id_from_name(etud_archive_id, archive_name) archive_id = ETUDS_ARCHIVER.get_id_from_name(
etud_archive_id, archive_name, dept_id=etud["dept_id"]
)
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Confirmer la suppression des fichiers ?</h2> """<h2>Confirmer la suppression des fichiers ?</h2>
<p>Fichier associé le %s à l'étudiant %s</p> <p>Fichier associé le %s à l'étudiant %s</p>
<p>La suppression sera définitive.</p>""" <p>La suppression sera définitive.</p>"""
% ( % (
EtudsArchive.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"), ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
etud["nomprenom"], etud["nomprenom"],
), ),
dest_url="", dest_url="",
@ -232,7 +236,7 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
parameters={"etudid": etudid, "archive_name": archive_name}, parameters={"etudid": etudid, "archive_name": archive_name},
) )
EtudsArchive.delete_archive(archive_id) ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud["dept_id"])
flash("Archive supprimée") flash("Archive supprimée")
return flask.redirect( return flask.redirect(
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
@ -246,7 +250,9 @@ def etud_get_archived_file(etudid, archive_name, filename):
raise ScoValueError("étudiant inexistant") raise ScoValueError("étudiant inexistant")
etud = etuds[0] etud = etuds[0]
etud_archive_id = etud["etudid"] etud_archive_id = etud["etudid"]
return EtudsArchive.get_archived_file(etud_archive_id, archive_name, filename) return ETUDS_ARCHIVER.get_archived_file(
etud_archive_id, archive_name, filename, dept_id=etud["dept_id"]
)
# --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants) # --- Upload d'un ensemble de fichiers (pour un groupe d'étudiants)

View File

@ -12,11 +12,14 @@ from app.scodoc.sco_archives import BaseArchiver
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_utils import is_iso_formated from app.scodoc.sco_utils import is_iso_formated
from app import log
class Trace: class Trace:
"""gestionnaire de la trace des fichiers justificatifs""" """gestionnaire de la trace des fichiers justificatifs"""
def __init__(self, path: str) -> None: def __init__(self, path: str) -> None:
log(f"init Trace {path}")
self.path: str = path + "/_trace.csv" self.path: str = path + "/_trace.csv"
self.content: dict[str, list[datetime, datetime, str]] = {} self.content: dict[str, list[datetime, datetime, str]] = {}
self.import_from_file() self.import_from_file()
@ -45,7 +48,7 @@ class Trace:
if fname in modes: if fname in modes:
continue continue
traced: list[datetime, datetime, str] = self.content.get(fname, False) traced: list[datetime, datetime, str] = self.content.get(fname, False)
if not traced: if not traced or mode == "entry":
self.content[fname] = [None, None, None] self.content[fname] = [None, None, None]
traced = self.content[fname] traced = self.content[fname]
@ -102,7 +105,7 @@ class JustificatifArchiver(BaseArchiver):
def save_justificatif( def save_justificatif(
self, self,
etudid: int, etud: Identite,
filename: str, filename: str,
data: bytes or str, data: bytes or str,
archive_name: str = None, archive_name: str = None,
@ -113,17 +116,18 @@ class JustificatifArchiver(BaseArchiver):
Ajoute un fichier dans une archive "justificatif" pour l'etudid donné Ajoute un fichier dans une archive "justificatif" pour l'etudid donné
Retourne l'archive_name utilisé Retourne l'archive_name utilisé
""" """
self._set_dept(etudid)
if archive_name is None: if archive_name is None:
archive_id: str = self.create_obj_archive( archive_id: str = self.create_obj_archive(
oid=etudid, description=description oid=etud.id, description=description, dept_id=etud.dept_id
) )
else: else:
archive_id: str = self.get_id_from_name(etudid, archive_name) archive_id: str = self.get_id_from_name(
etud.id, archive_name, dept_id=etud.dept_id
)
fname: str = self.store(archive_id, filename, data) fname: str = self.store(archive_id, filename, data, dept_id=etud.dept_id)
log(f"obj_dir {self.get_obj_dir(etud.id, dept_id=etud.dept_id)} | {archive_id}")
trace = Trace(self.get_obj_dir(etudid)) trace = Trace(archive_id)
trace.set_trace(fname, mode="entry") trace.set_trace(fname, mode="entry")
if user_id is not None: if user_id is not None:
trace.set_trace(fname, mode="user_id", current_user=user_id) trace.set_trace(fname, mode="user_id", current_user=user_id)
@ -132,7 +136,7 @@ class JustificatifArchiver(BaseArchiver):
def delete_justificatif( def delete_justificatif(
self, self,
etudid: int, etud: Identite,
archive_name: str, archive_name: str,
filename: str = None, filename: str = None,
has_trace: bool = True, has_trace: bool = True,
@ -140,92 +144,84 @@ class JustificatifArchiver(BaseArchiver):
""" """
Supprime une archive ou un fichier particulier de l'archivage de l'étudiant donné 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 Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s)
dans la trace de l'étudiant
""" """
self._set_dept(etudid) if str(etud.id) not in self.list_oids(etud.dept_id):
if str(etudid) not in self.list_oids(): raise ValueError(f"Aucune archive pour etudid[{etud.id}]")
raise ValueError(f"Aucune archive pour etudid[{etudid}]")
archive_id = self.get_id_from_name(etudid, archive_name) archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id)
if filename is not None: if filename is not None:
if filename not in self.list_archive(archive_id): if filename not in self.list_archive(archive_id, dept_id=etud.dept_id):
raise ValueError( raise ValueError(
f"filename {filename} inconnu dans l'archive archive_id[{archive_id}] -> etudid[{etudid}]" f"""filename {filename} inconnu dans l'archive archive_id[{
archive_id}] -> etudid[{etud.id}]"""
) )
path: str = os.path.join(self.get_obj_dir(etudid), archive_id, filename) path: str = os.path.join(
self.get_obj_dir(etud.id, dept_id=etud.dept_id), archive_id, filename
)
if os.path.isfile(path): if os.path.isfile(path):
if has_trace: if has_trace:
trace = Trace(self.get_obj_dir(etudid)) trace = Trace(archive_id)
trace.set_trace(filename, mode="delete") trace.set_trace(filename, mode="delete")
os.remove(path) os.remove(path)
else: else:
if has_trace: if has_trace:
trace = Trace(self.get_obj_dir(etudid)) trace = Trace(archive_id)
trace.set_trace(*self.list_archive(archive_id), mode="delete") trace.set_trace(
*self.list_archive(archive_id, dept_id=etud.dept_id), mode="delete"
)
self.delete_archive( self.delete_archive(
os.path.join( os.path.join(
self.get_obj_dir(etudid), self.get_obj_dir(etud.id, dept_id=etud.dept_id),
archive_id, archive_id,
) )
) )
def list_justificatifs( def list_justificatifs(
self, archive_name: str, etudid: int self, archive_name: str, etud: Identite
) -> list[tuple[str, int]]: ) -> list[tuple[str, int]]:
""" """
Retourne la liste des noms de fichiers dans l'archive donnée Retourne la liste des noms de fichiers dans l'archive donnée
""" """
self._set_dept(etudid)
filenames: list[str] = [] filenames: list[str] = []
archive_id = self.get_id_from_name(etudid, archive_name) archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id)
filenames = self.list_archive(archive_id) filenames = self.list_archive(archive_id, dept_id=etud.dept_id)
trace: Trace = Trace(self.get_obj_dir(etudid)) trace: Trace = Trace(archive_id)
traced = trace.get_trace(filenames) traced = trace.get_trace(filenames)
retour = [(key, value[2]) for key, value in traced.items()] retour = [(key, value[2]) for key, value in traced.items()]
return retour return retour
def get_justificatif_file(self, archive_name: str, etudid: int, filename: str): def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str):
""" """
Retourne une réponse de téléchargement de fichier si le fichier existe Retourne une réponse de téléchargement de fichier si le fichier existe
""" """
self._set_dept(etudid) archive_id: str = self.get_id_from_name(
archive_id: str = self.get_id_from_name(etudid, archive_name) etud.id, archive_name, dept_id=etud.dept_id
if filename in self.list_archive(archive_id): )
return self.get_archived_file(etudid, archive_name, filename) if filename in self.list_archive(archive_id, dept_id=etud.dept_id):
return self.get_archived_file(
etud.id, archive_name, filename, dept_id=etud.dept_id
)
raise ScoValueError( raise ScoValueError(
f"Fichier {filename} introuvable dans l'archive {archive_name}" f"Fichier {filename} introuvable dans l'archive {archive_name}"
) )
def _set_dept(self, etudid: int):
"""
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)
def remove_dept_archive(self, dept_id: int = None): def remove_dept_archive(self, dept_id: int = None):
""" """
Supprime toutes les archives d'un département (ou de tous les départements) Supprime toutes les archives d'un département (ou de tous les départements)
Supprime aussi les fichiers de trace Supprime aussi les fichiers de trace
""" """
self.set_dept_id(1) # juste pour récupérer .root, dept_id n'a pas d'importance
self.initialize() self.initialize(dept_id=1)
if dept_id is None: if dept_id is None:
rmtree(self.root, ignore_errors=True) rmtree(self.root, ignore_errors=True)
else: else:
rmtree(os.path.join(self.root, str(dept_id)), ignore_errors=True) 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

@ -4,9 +4,9 @@ Ecrit par Matthias Hartmann.
from datetime import date, datetime, time, timedelta from datetime import date, datetime, time, timedelta
from pytz import UTC from pytz import UTC
from app import log from app import log, db
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.models.assiduites import Assiduite, Justificatif from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.formsemestre import FormSemestre, FormSemestreInscription from app.models.formsemestre import FormSemestre, FormSemestreInscription
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
@ -141,12 +141,9 @@ class CountCalculator:
self.hours += finish_hours.total_seconds() / 3600 self.hours += finish_hours.total_seconds() / 3600
self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600) self.hours += self.hour_per_day - (start_hours.total_seconds() / 3600)
def compute_assiduites(self, assiduites: Assiduite): def compute_assiduites(self, assiduites: Query | list):
"""Calcule les métriques pour la collection d'assiduité donnée""" """Calcule les métriques pour la collection d'assiduité donnée"""
assi: Assiduite assi: Assiduite
assiduites: list[Assiduite] = (
assiduites.all() if isinstance(assiduites, Assiduite) else assiduites
)
for assi in assiduites: for assi in assiduites:
self.count += 1 self.count += 1
delta: timedelta = assi.date_fin - assi.date_debut delta: timedelta = assi.date_fin - assi.date_debut
@ -167,7 +164,7 @@ class CountCalculator:
self.hours += delta.total_seconds() / 3600 self.hours += delta.total_seconds() / 3600
def to_dict(self) -> dict[str, int or float]: def to_dict(self) -> dict[str, int | float]:
"""Retourne les métriques sous la forme d'un dictionnaire""" """Retourne les métriques sous la forme d'un dictionnaire"""
return { return {
"compte": self.count, "compte": self.count,
@ -179,7 +176,7 @@ class CountCalculator:
def get_assiduites_stats( def get_assiduites_stats(
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
) -> dict[str, int or float]: ) -> dict[str, int | float]:
"""Compte les assiduités en fonction des filtres""" """Compte les assiduités en fonction des filtres"""
if filtered is not None: if filtered is not None:
@ -212,7 +209,7 @@ def get_assiduites_stats(
output: dict = {} output: dict = {}
calculator: CountCalculator = CountCalculator() calculator: CountCalculator = CountCalculator()
if "split" not in filtered: if filtered is None or "split" not in filtered:
calculator.compute_assiduites(assiduites) calculator.compute_assiduites(assiduites)
count: dict = calculator.to_dict() count: dict = calculator.to_dict()
@ -276,7 +273,7 @@ def filter_assiduites_by_est_just(assiduites: Assiduite, est_just: bool) -> Quer
def filter_by_user_id( def filter_by_user_id(
collection: Assiduite or Justificatif, collection: Assiduite | Justificatif,
user_id: int, user_id: int,
) -> Query: ) -> Query:
""" """
@ -286,8 +283,8 @@ def filter_by_user_id(
def filter_by_date( def filter_by_date(
collection: Assiduite or Justificatif, collection: Assiduite | Justificatif,
collection_cls: Assiduite or Justificatif, collection_cls: Assiduite | Justificatif,
date_deb: datetime = None, date_deb: datetime = None,
date_fin: datetime = None, date_fin: datetime = None,
strict: bool = False, strict: bool = False,
@ -311,7 +308,7 @@ def filter_by_date(
) )
def filter_justificatifs_by_etat(justificatifs: Justificatif, etat: str) -> Query: def filter_justificatifs_by_etat(justificatifs: Query, etat: str) -> Query:
""" """
Filtrage d'une collection de justificatifs en fonction de leur état Filtrage d'une collection de justificatifs en fonction de leur état
""" """
@ -320,7 +317,7 @@ def filter_justificatifs_by_etat(justificatifs: Justificatif, etat: str) -> Quer
return justificatifs.filter(Justificatif.etat.in_(etats)) return justificatifs.filter(Justificatif.etat.in_(etats))
def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int or None) -> Query: def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int | None) -> Query:
""" """
Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl Filtrage d'une collection d'assiduites en fonction de l'ID du module_impl
""" """
@ -328,8 +325,8 @@ def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int or None) ->
def filter_by_formsemestre( def filter_by_formsemestre(
collection_query: Assiduite or Justificatif, collection_query: Assiduite | Justificatif,
collection_class: Assiduite or Justificatif, collection_class: Assiduite | Justificatif,
formsemestre: FormSemestre, formsemestre: FormSemestre,
) -> Query: ) -> Query:
""" """
@ -358,7 +355,7 @@ def filter_by_formsemestre(
return collection_result.filter(collection_class.date_fin <= form_date_fin) return collection_result.filter(collection_class.date_fin <= form_date_fin)
def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query: def justifies(justi: Justificatif, obj: bool = False) -> list[int] | Query:
""" """
Retourne la liste des assiduite_id qui sont justifié par la justification Retourne la liste des assiduite_id qui sont justifié par la justification
Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT Une assiduité est justifiée si elle est COMPLETEMENT ou PARTIELLEMENT
@ -382,7 +379,10 @@ def justifies(justi: Justificatif, obj: bool = False) -> list[int] or Query:
def get_all_justified( def get_all_justified(
etudid: int, date_deb: datetime = None, date_fin: datetime = None etudid: int,
date_deb: datetime = None,
date_fin: datetime = None,
moduleimpl_id: int = None,
) -> Query: ) -> Query:
"""Retourne toutes les assiduités justifiées sur une période""" """Retourne toutes les assiduités justifiées sur une période"""
@ -393,7 +393,9 @@ def get_all_justified(
date_deb = scu.localize_datetime(date_deb) date_deb = scu.localize_datetime(date_deb)
date_fin = scu.localize_datetime(date_fin) date_fin = scu.localize_datetime(date_fin)
justified = Assiduite.query.filter_by(est_just=True, etudid=etudid) justified: Query = Assiduite.query.filter_by(est_just=True, etudid=etudid)
if moduleimpl_id is not None:
justified = justified.filter_by(moduleimpl_id=moduleimpl_id)
after = filter_by_date( after = filter_by_date(
justified, justified,
Assiduite, Assiduite,
@ -403,6 +405,42 @@ def get_all_justified(
return after return after
def create_absence(
date_debut: datetime,
date_fin: datetime,
etudid: int,
description: str = None,
est_just: bool = False,
) -> int:
etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404()
assiduite_unique: Assiduite = Assiduite.create_assiduite(
etud=etud,
date_debut=date_debut,
date_fin=date_fin,
etat=scu.EtatAssiduite.ABSENT,
description=description,
)
db.session.add(assiduite_unique)
db.session.commit()
if est_just:
justi = Justificatif.create_justificatif(
etud=etud,
date_debut=date_debut,
date_fin=date_fin,
etat=scu.EtatJustificatif.VALIDE,
raison=description,
)
db.session.add(justi)
db.session.commit()
compute_assiduites_justified(etud.id, [justi])
calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites([assiduite_unique])
return calculator.to_dict()["demi"]
# Gestion du cache # Gestion du cache
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]: def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre: """Les comptes d'absences de cet étudiant dans ce semestre:
@ -419,7 +457,7 @@ def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
def formsemestre_get_assiduites_count( def formsemestre_get_assiduites_count(
etudid: int, formsemestre: FormSemestre etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
) -> tuple[int, int]: ) -> tuple[int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre: """Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées) tuple (nb abs non justifiées, nb abs justifiées)
@ -428,9 +466,14 @@ def formsemestre_get_assiduites_count(
metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id) metrique = sco_preferences.get_preference("assi_metrique", formsemestre.id)
return get_assiduites_count_in_interval( return get_assiduites_count_in_interval(
etudid, etudid,
date_debut=formsemestre.date_debut, date_debut=scu.localize_datetime(
date_fin=formsemestre.date_fin, datetime.combine(formsemestre.date_debut, time(8, 0))
),
date_fin=scu.localize_datetime(
datetime.combine(formsemestre.date_fin, time(18, 0))
),
metrique=scu.translate_assiduites_metric(metrique), metrique=scu.translate_assiduites_metric(metrique),
moduleimpl_id=moduleimpl_id,
) )
@ -441,6 +484,7 @@ def get_assiduites_count_in_interval(
metrique="demi", metrique="demi",
date_debut: datetime = None, date_debut: datetime = None,
date_fin: datetime = None, date_fin: datetime = None,
moduleimpl_id: int = None,
): ):
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses: """Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs, nb abs justifiées) tuple (nb abs, nb abs justifiées)
@ -452,33 +496,39 @@ def get_assiduites_count_in_interval(
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites" key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites"
r = sco_cache.AbsSemEtudCache.get(key) r = sco_cache.AbsSemEtudCache.get(key)
if not r: if not r or moduleimpl_id is not None:
date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso) date_debut: datetime = date_debut or datetime.fromisoformat(date_debut_iso)
date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso) date_fin: datetime = date_fin or datetime.fromisoformat(date_fin_iso)
assiduites: Assiduite = Assiduite.query.filter_by(etudid=etudid) assiduites: Query = Assiduite.query.filter_by(etudid=etudid)
assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT) assiduites = assiduites.filter(Assiduite.etat == scu.EtatAssiduite.ABSENT)
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid) justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
assiduites = filter_by_date(assiduites, Assiduite, date_debut, date_fin) assiduites = filter_by_date(assiduites, Assiduite, date_debut, date_fin)
if moduleimpl_id is not None:
assiduites = assiduites.filter_by(moduleimpl_id=moduleimpl_id)
justificatifs = filter_by_date( justificatifs = filter_by_date(
justificatifs, Justificatif, date_debut, date_fin justificatifs, Justificatif, date_debut, date_fin
) )
calculator: CountCalculator = CountCalculator() calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites(assiduites) calculator.compute_assiduites(assiduites)
nb_abs: dict = calculator.to_dict()[metrique] nb_abs: dict = calculator.to_dict()[metrique]
abs_just: list[Assiduite] = get_all_justified(etudid, date_debut, date_fin) abs_just: list[Assiduite] = get_all_justified(
etudid, date_debut, date_fin, moduleimpl_id
)
calculator.reset() calculator.reset()
calculator.compute_assiduites(abs_just) calculator.compute_assiduites(abs_just)
nb_abs_just: dict = calculator.to_dict()[metrique] nb_abs_just: dict = calculator.to_dict()[metrique]
r = (nb_abs, nb_abs_just) r = (nb_abs, nb_abs_just)
ans = sco_cache.AbsSemEtudCache.set(key, r) if moduleimpl_id is None:
if not ans: ans = sco_cache.AbsSemEtudCache.set(key, r)
log("warning: get_assiduites_count failed to cache") if not ans:
log("warning: get_assiduites_count failed to cache")
return r return r
@ -544,7 +594,7 @@ def invalidate_assiduites_etud_date(etudid, date: datetime):
invalidate_assiduites_count(etudid, sem) invalidate_assiduites_count(etudid, sem)
def simple_invalidate_cache(obj: dict, etudid: str or int = None): def simple_invalidate_cache(obj: dict, etudid: str | int = None):
"""Invalide le cache de l'étudiant et du / des semestres""" """Invalide le cache de l'étudiant et du / des semestres"""
date_debut = ( date_debut = (
obj["date_debut"] obj["date_debut"]

View File

@ -95,7 +95,7 @@ def get_formsemestre_bulletin_etud_json(
return formsemestre_bulletinetud( return formsemestre_bulletinetud(
etud, etud,
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
format="json", fmt="json",
version=version, version=version,
xml_with_decisions=True, xml_with_decisions=True,
force_publishing=force_publishing, force_publishing=force_publishing,
@ -201,7 +201,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
infos, dpv = etud_descr_situation_semestre( infos, dpv = etud_descr_situation_semestre(
etudid, etudid,
formsemestre, formsemestre,
format="html", fmt="html",
show_date_inscr=prefs["bul_show_date_inscr"], show_date_inscr=prefs["bul_show_date_inscr"],
show_decisions=prefs["bul_show_decision"], show_decisions=prefs["bul_show_decision"],
show_uevalid=prefs["bul_show_uevalid"], show_uevalid=prefs["bul_show_uevalid"],
@ -582,7 +582,7 @@ def _ue_mod_bulletin(
"notes.evaluation_listenotes", "notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
evaluation_id=e.id, evaluation_id=e.id,
format="html", fmt="html",
tf_submitted=1, tf_submitted=1,
) )
e_dict[ e_dict[
@ -679,14 +679,14 @@ def etud_descr_situation_semestre(
etudid, etudid,
formsemestre: FormSemestre, formsemestre: FormSemestre,
ne="", ne="",
format="html", # currently unused fmt="html", # currently unused
show_decisions=True, show_decisions=True,
show_uevalid=True, show_uevalid=True,
show_date_inscr=True, show_date_inscr=True,
show_mention=False, show_mention=False,
): ):
"""Dict décrivant la situation de l'étudiant dans ce semestre. """Dict décrivant la situation de l'étudiant dans ce semestre.
Si format == 'html', peut inclure du balisage html (actuellement inutilisé) Si fmt == 'html', peut inclure du balisage html (actuellement inutilisé)
situation : chaine résumant en français la situation de l'étudiant. situation : chaine résumant en français la situation de l'étudiant.
Par ex. "Inscrit le 31/12/1999. Décision jury: Validé. ..." Par ex. "Inscrit le 31/12/1999. Décision jury: Validé. ..."
@ -889,7 +889,7 @@ def _format_situation_fields(
def formsemestre_bulletinetud( def formsemestre_bulletinetud(
etud: Identite = None, etud: Identite = None,
formsemestre_id=None, formsemestre_id=None,
format=None, fmt=None,
version="long", version="long",
xml_with_decisions=False, xml_with_decisions=False,
force_publishing=False, # force publication meme si semestre non publie sur "portail" force_publishing=False, # force publication meme si semestre non publie sur "portail"
@ -910,7 +910,7 @@ def formsemestre_bulletinetud(
- prefer_mail_perso: pour pdfmail, utilise adresse mail perso en priorité. - prefer_mail_perso: pour pdfmail, utilise adresse mail perso en priorité.
""" """
format = format or "html" fmt = fmt or "html"
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
if not formsemestre: if not formsemestre:
raise ScoValueError(f"semestre {formsemestre_id} inconnu !") raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
@ -918,21 +918,21 @@ def formsemestre_bulletinetud(
bulletin = do_formsemestre_bulletinetud( bulletin = do_formsemestre_bulletinetud(
formsemestre, formsemestre,
etud, etud,
format=format, fmt=fmt,
version=version, version=version,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
prefer_mail_perso=prefer_mail_perso, prefer_mail_perso=prefer_mail_perso,
)[0] )[0]
if format not in {"html", "pdfmail"}: if fmt not in {"html", "pdfmail"}:
filename = scu.bul_filename(formsemestre, etud) filename = scu.bul_filename(formsemestre, etud)
mime, suffix = scu.get_mime_suffix(format) mime, suffix = scu.get_mime_suffix(fmt)
return scu.send_file(bulletin, filename, mime=mime, suffix=suffix) return scu.send_file(bulletin, filename, mime=mime, suffix=suffix)
elif format == "pdfmail": elif fmt == "pdfmail":
return "" return ""
H = [ H = [
_formsemestre_bulletinetud_header_html(etud, formsemestre, format, version), _formsemestre_bulletinetud_header_html(etud, formsemestre, fmt, version),
bulletin, bulletin,
render_template( render_template(
"bul_foot.j2", "bul_foot.j2",
@ -963,7 +963,7 @@ def do_formsemestre_bulletinetud(
formsemestre: FormSemestre, formsemestre: FormSemestre,
etud: Identite, etud: Identite,
version="long", # short, long, selectedevals version="long", # short, long, selectedevals
format=None, fmt=None,
xml_with_decisions: bool = False, xml_with_decisions: bool = False,
force_publishing: bool = False, force_publishing: bool = False,
prefer_mail_perso: bool = False, prefer_mail_perso: bool = False,
@ -985,8 +985,8 @@ def do_formsemestre_bulletinetud(
bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json) bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json)
et filigranne est un message à placer en "filigranne" (eg "Provisoire"). et filigranne est un message à placer en "filigranne" (eg "Provisoire").
""" """
format = format or "html" fmt = fmt or "html"
if format == "xml": if fmt == "xml":
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud( bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
formsemestre.id, formsemestre.id,
etud.id, etud.id,
@ -997,7 +997,7 @@ def do_formsemestre_bulletinetud(
return bul, "" return bul, ""
elif format == "json": # utilisé pour classic et "oldjson" elif fmt == "json": # utilisé pour classic et "oldjson"
bul = sco_bulletins_json.make_json_formsemestre_bulletinetud( bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
formsemestre.id, formsemestre.id,
etud.id, etud.id,
@ -1015,23 +1015,23 @@ def do_formsemestre_bulletinetud(
else: else:
bul_dict = formsemestre_bulletinetud_dict(formsemestre.id, etud.id) bul_dict = formsemestre_bulletinetud_dict(formsemestre.id, etud.id)
if format == "html": if fmt == "html":
htm, _ = sco_bulletins_generator.make_formsemestre_bulletin_etud( htm, _ = sco_bulletins_generator.make_formsemestre_bulletin_etud(
bul_dict, etud=etud, formsemestre=formsemestre, version=version, fmt="html" bul_dict, etud=etud, formsemestre=formsemestre, version=version, fmt="html"
) )
return htm, bul_dict["filigranne"] return htm, bul_dict["filigranne"]
elif format == "pdf" or format == "pdfpart": if fmt == "pdf" or fmt == "pdfpart":
bul, filename = sco_bulletins_generator.make_formsemestre_bulletin_etud( bul, filename = sco_bulletins_generator.make_formsemestre_bulletin_etud(
bul_dict, bul_dict,
etud=etud, etud=etud,
formsemestre=formsemestre, formsemestre=formsemestre,
version=version, version=version,
fmt="pdf", fmt="pdf",
stand_alone=(format != "pdfpart"), stand_alone=(fmt != "pdfpart"),
with_img_signatures_pdf=with_img_signatures_pdf, with_img_signatures_pdf=with_img_signatures_pdf,
) )
if format == "pdf": if fmt == "pdf":
return ( return (
scu.sendPDFFile(bul, filename), scu.sendPDFFile(bul, filename),
bul_dict["filigranne"], bul_dict["filigranne"],
@ -1039,7 +1039,7 @@ def do_formsemestre_bulletinetud(
else: else:
return bul, bul_dict["filigranne"] return bul, bul_dict["filigranne"]
elif format == "pdfmail": elif fmt == "pdfmail":
# format pdfmail: envoie le pdf par mail a l'etud, et affiche le html # format pdfmail: envoie le pdf par mail a l'etud, et affiche le html
# check permission # check permission
if not can_send_bulletin_by_mail(formsemestre.id): if not can_send_bulletin_by_mail(formsemestre.id):
@ -1067,7 +1067,7 @@ def do_formsemestre_bulletinetud(
return True, bul_dict["filigranne"] return True, bul_dict["filigranne"]
raise ValueError(f"do_formsemestre_bulletinetud: invalid format ({format})") raise ValueError(f"do_formsemestre_bulletinetud: invalid format ({fmt})")
def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr): def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
@ -1097,10 +1097,12 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
hea = "" hea = ""
if sco_preferences.get_preference("bul_mail_list_abs"): if sco_preferences.get_preference("bul_mail_list_abs"):
hea += "\n\n" + "(LISTE D'ABSENCES NON DISPONIBLE)" # XXX TODO-ASSIDUITE from app.views.assiduites import generate_bul_list
# sco_abs_views.ListeAbsEtud(
# etud["etudid"], with_evals=False, format="text" etud_identite: Identite = Identite.get_etud(etud["etudid"])
# ) form_semestre: FormSemestre = FormSemestre.get_formsemestre(formsemestre_id)
hea += "\n\n"
hea += generate_bul_list(etud_identite, form_semestre)
subject = f"""Relevé de notes de {etud["nomprenom"]}""" subject = f"""Relevé de notes de {etud["nomprenom"]}"""
recipients = [recipient_addr] recipients = [recipient_addr]
@ -1154,7 +1156,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etudid": etud.id, "etudid": etud.id,
"version": version, "version": version,
"format": "pdf", "fmt": "pdf",
}, },
}, },
{ {
@ -1164,7 +1166,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etudid": etud.id, "etudid": etud.id,
"version": version, "version": version,
"format": "pdfmail", "fmt": "pdfmail",
}, },
# possible slt si on a un mail... # possible slt si on a un mail...
"enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id), "enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id),
@ -1176,7 +1178,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etudid": etud.id, "etudid": etud.id,
"version": version, "version": version,
"format": "pdfmail", "fmt": "pdfmail",
"prefer_mail_perso": 1, "prefer_mail_perso": 1,
}, },
# possible slt si on a un mail... # possible slt si on a un mail...
@ -1189,7 +1191,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etudid": etud.id, "etudid": etud.id,
"version": version, "version": version,
"format": "json", "fmt": "json",
}, },
}, },
{ {
@ -1199,7 +1201,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etudid": etud.id, "etudid": etud.id,
"version": version, "version": version,
"format": "xml", "fmt": "xml",
}, },
}, },
{ {
@ -1267,7 +1269,7 @@ def make_menu_autres_operations(
def _formsemestre_bulletinetud_header_html( def _formsemestre_bulletinetud_header_html(
etud, etud,
formsemestre: FormSemestre, formsemestre: FormSemestre,
format=None, fmt=None,
version=None, version=None,
): ):
H = [ H = [
@ -1283,7 +1285,7 @@ def _formsemestre_bulletinetud_header_html(
render_template( render_template(
"bul_head.j2", "bul_head.j2",
etud=etud, etud=etud,
format=format, fmt=fmt,
formsemestre=formsemestre, formsemestre=formsemestre,
menu_autres_operations=make_menu_autres_operations( menu_autres_operations=make_menu_autres_operations(
etud=etud, etud=etud,

View File

@ -35,7 +35,7 @@ class BulletinGenerator:
.bul_part_below(fmt) .bul_part_below(fmt)
.bul_signatures_pdf() .bul_signatures_pdf()
.__init__ et .generate(format) methodes appelees par le client (sco_bulletin) .__init__ et .generate(fmt) methodes appelees par le client (sco_bulletin)
La préférence 'bul_class_name' donne le nom de la classe generateur. La préférence 'bul_class_name' donne le nom de la classe generateur.
La préférence 'bul_pdf_class_name' est obsolete (inutilisée). La préférence 'bul_pdf_class_name' est obsolete (inutilisée).
@ -62,7 +62,7 @@ from reportlab.platypus import Table, TableStyle, Image, KeepInFrame
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite, ScoDocSiteConfig
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import NoteProcessError from app.scodoc.sco_exceptions import NoteProcessError
from app import log from app import log
@ -139,18 +139,18 @@ class BulletinGenerator:
sem = sco_formsemestre.get_formsemestre(self.bul_dict["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(self.bul_dict["formsemestre_id"])
return scu.bul_filename_old(sem, self.bul_dict["etud"], "pdf") return scu.bul_filename_old(sem, self.bul_dict["etud"], "pdf")
def generate(self, format="", stand_alone=True): def generate(self, fmt="", stand_alone=True):
"""Return bulletin in specified format""" """Return bulletin in specified format"""
if not format in self.supported_formats: if not fmt in self.supported_formats:
raise ValueError(f"unsupported bulletin format ({format})") raise ValueError(f"unsupported bulletin format ({fmt})")
try: try:
PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant
if format == "html": if fmt == "html":
return self.generate_html() return self.generate_html()
elif format == "pdf": elif fmt == "pdf":
return self.generate_pdf(stand_alone=stand_alone) return self.generate_pdf(stand_alone=stand_alone)
else: else:
raise ValueError(f"invalid bulletin format ({format})") raise ValueError(f"invalid bulletin format ({fmt})")
finally: finally:
PDFLOCK.release() PDFLOCK.release()
@ -197,6 +197,10 @@ class BulletinGenerator:
else: else:
# Insere notre marqueur qui permet de générer les bookmarks et filigrannes: # Insere notre marqueur qui permet de générer les bookmarks et filigrannes:
story.insert(index_obj_debut, marque_debut_bulletin) story.insert(index_obj_debut, marque_debut_bulletin)
if ScoDocSiteConfig.is_bul_pdf_disabled():
story = [Paragraph("<p>Export des PDF interdit par l'administrateur</p>")]
# #
# objects.append(sco_pdf.FinBulletin()) # objects.append(sco_pdf.FinBulletin())
if not stand_alone: if not stand_alone:
@ -288,8 +292,10 @@ def make_formsemestre_bulletin_etud(
): ):
if bul_dict.get("type") == "BUT" and fmt.startswith("pdf"): if bul_dict.get("type") == "BUT" and fmt.startswith("pdf"):
gen_class = bulletin_get_class(bul_class_name + "BUT") gen_class = bulletin_get_class(bul_class_name + "BUT")
if gen_class is None: if gen_class is None and bul_dict.get("type") != "BUT":
gen_class = bulletin_get_class(bul_class_name) gen_class = bulletin_get_class(bul_class_name)
if gen_class is not None:
break
if gen_class is None: if gen_class is None:
raise ValueError(f"Type de bulletin PDF invalide (paramètre: {bul_class_name})") raise ValueError(f"Type de bulletin PDF invalide (paramètre: {bul_class_name})")
@ -324,7 +330,7 @@ def make_formsemestre_bulletin_etud(
version=version, version=version,
with_img_signatures_pdf=with_img_signatures_pdf, with_img_signatures_pdf=with_img_signatures_pdf,
) )
data = bul_generator.generate(format=fmt, stand_alone=stand_alone) data = bul_generator.generate(fmt=fmt, stand_alone=stand_alone)
finally: finally:
PDFLOCK.release() PDFLOCK.release()

View File

@ -405,6 +405,7 @@ def dict_decision_jury(
"""dict avec decision pour bulletins json """dict avec decision pour bulletins json
- autorisation_inscription - autorisation_inscription
- decision : décision semestre - decision : décision semestre
- decision_annee : annee BUT
- decision_ue : list des décisions UE - decision_ue : list des décisions UE
- situation - situation

View File

@ -252,7 +252,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
elif fmt == "html": elif fmt == "html":
return self.bul_part_below_html() return self.bul_part_below_html()
else: else:
raise ValueError("invalid bulletin format (%s)" % fmt) raise ValueError(f"invalid bulletin format ({fmt})")
def bul_part_below_pdf(self): def bul_part_below_pdf(self):
""" """

View File

@ -146,15 +146,15 @@ def process_field(
field, cdict, style, suppress_empty_pars=False, fmt="pdf", field_name=None field, cdict, style, suppress_empty_pars=False, fmt="pdf", field_name=None
): ):
"""Process a field given in preferences, returns """Process a field given in preferences, returns
- if format = 'pdf': a list of Platypus objects - if fmt = 'pdf': a list of Platypus objects
- if format = 'html' : a string - if fmt = 'html' : a string
Substitutes all %()s markup Substitutes all %()s markup
Remove potentialy harmful <img> tags Remove potentialy harmful <img> tags
Replaces <logo name="header" width="xxx" height="xxx"> Replaces <logo name="header" width="xxx" height="xxx">
by <img src=".../logos/logo_header" width="xxx" height="xxx"> by <img src=".../logos/logo_header" width="xxx" height="xxx">
If format = 'html', replaces <para> by <p>. HTML does not allow logos. If fmt = 'html', replaces <para> by <p>. HTML does not allow logos.
""" """
try: try:
# None values are mapped to empty strings by WrapDict # None values are mapped to empty strings by WrapDict
@ -225,7 +225,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
frag, _ = sco_bulletins.do_formsemestre_bulletinetud( frag, _ = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre, formsemestre,
etud, etud,
format="pdfpart", fmt="pdfpart",
version=version, version=version,
) )
fragments += frag fragments += frag
@ -270,7 +270,7 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud( frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre, formsemestre,
etud, etud,
format="pdfpart", fmt="pdfpart",
version=version, version=version,
) )
fragments += frag fragments += frag

View File

@ -116,7 +116,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
html_with_td_classes=True, html_with_td_classes=True,
) )
return T.gen(format=fmt) return T.gen(fmt=fmt)
def bul_part_below(self, fmt="html"): def bul_part_below(self, fmt="html"):
"""Génère les informations placées sous la table de notes """Génère les informations placées sous la table de notes

View File

@ -357,7 +357,7 @@ def make_xml_formsemestre_bulletinetud(
infos, dpv = sco_bulletins.etud_descr_situation_semestre( infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etudid, etudid,
formsemestre, formsemestre,
format="xml", fmt="xml",
show_uevalid=sco_preferences.get_preference( show_uevalid=sco_preferences.get_preference(
"bul_show_uevalid", formsemestre_id "bul_show_uevalid", formsemestre_id
), ),

View File

@ -152,7 +152,7 @@ def formsemestre_estim_cost(
n_group_tp=1, n_group_tp=1,
coef_tp=1, coef_tp=1,
coef_cours=1.5, coef_cours=1.5,
format="html", fmt="html",
): ):
"""Page (formulaire) estimation coûts""" """Page (formulaire) estimation coûts"""
@ -192,4 +192,4 @@ def formsemestre_estim_cost(
coef_tp, coef_tp,
) )
return tab.make_page(format=format) return tab.make_page(fmt=fmt)

View File

@ -49,7 +49,7 @@ from app.scodoc import sco_etud
import sco_version import sco_version
def report_debouche_date(start_year=None, format="html"): def report_debouche_date(start_year=None, fmt="html"):
"""Rapport (table) pour les débouchés des étudiants sortis """Rapport (table) pour les débouchés des étudiants sortis
à partir de l'année indiquée. à partir de l'année indiquée.
""" """
@ -63,7 +63,7 @@ def report_debouche_date(start_year=None, format="html"):
"Année invalide. Année de début de la recherche" "Année invalide. Année de début de la recherche"
) )
if format == "xls": if fmt == "xls":
keep_numeric = True # pas de conversion des notes en strings keep_numeric = True # pas de conversion des notes en strings
else: else:
keep_numeric = False keep_numeric = False
@ -81,7 +81,7 @@ def report_debouche_date(start_year=None, format="html"):
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""", title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js"], javascripts=["js/etud_info.js"],
format=format, fmt=fmt,
with_html_headers=True, with_html_headers=True,
) )
@ -276,7 +276,7 @@ def itemsuivi_suppress(itemsuivi_id):
return ("", 204) return ("", 204)
def itemsuivi_create(etudid, item_date=None, situation="", format=None): def itemsuivi_create(etudid, item_date=None, situation="", fmt=None):
"""Creation d'un item""" """Creation d'un item"""
if not sco_permissions_check.can_edit_suivi(): if not sco_permissions_check.can_edit_suivi():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
@ -287,7 +287,7 @@ def itemsuivi_create(etudid, item_date=None, situation="", format=None):
logdb(cnx, method="itemsuivi_create", etudid=etudid) logdb(cnx, method="itemsuivi_create", etudid=etudid)
log("created itemsuivi %s for %s" % (itemsuivi_id, etudid)) log("created itemsuivi %s for %s" % (itemsuivi_id, etudid))
item = itemsuivi_get(cnx, itemsuivi_id) item = itemsuivi_get(cnx, itemsuivi_id)
if format == "json": if fmt == "json":
return scu.sendJSON(item) return scu.sendJSON(item)
return item return item
@ -320,13 +320,13 @@ def itemsuivi_set_situation(object, value):
return situation or scu.IT_SITUATION_MISSING_STR return situation or scu.IT_SITUATION_MISSING_STR
def itemsuivi_list_etud(etudid, format=None): def itemsuivi_list_etud(etudid, fmt=None):
"""Liste des items pour cet étudiant, avec tags""" """Liste des items pour cet étudiant, avec tags"""
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
items = _itemsuivi_list(cnx, {"etudid": etudid}) items = _itemsuivi_list(cnx, {"etudid": etudid})
for it in items: for it in items:
it["tags"] = ", ".join(itemsuivi_tag_list(it["itemsuivi_id"])) it["tags"] = ", ".join(itemsuivi_tag_list(it["itemsuivi_id"]))
if format == "json": if fmt == "json":
return scu.sendJSON(items) return scu.sendJSON(items)
return items return items

View File

@ -979,18 +979,18 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</li> </li>
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept, url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='xml') formation_id=formation_id, fmt='xml')
}">Export XML de la formation</a> ou }">Export XML de la formation</a> ou
<a class="stdlink" href="{ <a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept, url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='xml', export_codes_apo=0) formation_id=formation_id, fmt='xml', export_codes_apo=0)
}">sans codes Apogée</a> }">sans codes Apogée</a>
(permet de l'enregistrer pour l'échanger avec un autre site) (permet de l'enregistrer pour l'échanger avec un autre site)
</li> </li>
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept, url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, format='json') formation_id=formation_id, fmt='json')
}">Export JSON de la formation</a> }">Export JSON de la formation</a>
</li> </li>

View File

@ -85,7 +85,7 @@ class ApoCSVArchiver(sco_archives.BaseArchiver):
sco_archives.BaseArchiver.__init__(self, archive_type="apo_csv") sco_archives.BaseArchiver.__init__(self, archive_type="apo_csv")
ApoCSVArchive = ApoCSVArchiver() APO_CSV_ARCHIVER = ApoCSVArchiver()
# def get_sem_apo_archive(formsemestre_id): # def get_sem_apo_archive(formsemestre_id):
@ -126,9 +126,9 @@ def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
oid = f"{annee_scolaire}-{sem_id}" oid = f"{annee_scolaire}-{sem_id}"
description = f"""{str(apo_data.etape)};{annee_scolaire};{sem_id}""" description = f"""{str(apo_data.etape)};{annee_scolaire};{sem_id}"""
archive_id = ApoCSVArchive.create_obj_archive(oid, description) archive_id = APO_CSV_ARCHIVER.create_obj_archive(oid, description)
csv_data_bytes = csv_data.encode(sco_apogee_reader.APO_OUTPUT_ENCODING) csv_data_bytes = csv_data.encode(sco_apogee_reader.APO_OUTPUT_ENCODING)
ApoCSVArchive.store(archive_id, filename, csv_data_bytes) APO_CSV_ARCHIVER.store(archive_id, filename, csv_data_bytes)
return apo_data.etape return apo_data.etape
@ -138,7 +138,7 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None):
:return: list of informations about stored CSV :return: list of informations about stored CSV
[ { } ] [ { } ]
""" """
oids = ApoCSVArchive.list_oids() # [ '2016-1', ... ] oids = APO_CSV_ARCHIVER.list_oids() # [ '2016-1', ... ]
# filter # filter
if annee_scolaire: if annee_scolaire:
e = re.compile(str(annee_scolaire) + "-.+") e = re.compile(str(annee_scolaire) + "-.+")
@ -149,9 +149,9 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None):
infos = [] # liste d'infos infos = [] # liste d'infos
for oid in oids: for oid in oids:
archive_ids = ApoCSVArchive.list_obj_archives(oid) archive_ids = APO_CSV_ARCHIVER.list_obj_archives(oid)
for archive_id in archive_ids: for archive_id in archive_ids:
description = ApoCSVArchive.get_archive_description(archive_id) description = APO_CSV_ARCHIVER.get_archive_description(archive_id)
fs = tuple(description.split(";")) fs = tuple(description.split(";"))
if len(fs) == 3: if len(fs) == 3:
arch_etape_apo, arch_annee_scolaire, arch_sem_id = fs arch_etape_apo, arch_annee_scolaire, arch_sem_id = fs
@ -165,7 +165,7 @@ def apo_csv_list_stored_archives(annee_scolaire=None, sem_id=None, etapes=None):
"annee_scolaire": int(arch_annee_scolaire), "annee_scolaire": int(arch_annee_scolaire),
"sem_id": int(arch_sem_id), "sem_id": int(arch_sem_id),
"etape_apo": arch_etape_apo, # qui contient éventuellement le VDI "etape_apo": arch_etape_apo, # qui contient éventuellement le VDI
"date": ApoCSVArchive.get_archive_date(archive_id), "date": APO_CSV_ARCHIVER.get_archive_date(archive_id),
} }
) )
infos.sort(key=lambda x: x["etape_apo"]) infos.sort(key=lambda x: x["etape_apo"])
@ -185,7 +185,7 @@ def apo_csv_list_stored_etapes(annee_scolaire, sem_id=None, etapes=None):
def apo_csv_delete(archive_id): def apo_csv_delete(archive_id):
"""Delete archived CSV""" """Delete archived CSV"""
ApoCSVArchive.delete_archive(archive_id) APO_CSV_ARCHIVER.delete_archive(archive_id)
def apo_csv_get_archive(etape_apo, annee_scolaire="", sem_id=""): def apo_csv_get_archive(etape_apo, annee_scolaire="", sem_id=""):
@ -209,7 +209,7 @@ def apo_csv_get(etape_apo="", annee_scolaire="", sem_id="") -> str:
"Etape %s non enregistree (%s, %s)" % (etape_apo, annee_scolaire, sem_id) "Etape %s non enregistree (%s, %s)" % (etape_apo, annee_scolaire, sem_id)
) )
archive_id = info["archive_id"] archive_id = info["archive_id"]
data = ApoCSVArchive.get(archive_id, etape_apo + ".csv") data = APO_CSV_ARCHIVER.get(archive_id, etape_apo + ".csv")
# ce fichier a été archivé donc généré par ScoDoc # ce fichier a été archivé donc généré par ScoDoc
# son encodage est donc APO_OUTPUT_ENCODING # son encodage est donc APO_OUTPUT_ENCODING
return data.decode(sco_apogee_reader.APO_OUTPUT_ENCODING) return data.decode(sco_apogee_reader.APO_OUTPUT_ENCODING)

View File

@ -495,7 +495,7 @@ def table_apo_csv_list(semset):
return tab return tab
def view_apo_etuds(semset_id, title="", nip_list="", format="html"): def view_apo_etuds(semset_id, title="", nip_list="", fmt="html"):
"""Table des étudiants Apogée par nips """Table des étudiants Apogée par nips
nip_list est une chaine, codes nip séparés par des , nip_list est une chaine, codes nip séparés par des ,
""" """
@ -530,11 +530,11 @@ def view_apo_etuds(semset_id, title="", nip_list="", format="html"):
title=title, title=title,
etuds=list(etuds.values()), etuds=list(etuds.values()),
keys=("nip", "etape_apo", "nom", "prenom", "inscriptions_scodoc"), keys=("nip", "etape_apo", "nom", "prenom", "inscriptions_scodoc"),
format=format, fmt=fmt,
) )
def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"): def view_scodoc_etuds(semset_id, title="", nip_list="", fmt="html"):
"""Table des étudiants ScoDoc par nips ou etudids""" """Table des étudiants ScoDoc par nips ou etudids"""
if not isinstance(nip_list, str): if not isinstance(nip_list, str):
nip_list = str(nip_list) nip_list = str(nip_list)
@ -553,12 +553,12 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"):
title=title, title=title,
etuds=etuds, etuds=etuds,
keys=("code_nip", "nom", "prenom"), keys=("code_nip", "nom", "prenom"),
format=format, fmt=fmt,
) )
def _view_etuds_page( def _view_etuds_page(
semset_id: int, title="", etuds: list = None, keys=(), format="html" semset_id: int, title="", etuds: list = None, keys=(), fmt="html"
) -> str: ) -> str:
"Affiche les étudiants indiqués" "Affiche les étudiants indiqués"
# Tri les étudiants par nom: # Tri les étudiants par nom:
@ -581,8 +581,8 @@ def _view_etuds_page(
filename="students_apo", filename="students_apo",
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
if format != "html": if fmt != "html":
return tab.make_page(format=format) return tab.make_page(fmt=fmt)
return f""" return f"""
{html_sco_header.sco_header( {html_sco_header.sco_header(
@ -711,9 +711,9 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
return flask.redirect(dest_url) return flask.redirect(dest_url)
def view_apo_csv(etape_apo="", semset_id="", format="html"): def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
"""Visualise une maquette stockée """Visualise une maquette stockée
Si format="raw", renvoie le fichier maquette tel quel Si fmt="raw", renvoie le fichier maquette tel quel
""" """
if not semset_id: if not semset_id:
raise ValueError("invalid null semset_id") raise ValueError("invalid null semset_id")
@ -721,7 +721,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
annee_scolaire = semset["annee_scolaire"] annee_scolaire = semset["annee_scolaire"]
sem_id = semset["sem_id"] sem_id = semset["sem_id"]
csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id) csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id)
if format == "raw": if fmt == "raw":
return scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE) return scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"]) apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
@ -798,8 +798,8 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
if format != "html": if fmt != "html":
return tab.make_page(format=format) return tab.make_page(fmt=fmt)
H += [ H += [
f""" f"""
@ -807,7 +807,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
<p><a class="stdlink" href="{ <p><a class="stdlink" href="{
url_for("notes.view_apo_csv", url_for("notes.view_apo_csv",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
etape_apo=etape_apo, semset_id=semset_id, format="raw") etape_apo=etape_apo, semset_id=semset_id, fmt="raw")
}">fichier maquette CSV brut (non rempli par ScoDoc)</a> }">fichier maquette CSV brut (non rempli par ScoDoc)</a>
</p> </p>
<div> <div>

View File

@ -668,7 +668,7 @@ class EtapeBilan:
self.titres, self.titres,
html_class="repartition", html_class="repartition",
html_with_td_classes=True, html_with_td_classes=True,
).gen(format="html") ).gen(fmt="html")
) )
return "\n".join(H) return "\n".join(H)
@ -766,7 +766,7 @@ class EtapeBilan:
table_id="detail", table_id="detail",
html_class="table_leftalign", html_class="table_leftalign",
html_sortable=True, html_sortable=True,
).gen(format="html") ).gen(fmt="html")
) )
return "\n".join(H) return "\n".join(H)

View File

@ -30,16 +30,17 @@
from flask import url_for, g from flask import url_for, g
from app import db from app import db
from app.models import Evaluation, FormSemestre, Identite from app.models import Evaluation, FormSemestre, Identite, Assiduite
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_groups from app.scodoc import sco_groups
from flask_sqlalchemy.query import Query
from sqlalchemy import or_, and_
# XXX TODO-ASSIDUITE https://scodoc.org/git/ScoDoc/ScoDoc/issues/685
def evaluation_check_absences(evaluation: Evaluation): def evaluation_check_absences(evaluation: Evaluation):
"""Vérifie les absences au moment de cette évaluation. """Vérifie les absences au moment de cette évaluation.
Cas incohérents que l'on peut rencontrer pour chaque étudiant: Cas incohérents que l'on peut rencontrer pour chaque étudiant:
@ -50,28 +51,30 @@ def evaluation_check_absences(evaluation: Evaluation):
EXC et pas justifie EXC et pas justifie
Ramene 5 listes d'etudid Ramene 5 listes d'etudid
""" """
raise ScoValueError("Fonction non disponible, patience !") # XXX TODO-ASSIDUITE if not evaluation.date_debut or not evaluation.date_fin:
if not evaluation.date_debut:
return [], [], [], [], [] # evaluation sans date return [], [], [], [], [] # evaluation sans date
am, pm = evaluation.is_matin(), evaluation.is_apresmidi() etudids = [
etudid
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
evaluation.id, getallstudents=True
)
]
# Liste les absences à ce moment: deb, fin = scu.localize_datetime(evaluation.date_debut), scu.localize_datetime(
absences = sco_abs.list_abs_jour(evaluation.date_debut, am=am, pm=pm) evaluation.date_fin
abs_etudids = set([x["etudid"] for x in absences]) # ensemble des etudiants absents
abs_non_just = sco_abs.list_abs_non_just_jour(
evaluation.date_debut.date(), am=am, pm=pm
) )
abs_nj_etudids = set(
[x["etudid"] for x in abs_non_just] assiduites: Query = Assiduite.query.filter(
) # ensemble des etudiants absents non justifies Assiduite.etudid.in_(etudids),
justifs = sco_abs.list_abs_jour( Assiduite.etat == scu.EtatAssiduite.ABSENT,
evaluation.date_debut.date(), am=am, pm=pm, is_abs=None, is_just=True fin >= Assiduite.date_debut,
deb <= Assiduite.date_fin,
) )
just_etudids = set(
[x["etudid"] for x in justifs] abs_etudids = set(assi.etudid for assi in assiduites)
) # ensemble des etudiants avec justif abs_nj_etudids = set(assi.etudid for assi in assiduites if assi.est_just is False)
just_etudids = set(assi.etudid for assi in assiduites if assi.est_just is True)
# Les notes: # Les notes:
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id) notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation.id)
@ -80,9 +83,7 @@ def evaluation_check_absences(evaluation: Evaluation):
ExcNonSignalee = [] # note EXC mais pas noté absent ExcNonSignalee = [] # note EXC mais pas noté absent
ExcNonJust = [] # note EXC mais absent non justifie ExcNonJust = [] # note EXC mais absent non justifie
AbsButExc = [] # note ABS mais justifié AbsButExc = [] # note ABS mais justifié
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups( for etudid in etudids:
evaluation.id, getallstudents=True
):
if etudid in notes_db: if etudid in notes_db:
val = notes_db[etudid]["value"] val = notes_db[etudid]["value"]
if ( if (
@ -108,9 +109,10 @@ def evaluation_check_absences(evaluation: Evaluation):
return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc return ValButAbs, AbsNonSignalee, ExcNonSignalee, ExcNonJust, AbsButExc
def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True): def evaluation_check_absences_html(
evaluation: Evaluation, with_header=True, show_ok=True
):
"""Affiche état vérification absences d'une évaluation""" """Affiche état vérification absences d'une évaluation"""
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
am, pm = evaluation.is_matin(), evaluation.is_apresmidi() am, pm = evaluation.is_matin(), evaluation.is_apresmidi()
# 1 si matin, 0 si apres midi, 2 si toute la journee: # 1 si matin, 0 si apres midi, 2 si toute la journee:
match am, pm: match am, pm:
@ -169,14 +171,10 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True
) )
if linkabs: if linkabs:
url = url_for( url = url_for(
"absences.doSignaleAbsence", # XXX TODO-ASSIDUITE "assiduites.signal_evaluation_abs",
scodoc_dept=g.scodoc_dept,
etudid=etudid, etudid=etudid,
# par defaut signale le jour du début de l'éval evaluation_id=evaluation.id,
datedebut=evaluation.date_debut.strftime("%d/%m/%Y"), scodoc_dept=g.scodoc_dept,
datefin=evaluation.date_debut.strftime("%d/%m/%Y"),
demijournee=demijournee,
moduleimpl_id=evaluation.moduleimpl_id,
) )
H.append( H.append(
f"""<a class="stdlink" href="{url}">signaler cette absence</a>""" f"""<a class="stdlink" href="{url}">signaler cette absence</a>"""
@ -249,7 +247,7 @@ def formsemestre_check_absences_html(formsemestre_id):
): ):
H.append( H.append(
evaluation_check_absences_html( evaluation_check_absences_html(
evaluation.id, # XXX TODO-ASSIDUITE remplacer par evaluation ... evaluation,
with_header=False, with_header=False,
show_ok=False, show_ok=False,
) )

View File

@ -46,7 +46,6 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences from app.scodoc import sco_preferences

View File

@ -561,7 +561,7 @@ def evaluation_date_first_completion(evaluation_id) -> datetime.datetime:
return max(date_premiere_note.values()) return max(date_premiere_note.values())
def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"): def formsemestre_evaluations_delai_correction(formsemestre_id, fmt="html"):
"""Experimental: un tableau indiquant pour chaque évaluation """Experimental: un tableau indiquant pour chaque évaluation
le nombre de jours avant la publication des notes. le nombre de jours avant la publication des notes.
@ -638,7 +638,7 @@ def formsemestre_evaluations_delai_correction(formsemestre_id, format="html"):
origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""", origin=f"""Généré par {sco_version.SCONAME} le {scu.timedate_human_repr()}""",
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()), filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
) )
return tab.make_page(format=format) return tab.make_page(fmt=fmt)
# -------------- VIEWS # -------------- VIEWS

View File

@ -220,7 +220,7 @@ def get_set_formsemestre_id_dates(start_date, end_date) -> set:
def scodoc_table_results( def scodoc_table_results(
start_date="", end_date="", types_parcours: list = None, format="html" start_date="", end_date="", types_parcours: list = None, fmt="html"
): ):
"""Page affichant la table des résultats """Page affichant la table des résultats
Les dates sont en dd/mm/yyyy (datepicker javascript) Les dates sont en dd/mm/yyyy (datepicker javascript)
@ -248,8 +248,8 @@ def scodoc_table_results(
end_date, end_date,
"&types_parcours=".join([str(x) for x in types_parcours]), "&types_parcours=".join([str(x) for x in types_parcours]),
) )
if format != "html": if fmt != "html":
return tab.make_page(format=format, with_html_headers=False) return tab.make_page(fmt=fmt, with_html_headers=False)
tab_html = tab.html() tab_html = tab.html()
nb_rows = tab.get_nb_rows() nb_rows = tab.get_nb_rows()
else: else:

View File

@ -366,7 +366,7 @@ def table_etud_in_accessible_depts(expnom=None):
) )
def search_inscr_etud_by_nip(code_nip, format="json"): def search_inscr_etud_by_nip(code_nip, fmt="json"):
"""Recherche multi-departement d'un étudiant par son code NIP """Recherche multi-departement d'un étudiant par son code NIP
Seuls les départements accessibles par l'utilisateur sont cherchés. Seuls les départements accessibles par l'utilisateur sont cherchés.
@ -408,4 +408,4 @@ def search_inscr_etud_by_nip(code_nip, format="json"):
) )
tab = GenTable(columns_ids=columns_ids, rows=T) tab = GenTable(columns_ids=columns_ids, rows=T)
return tab.make_page(format=format, with_html_headers=False, publish=True) return tab.make_page(fmt=fmt, with_html_headers=False, publish=True)

View File

@ -45,7 +45,7 @@ import app.scodoc.sco_utils as scu
# ---- Table recap formation # ---- Table recap formation
def formation_table_recap(formation_id, format="html") -> Response: def formation_table_recap(formation_id, fmt="html") -> Response:
"""Table recapitulant formation.""" """Table recapitulant formation."""
T = [] T = []
formation = Formation.query.get_or_404(formation_id) formation = Formation.query.get_or_404(formation_id)
@ -162,7 +162,7 @@ def formation_table_recap(formation_id, format="html") -> Response:
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
table_id="formation_table_recap", table_id="formation_table_recap",
) )
return tab.make_page(format=format, javascripts=["js/formation_recap.js"]) return tab.make_page(fmt=fmt, javascripts=["js/formation_recap.js"])
def export_recap_formations_annee_scolaire(annee_scolaire): def export_recap_formations_annee_scolaire(annee_scolaire):
@ -179,7 +179,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire):
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres} formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
for formation_id in formation_ids: for formation_id in formation_ids:
formation = db.session.get(Formation, formation_id) formation = db.session.get(Formation, formation_id)
xls = formation_table_recap(formation_id, format="xlsx").data xls = formation_table_recap(formation_id, fmt="xlsx").data
filename = ( filename = (
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
) )

View File

@ -212,7 +212,7 @@ def formation_export(
export_tags=True, export_tags=True,
export_external_ues=False, export_external_ues=False,
export_codes_apo=True, export_codes_apo=True,
format=None, fmt=None,
) -> flask.Response: ) -> flask.Response:
"""Get a formation, with UE, matieres, modules """Get a formation, with UE, matieres, modules
in desired format in desired format
@ -224,13 +224,13 @@ def formation_export(
export_tags=export_tags, export_tags=export_tags,
export_external_ues=export_external_ues, export_external_ues=export_external_ues,
export_codes_apo=export_codes_apo, export_codes_apo=export_codes_apo,
ac_as_list=format == "xml", ac_as_list=fmt == "xml",
) )
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}" filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
return scu.sendResult( return scu.sendResult(
f_dict, f_dict,
name="formation", name="formation",
format=format, fmt=fmt,
force_outer_xml_tag=False, force_outer_xml_tag=False,
attached=True, attached=True,
filename=filename, filename=filename,
@ -283,7 +283,7 @@ def _formation_retreive_apc_niveau(
def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False): def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
"""Create a formation from XML representation """Create a formation from XML representation
(format dumped by formation_export( format='xml' )) (format dumped by formation_export( fmt='xml' ))
XML may contain object (UE, modules) ids: this function returns two XML may contain object (UE, modules) ids: this function returns two
dicts mapping these ids to the created ids. dicts mapping these ids to the created ids.
@ -627,7 +627,7 @@ def formation_create_new_version(formation_id, redirect=True):
"duplicate formation, with new version number" "duplicate formation, with new version number"
formation = Formation.query.get_or_404(formation_id) formation = Formation.query.get_or_404(formation_id)
resp = formation_export( resp = formation_export(
formation_id, export_ids=True, export_external_ues=True, format="xml" formation_id, export_ids=True, export_external_ues=True, fmt="xml"
) )
xml_data = resp.get_data(as_text=True) xml_data = resp.get_data(as_text=True)
new_id, modules_old2new, ues_old2new = formation_import_xml( new_id, modules_old2new, ues_old2new = formation_import_xml(

View File

@ -559,7 +559,7 @@ def list_formsemestre_by_etape(etape_apo=False, annee_scolaire=False) -> list[di
return sems return sems
def view_formsemestre_by_etape(etape_apo=None, format="html"): def view_formsemestre_by_etape(etape_apo=None, fmt="html"):
"""Affiche table des semestres correspondants à l'étape""" """Affiche table des semestres correspondants à l'étape"""
if etape_apo: if etape_apo:
html_title = f"""<h2>Semestres courants de l'étape <tt>{etape_apo}</tt></h2>""" html_title = f"""<h2>Semestres courants de l'étape <tt>{etape_apo}</tt></h2>"""
@ -575,7 +575,7 @@ def view_formsemestre_by_etape(etape_apo=None, format="html"):
</form>""", </form>""",
) )
tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "") tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "")
return tab.make_page(format=format) return tab.make_page(fmt=fmt)
def sem_has_etape(sem, code_etape): def sem_has_etape(sem, code_etape):

View File

@ -171,6 +171,13 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"enabled": True, "enabled": True,
"helpmsg": "Tableau de bord du semestre", "helpmsg": "Tableau de bord du semestre",
}, },
# {
# "title": "Assiduité du semestre",
# "endpoint": "assiduites.liste_assiduites_formsemestre",
# "args": {"formsemestre_id": formsemestre_id},
# "enabled": True,
# "helpmsg": "Tableau de l'assiduité et des justificatifs du semestre",
# },
{ {
"title": f"Voir la formation {formation.acronyme} (v{formation.version})", "title": f"Voir la formation {formation.acronyme} (v{formation.version})",
"endpoint": "notes.ue_table", "endpoint": "notes.ue_table",
@ -218,14 +225,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"enabled": True, "enabled": True,
"helpmsg": "", "helpmsg": "",
}, },
# TODO: Mettre à jour avec module Assiduités
# {
# "title": "Vérifier absences aux évaluations",
# "endpoint": "notes.formsemestre_check_absences_html",
# "args": {"formsemestre_id": formsemestre_id},
# "enabled": True,
# "helpmsg": "",
# },
{ {
"title": "Lister tous les enseignants", "title": "Lister tous les enseignants",
"endpoint": "notes.formsemestre_enseignants_list", "endpoint": "notes.formsemestre_enseignants_list",
@ -326,7 +325,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"title": "Exporter table des étudiants", "title": "Exporter table des étudiants",
"endpoint": "scolar.groups_view", "endpoint": "scolar.groups_view",
"args": { "args": {
"format": "allxls", "fmt": "allxls",
"group_ids": sco_groups.get_default_group( "group_ids": sco_groups.get_default_group(
formsemestre_id, fix_if_missing=True formsemestre_id, fix_if_missing=True
), ),
@ -448,7 +447,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"title": "Documents archivés", "title": "Documents archivés",
"endpoint": "notes.formsemestre_list_archives", "endpoint": "notes.formsemestre_list_archives",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
"enabled": sco_archives.PVArchive.list_obj_archives(formsemestre_id), "enabled": sco_archives.PV_ARCHIVER.list_obj_archives(formsemestre_id),
}, },
] ]
@ -503,13 +502,10 @@ def retreive_formsemestre_from_request() -> int:
group = sco_groups.get_group(args["group_id"]) group = sco_groups.get_group(args["group_id"])
formsemestre_id = group["formsemestre_id"] formsemestre_id = group["formsemestre_id"]
elif group_ids: elif group_ids:
if group_ids: if isinstance(group_ids, str):
if isinstance(group_ids, str): group_ids = group_ids.split(",")
group_id = group_ids group_id = group_ids[0]
else: group = sco_groups.get_group(group_id)
# prend le semestre du 1er groupe de la liste:
group_id = group_ids[0]
group = sco_groups.get_group(group_id)
formsemestre_id = group["formsemestre_id"] formsemestre_id = group["formsemestre_id"]
elif "partition_id" in args: elif "partition_id" in args:
partition = sco_groups.get_partition(args["partition_id"]) partition = sco_groups.get_partition(args["partition_id"])
@ -788,7 +784,7 @@ def formsemestre_description_table(
def formsemestre_description( def formsemestre_description(
formsemestre_id, format="html", with_evals=False, with_parcours=False formsemestre_id, fmt="html", with_evals=False, with_parcours=False
): ):
"""Description du semestre sous forme de table exportable """Description du semestre sous forme de table exportable
Liste des modules et de leurs coefficients Liste des modules et de leurs coefficients
@ -808,112 +804,124 @@ def formsemestre_description(
>indiquer les parcours BUT</input> >indiquer les parcours BUT</input>
""" """
return tab.make_page(format=format) return tab.make_page(fmt=fmt)
# genere liste html pour accès aux groupes de ce semestre # genere liste html pour accès aux groupes de ce semestre
def _make_listes_sem(formsemestre: FormSemestre, with_absences=True): def _make_listes_sem(formsemestre: FormSemestre) -> str:
# construit l'URL "destination" """La section avec les groupes et l'assiduité"""
# (a laquelle on revient apres saisie absences)
destination = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
#
H = [] H = []
# pas de menu absences si pas autorise: # pas de menu absences si pas autorise:
if with_absences and not current_user.has_permission(Permission.ScoAbsChange): can_edit_abs = current_user.has_permission(Permission.ScoAbsChange)
with_absences = False
# #
H.append( H.append(
f"""<h3>Listes de {formsemestre.titre} f"""<h3>Groupes et absences de {formsemestre.titre}
<span class="infostitresem">({formsemestre.mois_debut()} - {formsemestre.mois_fin()})</span></h3>""" <span class="infostitresem">({
formsemestre.mois_debut()} - {formsemestre.mois_fin()
})</span></h3>"""
) )
weekday = datetime.datetime.today().weekday()
try:
if with_absences:
form_abs_tmpl = f"""
<td>
<a class="btn" href="{
url_for("assiduites.visu_assi_group", scodoc_dept=g.scodoc_dept)
}?group_ids=%(group_id)s&date_debut={formsemestre.date_debut.isoformat()}&date_fin={formsemestre.date_fin.isoformat()}"><button>Visualiser l'assiduité</button></a>
"""
form_abs_tmpl += f"""
<a class="btn" href="{
url_for("assiduites.signal_assiduites_group", scodoc_dept=g.scodoc_dept)
}?group_ids=%(group_id)s&jour={
datetime.date.today().isoformat()
}&formsemestre_id={formsemestre.id}"><button>Saisie journalière</button></a>
<a class="btn" href="{
url_for("assiduites.signal_assiduites_diff", scodoc_dept=g.scodoc_dept)
}?group_ids=%(group_id)s&formsemestre_id={
formsemestre.formsemestre_id
}"><button>Saisie différée</button></a>
</td>
"""
else:
form_abs_tmpl = f"""
<td>
<a class="btn" href="{
url_for("assiduites.visu_assiduites_group", scodoc_dept=g.scodoc_dept)
}?group_ids=%(group_id)s&jour={datetime.date.today().isoformat()}&formsemestre_id={formsemestre.id}"><button>Voir l'assiduité</button></a>
</td>
"""
except ScoInvalidDateError: # dates incorrectes dans semestres ?
form_abs_tmpl = ""
# #
H.append('<div id="grouplists">') H.append('<div class="sem-groups-abs">')
# Genere liste pour chaque partition (categorie de groupes) # Genere liste pour chaque partition (categorie de groupes)
for partition in sco_groups.get_partitions_list(formsemestre.id): for partition in formsemestre.get_partitions_list():
if not partition["partition_name"]: groups = partition.groups.all()
H.append("<h4>Tous les étudiants</h4>") effectifs = {g.id: g.get_nb_inscrits() for g in groups}
else: partition_is_empty = sum(effectifs.values()) == 0
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition) H.append(
partition_is_empty = True f"""
groups = sco_groups.get_partition_groups(partition) <div class="sem-groups-partition">
<div class="sem-groups-partition-titre">{
'Groupes de ' + partition.partition_name
if partition.partition_name else
'Tous les étudiants'}
</div>
<div class="sem-groups-partition-titre">{
"Gestion de l'assiduité" if not partition_is_empty else ""
}</div>
"""
)
if groups: if groups:
H.append("<table>")
for group in groups: for group in groups:
n_members = len(sco_groups.get_group_members(group["group_id"])) n_members = effectifs[group.id]
if n_members == 0: if n_members == 0:
continue # skip empty groups continue # skip empty groups
partition_is_empty = False partition_is_empty = False
group["url_etat"] = url_for( group_label = f"{group.group_name}" if group.group_name else "liste"
"assiduites.visu_assi_group",
scodoc_dept=g.scodoc_dept,
group_ids=group["id"],
date_debut=formsemestre.date_debut.isoformat(),
date_fin=formsemestre.date_fin.isoformat(),
)
if group["group_name"]:
group["label"] = "groupe %(group_name)s" % group
else:
group["label"] = "liste"
H.append( H.append(
f""" f"""
<tr class="listegroupelink"> <div class="sem-groups-list">
<td> <div>
<a href="{ <a href="{
url_for("scolar.groups_view", url_for("scolar.groups_view",
group_ids=group["group_id"], group_ids=group.id,
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
) )
}">{group["label"]}</a> }">{group_label}
</td><td> - {n_members} étudiants</a>
</td> </div>
<td>({n_members} étudiants)</td> </div>
<div class="sem-groups-assi">
<div>
<a class="btn" href="{
url_for("assiduites.visu_assi_group",
scodoc_dept=g.scodoc_dept,
date_debut=formsemestre.date_debut.isoformat(),
date_fin=formsemestre.date_fin.isoformat(),
group_ids=group.id,
)}">
<button>Bilan assiduité</button></a>
</div>
""" """
) )
if can_edit_abs:
H.append(
f"""
<div>
<a class="btn" href="{
url_for("assiduites.visu_assiduites_group",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
jour = datetime.date.today().isoformat(),
group_ids=group.id,
)}">
<button>Visualiser l'assiduité</button></a>
</div>
<div>
<a class="btn" href="{
url_for("assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
jour=datetime.date.today().isoformat(),
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
<button>Saisie journalière</button></a>
</div>
<div>
<a class="btn" href="{
url_for("assiduites.signal_assiduites_diff",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id,
)}">
<button>Saisie différée</button></a>
</div>
<div>
<a class="btn" href="{
url_for("assiduites.bilan_dept",
scodoc_dept=g.scodoc_dept,
formsemestre=formsemestre.id,
group_ids=group.id,
)}">
<button>Justificatifs en attente</button></a>
</div>
"""
)
H.append(form_abs_tmpl % group) H.append("</div>") # /sem-groups-assi
H.append("</tr>")
H.append("</table>")
if partition_is_empty: if partition_is_empty:
H.append('<p class="help indent">Aucun groupe peuplé dans cette partition') H.append(
'<div class="help sem-groups-none">Aucun groupe peuplé dans cette partition'
)
if formsemestre.can_change_groups(): if formsemestre.can_change_groups():
H.append( H.append(
f""" (<a href="{url_for("scolar.partition_editor", f""" (<a href="{url_for("scolar.partition_editor",
@ -922,7 +930,9 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
edit_partition=1) edit_partition=1)
}" class="stdlink">créer</a>)""" }" class="stdlink">créer</a>)"""
) )
H.append("</p>") H.append("</div>")
H.append("</div>") # /sem-groups-partition
if formsemestre.can_change_groups(): if formsemestre.can_change_groups():
H.append( H.append(
f"""<h4><a class="stdlink" f"""<h4><a class="stdlink"
@ -1031,7 +1041,7 @@ def formsemestre_status_head(formsemestre_id: int = None, page_title: str = None
Le classement des étudiants n'a qu'une valeur indicative.""" Le classement des étudiants n'a qu'une valeur indicative."""
) )
if sem.bul_hide_xml: if sem.bul_hide_xml:
warnings.append("""Bulletins non publiés sur le portail. """) warnings.append("""Bulletins non publiés sur la passerelle.""")
if sem.block_moyennes: if sem.block_moyennes:
warnings.append("Calcul des moyennes bloqué !") warnings.append("Calcul des moyennes bloqué !")
if sem.semestre_id >= 0 and not sem.est_sur_une_annee(): if sem.semestre_id >= 0 and not sem.est_sur_une_annee():
@ -1181,7 +1191,7 @@ def formsemestre_status(formsemestre_id=None, check_parcours=True):
) )
# --- LISTE DES ETUDIANTS # --- LISTE DES ETUDIANTS
H += [ H += [
'<div id="groupes">', '<div class="formsemestre-groupes">',
_make_listes_sem(formsemestre), _make_listes_sem(formsemestre),
"</div>", "</div>",
] ]
@ -1230,7 +1240,11 @@ def formsemestre_tableau_modules(
mod_descr = "Module " + (mod.titre or "") mod_descr = "Module " + (mod.titre or "")
if mod.is_apc(): if mod.is_apc():
coef_descr = ", ".join( coef_descr = ", ".join(
[f"{ue.acronyme}: {co}" for ue, co in mod.ue_coefs_list()] [
f"{ue.acronyme}: {co}"
for ue, co in mod.ue_coefs_list()
if isinstance(co, float) and co > 0
]
) )
if coef_descr: if coef_descr:
mod_descr += " Coefs: " + coef_descr mod_descr += " Coefs: " + coef_descr

View File

@ -131,6 +131,7 @@ def get_partition(partition_id): # OBSOLETE
def get_partitions_list(formsemestre_id, with_default=True) -> list[dict]: def get_partitions_list(formsemestre_id, with_default=True) -> list[dict]:
"""Liste des partitions pour ce semestre (list of dicts), """Liste des partitions pour ce semestre (list of dicts),
triées par numéro, avec la partition par défaut en fin de liste. triées par numéro, avec la partition par défaut en fin de liste.
OBSOLETE: utiliser FormSemestre.get_partitions_list
""" """
partitions = ndb.SimpleDictFetch( partitions = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.* """SELECT p.id AS partition_id, p.*
@ -515,7 +516,7 @@ def get_etud_groups_in_partition(partition_id):
return R return R
def formsemestre_partition_list(formsemestre_id, format="xml"): def formsemestre_partition_list(formsemestre_id, fmt="xml"):
"""Get partitions and groups in this semestre """Get partitions and groups in this semestre
Supported formats: xml, json Supported formats: xml, json
""" """
@ -523,7 +524,7 @@ def formsemestre_partition_list(formsemestre_id, format="xml"):
# Ajoute les groupes # Ajoute les groupes
for p in partitions: for p in partitions:
p["group"] = get_partition_groups(p) p["group"] = get_partition_groups(p)
return scu.sendResult(partitions, name="partition", format=format) return scu.sendResult(partitions, name="partition", fmt=fmt)
# Encore utilisé par groupmgr.js # Encore utilisé par groupmgr.js
@ -1377,20 +1378,18 @@ def group_rename(group_id):
return group_set_name(group, tf[2]["group_name"]) return group_set_name(group, tf[2]["group_name"])
def groups_auto_repartition(partition_id=None): def groups_auto_repartition(partition: Partition):
"""Reparti les etudiants dans des groupes dans une partition, en respectant le niveau """Réparti les etudiants dans des groupes dans une partition, en respectant le niveau
et la mixité. et la mixité.
""" """
partition: Partition = Partition.query.get_or_404(partition_id)
if not partition.groups_editable: if not partition.groups_editable:
raise AccessDenied("Partition non éditable") raise AccessDenied("Partition non éditable")
formsemestre_id = partition.formsemestre_id
formsemestre = partition.formsemestre formsemestre = partition.formsemestre
# renvoie sur page édition partitions et groupes # renvoie sur page édition partitions et groupes
dest_url = url_for( dest_url = url_for(
"scolar.partition_editor", "scolar.partition_editor",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre.id,
) )
if not formsemestre.can_change_groups(): if not formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
@ -1409,7 +1408,9 @@ def groups_auto_repartition(partition_id=None):
] ]
H = [ H = [
html_sco_header.sco_header(page_title="Répartition des groupes"), html_sco_header.sco_header(
page_title="Répartition des groupes", formsemestre_id=formsemestre.id
),
f"""<h2>Répartition des groupes de {partition.partition_name}</h2> f"""<h2>Répartition des groupes de {partition.partition_name}</h2>
<p>Semestre {formsemestre.titre_annee()}</p> <p>Semestre {formsemestre.titre_annee()}</p>
<p class="help">Les groupes existants seront <b>effacés</b> et remplacés par <p class="help">Les groupes existants seront <b>effacés</b> et remplacés par
@ -1455,7 +1456,7 @@ def groups_auto_repartition(partition_id=None):
listes = {} listes = {}
for civilite in civilites: for civilite in civilites:
listes[civilite] = [ listes[civilite] = [
(_get_prev_moy(x["etudid"], formsemestre_id), x["etudid"]) (_get_prev_moy(x["etudid"], formsemestre.id), x["etudid"])
for x in identdict.values() for x in identdict.values()
if x["civilite"] == civilite if x["civilite"] == civilite
] ]

View File

@ -60,7 +60,7 @@ def groups_list_annotation(group_ids: list[int]) -> list[dict]:
return annotations return annotations
def groups_export_annotations(group_ids, formsemestre_id=None, format="html"): def groups_export_annotations(group_ids, formsemestre_id=None, fmt="html"):
"""Les annotations""" """Les annotations"""
groups_infos = sco_groups_view.DisplayedGroupsInfos( groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, formsemestre_id=formsemestre_id group_ids, formsemestre_id=formsemestre_id
@ -68,7 +68,7 @@ def groups_export_annotations(group_ids, formsemestre_id=None, format="html"):
annotations = groups_list_annotation(groups_infos.group_ids) annotations = groups_list_annotation(groups_infos.group_ids)
for annotation in annotations: for annotation in annotations:
annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M") annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M")
if format == "xls": if fmt == "xls":
columns_ids = ("etudid", "nom", "prenom", "date", "comment") columns_ids = ("etudid", "nom", "prenom", "date", "comment")
else: else:
columns_ids = ("etudid", "nom", "prenom", "date_str", "comment") columns_ids = ("etudid", "nom", "prenom", "date_str", "comment")
@ -93,4 +93,4 @@ def groups_export_annotations(group_ids, formsemestre_id=None, format="html"):
html_class="table_leftalign", html_class="table_leftalign",
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
) )
return table.make_page(format=format) return table.make_page(fmt=fmt)

View File

@ -70,7 +70,7 @@ CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
# view: # view:
def groups_view( def groups_view(
group_ids=(), group_ids=(),
format="html", fmt="html",
# Options pour listes: # Options pour listes:
with_codes=0, with_codes=0,
etat=None, etat=None,
@ -82,7 +82,7 @@ def groups_view(
): ):
"""Affichage des étudiants des groupes indiqués """Affichage des étudiants des groupes indiqués
group_ids: liste de group_id group_ids: liste de group_id
format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf fmt: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf
""" """
# Informations sur les groupes à afficher: # Informations sur les groupes à afficher:
groups_infos = DisplayedGroupsInfos( groups_infos = DisplayedGroupsInfos(
@ -92,10 +92,10 @@ def groups_view(
select_all_when_unspecified=True, select_all_when_unspecified=True,
) )
# Formats spéciaux: download direct # Formats spéciaux: download direct
if format != "html": if fmt != "html":
return groups_table( return groups_table(
groups_infos=groups_infos, groups_infos=groups_infos,
format=format, fmt=fmt,
with_codes=with_codes, with_codes=with_codes,
etat=etat, etat=etat,
with_paiement=with_paiement, with_paiement=with_paiement,
@ -135,7 +135,7 @@ def groups_view(
""", """,
groups_table( groups_table(
groups_infos=groups_infos, groups_infos=groups_infos,
format=format, fmt=fmt,
with_codes=with_codes, with_codes=with_codes,
etat=etat, etat=etat,
with_paiement=with_paiement, with_paiement=with_paiement,
@ -324,7 +324,9 @@ class DisplayedGroupsInfos:
if not formsemestre_id: if not formsemestre_id:
raise Exception("missing parameter formsemestre_id or group_ids") raise Exception("missing parameter formsemestre_id or group_ids")
if select_all_when_unspecified: if select_all_when_unspecified:
group_ids = [sco_groups.get_default_group(formsemestre_id)] group_ids = [
sco_groups.get_default_group(formsemestre_id, fix_if_missing=True)
]
else: else:
# selectionne le premier groupe trouvé, s'il y en a un # selectionne le premier groupe trouvé, s'il y en a un
partition = sco_groups.get_partitions_list( partition = sco_groups.get_partitions_list(
@ -437,14 +439,14 @@ def groups_table(
groups_infos: DisplayedGroupsInfos = None, groups_infos: DisplayedGroupsInfos = None,
with_codes=0, with_codes=0,
etat=None, etat=None,
format="html", fmt="html",
with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail) with_paiement=0, # si vrai, ajoute colonnes infos paiement droits et finalisation inscription (lent car interrogation portail)
with_archives=0, # ajoute colonne avec noms fichiers archivés with_archives=0, # ajoute colonne avec noms fichiers archivés
with_annotations=0, with_annotations=0,
with_bourse=0, with_bourse=0,
): ):
"""liste etudiants inscrits dans ce semestre """liste etudiants inscrits dans ce semestre
format: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf fmt: csv, json, xml, xls, allxls, xlsappel, moodlecsv, pdf
Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape Si with_codes, ajoute 4 colonnes avec les codes etudid, NIP, INE et etape
""" """
from app.scodoc import sco_report from app.scodoc import sco_report
@ -499,12 +501,12 @@ def groups_table(
p["partition_id"]: p["partition_name"] for p in groups_infos.partitions p["partition_id"]: p["partition_name"] for p in groups_infos.partitions
} }
if format != "html": # ne mentionne l'état que en Excel (style en html) if fmt != "html": # ne mentionne l'état que en Excel (style en html)
columns_ids.append("etat") columns_ids.append("etat")
columns_ids.append("email") columns_ids.append("email")
columns_ids.append("emailperso") columns_ids.append("emailperso")
if format == "moodlecsv": if fmt == "moodlecsv":
columns_ids = ["email", "semestre_groupe"] columns_ids = ["email", "semestre_groupe"]
if with_codes: if with_codes:
@ -579,7 +581,7 @@ def groups_table(
else: else:
s = "" s = ""
if format == "moodlecsv": if fmt == "moodlecsv":
# de la forme S1-[FI][FA]-groupe.csv # de la forme S1-[FI][FA]-groupe.csv
if not moodle_groupenames: if not moodle_groupenames:
moodle_groupenames = {"tous"} moodle_groupenames = {"tous"}
@ -612,7 +614,7 @@ def groups_table(
preferences=prefs, preferences=prefs,
) )
# #
if format == "html": if fmt == "html":
amail_inst = [ amail_inst = [
x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D" x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D"
] ]
@ -683,11 +685,11 @@ def groups_table(
[ [
tab.html(), tab.html(),
"<ul>", "<ul>",
'<li><a class="stdlink" href="%s&format=xlsappel">Feuille d\'appel Excel</a></li>' '<li><a class="stdlink" href="%s&fmt=xlsappel">Feuille d\'appel Excel</a></li>'
% (tab.base_url,), % (tab.base_url,),
'<li><a class="stdlink" href="%s&format=xls">Table Excel</a></li>' '<li><a class="stdlink" href="%s&fmt=xls">Table Excel</a></li>'
% (tab.base_url,), % (tab.base_url,),
'<li><a class="stdlink" href="%s&format=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a></li>' '<li><a class="stdlink" href="%s&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a></li>'
% (tab.base_url,), % (tab.base_url,),
"""<li> """<li>
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id=%s">Fichier CSV pour Moodle (tous les groupes)</a> <a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id=%s">Fichier CSV pour Moodle (tous les groupes)</a>
@ -723,17 +725,17 @@ def groups_table(
return "".join(H) + "</div>" return "".join(H) + "</div>"
elif ( elif (
format == "pdf" fmt == "pdf"
or format == "xml" or fmt == "xml"
or format == "json" or fmt == "json"
or format == "xls" or fmt == "xls"
or format == "moodlecsv" or fmt == "moodlecsv"
): ):
if format == "moodlecsv": if fmt == "moodlecsv":
format = "csv" fmt = "csv"
return tab.make_page(format=format) return tab.make_page(fmt=fmt)
elif format == "xlsappel": elif fmt == "xlsappel":
xls = sco_excel.excel_feuille_listeappel( xls = sco_excel.excel_feuille_listeappel(
groups_infos.formsemestre, groups_infos.formsemestre,
groups_infos.groups_titles, groups_infos.groups_titles,
@ -745,7 +747,7 @@ def groups_table(
) )
filename = "liste_%s" % groups_infos.groups_filename filename = "liste_%s" % groups_infos.groups_filename
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
elif format == "allxls": elif fmt == "allxls":
# feuille Excel avec toutes les infos etudiants # feuille Excel avec toutes les infos etudiants
if not groups_infos.members: if not groups_infos.members:
return "" return ""
@ -825,7 +827,7 @@ def tab_absences_html(groups_infos, etat=None):
H.extend( H.extend(
[ [
"<h3>Assiduités</h3>", "<h3>Assiduité</h3>",
'<ul class="ul_abs">', '<ul class="ul_abs">',
"<li>", "<li>",
form_choix_saisie_semaine(groups_infos), # Ajout Le Havre form_choix_saisie_semaine(groups_infos), # Ajout Le Havre
@ -833,21 +835,25 @@ def tab_absences_html(groups_infos, etat=None):
"<li>", "<li>",
form_choix_jour_saisie_hebdo(groups_infos), form_choix_jour_saisie_hebdo(groups_infos),
"</li>", "</li>",
f"""<li><a href="{ f"""<li><a class="stdlink" href="{
url_for("assiduites.visu_assi_group", scodoc_dept=g.scodoc_dept, group_ids=group_ids, date_debut=formsemestre.date_debut.isoformat(), date_fin=formsemestre.date_fin.isoformat()) url_for("assiduites.visu_assi_group", scodoc_dept=g.scodoc_dept,
}">État des assiduités du groupe</a></li>""", group_ids=group_ids,
date_debut=formsemestre.date_debut.isoformat(),
date_fin=formsemestre.date_fin.isoformat()
)
}">État de l'assiduité du groupe</a></li>""",
"</ul>", "</ul>",
"<h3>Feuilles</h3>", "<h3>Feuilles</h3>",
'<ul class="ul_feuilles">', '<ul class="ul_feuilles">',
"""<li><a class="stdlink" href="%s&format=xlsappel">Feuille d'émargement %s (Excel)</a></li>""" """<li><a class="stdlink" href="%s&fmt=xlsappel">Feuille d'émargement %s (Excel)</a></li>"""
% (groups_infos.base_url, groups_infos.groups_titles), % (groups_infos.base_url, groups_infos.groups_titles),
"""<li><a class="stdlink" href="trombino?%s&format=pdf">Trombinoscope en PDF</a></li>""" """<li><a class="stdlink" href="trombino?%s&fmt=pdf">Trombinoscope en PDF</a></li>"""
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="pdf_trombino_tours?%s&format=pdf">Trombinoscope en PDF (format "IUT de Tours", beta)</a></li>""" """<li><a class="stdlink" href="pdf_trombino_tours?%s&fmt=pdf">Trombinoscope en PDF (format "IUT de Tours", beta)</a></li>"""
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="pdf_feuille_releve_absences?%s&format=pdf">Feuille relevé absences hebdomadaire (beta)</a></li>""" """<li><a class="stdlink" href="pdf_feuille_releve_absences?%s&fmt=pdf">Feuille relevé absences hebdomadaire (beta)</a></li>"""
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="trombino?%s&format=pdflist">Liste d'appel avec photos</a></li>""" """<li><a class="stdlink" href="trombino?%s&fmt=pdflist">Liste d'appel avec photos</a></li>"""
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
"""<li><a class="stdlink" href="groups_export_annotations?%s">Liste des annotations</a></li>""" """<li><a class="stdlink" href="groups_export_annotations?%s">Liste des annotations</a></li>"""
% groups_infos.groups_query_args, % groups_infos.groups_query_args,
@ -890,76 +896,38 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
authuser = current_user authuser = current_user
if not authuser.has_permission(Permission.ScoAbsChange): if not authuser.has_permission(Permission.ScoAbsChange):
return "" return ""
sem = groups_infos.formsemestre return f"""
first_monday = sco_cal.ddmmyyyy(sem["date_debut"]).prev_monday() <button onclick="window.location='{
today_idx = datetime.date.today().weekday() url_for(
"assiduites.signal_assiduites_group",
FA = [] # formulaire avec menu saisi absences scodoc_dept=g.scodoc_dept,
FA.append( group_ids=",".join(map(str,groups_infos.group_ids)),
# TODO-ASSIDUITE et utiliser url_for... (was Absences/SignaleAbsenceGrSemestre) jour=datetime.date.today().isoformat(),
'<form id="form_choix_jour_saisie_hebdo" action="XXX" method="get">' formsemestre_id=groups_infos.formsemestre_id,
) moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
FA.append('<input type="hidden" name="datefin" value="%(date_fin)s"/>' % sem) )
FA.append(groups_infos.get_form_elem()) }';">Saisie du jour ({datetime.date.today().strftime('%d/%m/%Y')})</button>
if moduleimpl_id: """
FA.append(
'<input type="hidden" name="moduleimpl_id" value="%s"/>' % moduleimpl_id
)
FA.append('<input type="hidden" name="destination" value=""/>')
FA.append(
"""<input type="button" onclick="$('#form_choix_jour_saisie_hebdo')[0].destination.value=get_current_url(); $('#form_choix_jour_saisie_hebdo').submit();" value="Saisir absences du (NON DISPONIBLE) "/>"""
)
FA.append("""<select name="datedebut">""")
date = first_monday
i = 0
for jour in sco_cal.day_names():
if i == today_idx:
sel = "selected"
else:
sel = ""
i += 1
FA.append('<option value="%s" %s>%s</option>' % (date, sel, jour))
date = date.next_day()
FA.append("</select>")
FA.append("</form>")
return "\n".join(FA)
# Ajout Le Havre # Saisie de l'assiduité par semaine
# Formulaire saisie absences semaine
def form_choix_saisie_semaine(groups_infos): def form_choix_saisie_semaine(groups_infos):
authuser = current_user authuser = current_user
if not authuser.has_permission(Permission.ScoAbsChange): if not authuser.has_permission(Permission.ScoAbsChange):
return "" return ""
# construit l'URL "destination"
# (a laquelle on revient apres saisie absences)
query_args = parse_qs(request.query_string) query_args = parse_qs(request.query_string)
moduleimpl_id = query_args.get("moduleimpl_id", [""])[0] moduleimpl_id = query_args.get("moduleimpl_id", [None])[0]
if "head_message" in query_args: semaine = datetime.date.today().isocalendar().week
del query_args["head_message"] return f"""
destination = "%s?%s" % ( <button onclick="window.location='{url_for(
request.base_url, "assiduites.signal_assiduites_diff",
urllib.parse.urlencode(query_args, True), group_ids=",".join(map(str,groups_infos.group_ids)),
) semaine=semaine,
destination = destination.replace( scodoc_dept=g.scodoc_dept,
"%", "%%" formsemestre_id=groups_infos.formsemestre_id,
) # car ici utilisee dans un format string ! moduleimpl_id=moduleimpl_id
)}';">Saisie à la semaine</button>
DateJour = time.strftime("%d/%m/%Y") """
datelundi = sco_cal.ddmmyyyy(DateJour).prev_monday()
FA = [] # formulaire avec menu saisie hebdo des absences
# XXX TODO-ASSIDUITE et utiliser un POST
FA.append('<form action="Absences/SignaleAbsenceGrHebdo" method="get">')
FA.append('<input type="hidden" name="datelundi" value="%s"/>' % datelundi)
FA.append('<input type="hidden" name="moduleimpl_id" value="%s"/>' % moduleimpl_id)
FA.append('<input type="hidden" name="destination" value="%s"/>' % destination)
FA.append(groups_infos.get_form_elem())
FA.append(
'<input type="submit" class="button" value="Saisie à la semaine (NON DISPONIBLE)" />'
) # XXX
FA.append("</form>")
return "\n".join(FA)
def export_groups_as_moodle_csv(formsemestre_id=None): def export_groups_as_moodle_csv(formsemestre_id=None):
@ -1004,4 +972,4 @@ def export_groups_as_moodle_csv(formsemestre_id=None):
text_with_titles=prefs["moodle_csv_with_headerline"], text_with_titles=prefs["moodle_csv_with_headerline"],
preferences=prefs, preferences=prefs,
) )
return tab.make_page(format="csv") return tab.make_page(fmt="csv")

View File

@ -60,7 +60,7 @@ from app.scodoc.htmlutils import histogram_notes
def do_evaluation_listenotes( def do_evaluation_listenotes(
evaluation_id=None, moduleimpl_id=None, format="html" evaluation_id=None, moduleimpl_id=None, fmt="html"
) -> tuple[str, str]: ) -> tuple[str, str]:
""" """
Affichage des notes d'une évaluation (si evaluation_id) Affichage des notes d'une évaluation (si evaluation_id)
@ -220,7 +220,7 @@ def do_evaluation_listenotes(
_make_table_notes( _make_table_notes(
tf[1], tf[1],
evals, evals,
fmt=format, fmt=fmt,
note_sur_20=note_sur_20, note_sur_20=note_sur_20,
anonymous_listing=anonymous_listing, anonymous_listing=anonymous_listing,
group_ids=group_ids, group_ids=group_ids,
@ -424,7 +424,7 @@ def _make_table_notes(
key_mgr, key_mgr,
note_sur_20, note_sur_20,
keep_numeric, keep_numeric,
format=fmt, fmt=fmt,
) )
columns_ids.append(e["evaluation_id"]) columns_ids.append(e["evaluation_id"])
# #
@ -596,7 +596,7 @@ def _make_table_notes(
) )
if fmt == "bordereau": if fmt == "bordereau":
fmt = "pdf" fmt = "pdf"
t = tab.make_page(format=fmt, with_html_headers=False) t = tab.make_page(fmt=fmt, with_html_headers=False)
if fmt != "html": if fmt != "html":
return t return t
@ -622,7 +622,7 @@ def _make_table_notes(
histo = histogram_notes(notes) histo = histogram_notes(notes)
# 2 colonnes: histo, comments # 2 colonnes: histo, comments
C = [ C = [
f'<br><a class="stdlink" href="{base_url}&format=bordereau">Bordereau de Signatures (version PDF)</a>', f'<br><a class="stdlink" href="{base_url}&fmt=bordereau">Bordereau de Signatures (version PDF)</a>',
"<table><tr><td><div><h4>Répartition des notes:</h4>" "<table><tr><td><div><h4>Répartition des notes:</h4>"
+ histo + histo
+ "</div></td>\n", + "</div></td>\n",
@ -670,7 +670,7 @@ def _add_eval_columns(
K, K,
note_sur_20, note_sur_20,
keep_numeric, keep_numeric,
format="html", fmt="html",
): ):
"""Add eval e""" """Add eval e"""
nb_notes = 0 nb_notes = 0
@ -774,7 +774,7 @@ def _add_eval_columns(
row_coefs[evaluation_id] = "coef. %s" % e["coefficient"] row_coefs[evaluation_id] = "coef. %s" % e["coefficient"]
if is_apc: if is_apc:
if format == "html": if fmt == "html":
row_poids[evaluation_id] = _mini_table_eval_ue_poids( row_poids[evaluation_id] = _mini_table_eval_ue_poids(
evaluation_id, evals_poids, ues evaluation_id, evals_poids, ues
) )

View File

@ -63,7 +63,7 @@ def formsemestre_table_etuds_lycees(
) )
def scodoc_table_etuds_lycees(format="html"): def scodoc_table_etuds_lycees(fmt="html"):
"""Table avec _tous_ les étudiants des semestres non verrouillés """Table avec _tous_ les étudiants des semestres non verrouillés
de _tous_ les départements. de _tous_ les départements.
""" """
@ -71,7 +71,7 @@ def scodoc_table_etuds_lycees(format="html"):
semdepts = sco_formsemestre.scodoc_get_all_unlocked_sems() semdepts = sco_formsemestre.scodoc_get_all_unlocked_sems()
etuds = [] etuds = []
try: try:
for (sem, dept) in semdepts: for sem, dept in semdepts:
app.set_sco_dept(dept.acronym) app.set_sco_dept(dept.acronym)
etuds += sco_report.tsp_etud_list(sem["formsemestre_id"])[0] etuds += sco_report.tsp_etud_list(sem["formsemestre_id"])[0]
finally: finally:
@ -85,8 +85,8 @@ def scodoc_table_etuds_lycees(format="html"):
no_links=True, no_links=True,
) )
tab.base_url = request.base_url tab.base_url = request.base_url
t = tab.make_page(format=format, with_html_headers=False) t = tab.make_page(fmt=fmt, with_html_headers=False)
if format != "html": if fmt != "html":
return t return t
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
@ -178,7 +178,7 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False)
def formsemestre_etuds_lycees( def formsemestre_etuds_lycees(
formsemestre_id, formsemestre_id,
format="html", fmt="html",
only_primo=False, only_primo=False,
no_grouping=False, no_grouping=False,
): ):
@ -191,14 +191,10 @@ def formsemestre_etuds_lycees(
tab.base_url += "&only_primo=1" tab.base_url += "&only_primo=1"
if no_grouping: if no_grouping:
tab.base_url += "&no_grouping=1" tab.base_url += "&no_grouping=1"
t = tab.make_page(format=format, with_html_headers=False) t = tab.make_page(fmt=fmt, with_html_headers=False)
if format != "html": if fmt != "html":
return t return t
F = [ F = [sco_report.tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt)]
sco_report.tsp_form_primo_group(
only_primo, no_grouping, formsemestre_id, format
)
]
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title=tab.page_title, page_title=tab.page_title,

View File

@ -299,27 +299,15 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
has_expression = sco_compute_moy.moduleimpl_has_expression(modimpl) has_expression = sco_compute_moy.moduleimpl_has_expression(modimpl)
if has_expression: if has_expression:
H.append( H.append(
f"""<tr> """<tr>
<td class="fichetitre2" colspan="4">Règle de calcul: <td class="fichetitre2" colspan="4">Règle de calcul:
<span class="formula" title="mode de calcul de la moyenne du module" <span class="warning">inutilisée dans cette version de ScoDoc</span>
>moyenne=<tt>{modimpl.computation_expr}</tt> </td>
</span>""" </tr>
"""
) )
H.append("""<span class="warning">inutilisée dans cette version de ScoDoc""")
if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
H.append(
f""" <a href="{
url_for("notes.delete_moduleimpl_expr", scodoc_dept=g.scodoc_dept,
moduleimpl_id=moduleimpl_id)
}" class="stdlink">supprimer</a>"""
)
H.append("""</span>""")
H.append("</td></tr>")
else: else:
H.append( H.append('<tr><td colspan="4">')
'<tr><td colspan="4">'
# <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
)
H.append("</td></tr>") H.append("</td></tr>")
H.append( H.append(
f"""<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink" f"""<tr><td colspan="4"><span class="moduleimpl_abs_link"><a class="stdlink"
@ -343,6 +331,21 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
}&formsemestre_id={formsemestre.id} }&formsemestre_id={formsemestre.id}
&moduleimpl_id={moduleimpl_id} &moduleimpl_id={moduleimpl_id}
" "
>Saisie Absences journée</a></span>
"""
)
H.append(
f"""
<span class="moduleimpl_abs_link"><a class="stdlink" href="{
url_for(
"assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
group_ids=group_id,
jour=datetime.date.today().isoformat(),
formsemestre_id=formsemestre.id,
moduleimpl_id="" if moduleimpl_id is None else moduleimpl_id
)}"
>Saisie Absences hebdo</a></span> >Saisie Absences hebdo</a></span>
""" """
) )

View File

@ -441,7 +441,7 @@ def ficheEtud(etudid=None):
# Fichiers archivés: # Fichiers archivés:
info["fichiers_archive_htm"] = ( info["fichiers_archive_htm"] = (
'<div class="fichetitre">Fichiers associés</div>' '<div class="fichetitre">Fichiers associés</div>'
+ sco_archives_etud.etud_list_archives_html(etudid) + sco_archives_etud.etud_list_archives_html(etud)
) )
# Devenir de l'étudiant: # Devenir de l'étudiant:

View File

@ -27,7 +27,7 @@ _SCO_PERMISSIONS = (
(1 << 13, "ScoAbsChange", "Saisir des absences"), (1 << 13, "ScoAbsChange", "Saisir des absences"),
(1 << 14, "ScoAbsAddBillet", "Saisir des billets d'absences"), (1 << 14, "ScoAbsAddBillet", "Saisir des billets d'absences"),
# changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche # changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche
(1 << 15, "ScoEtudChangeAdr", "Changer les addresses d'étudiants"), (1 << 15, "ScoEtudChangeAdr", "Changer les adresses d'étudiants"),
( (
1 << 16, 1 << 16,
"APIEditGroups", "APIEditGroups",

View File

@ -383,7 +383,7 @@ class PlacementRunner:
self.moduleimpl_data["formsemestre_id"] self.moduleimpl_data["formsemestre_id"]
), ),
) )
return tab.make_page(format="pdf", with_html_headers=False) return tab.make_page(fmt="pdf", with_html_headers=False)
def _one_header(self, worksheet): def _one_header(self, worksheet):
cells = [ cells = [

View File

@ -178,7 +178,7 @@ def _getEtudInfoGroupes(group_ids, etat=None):
return etuds return etuds
def formsemestre_poursuite_report(formsemestre_id, format="html"): def formsemestre_poursuite_report(formsemestre_id, fmt="html"):
"""Table avec informations "poursuite" """ """Table avec informations "poursuite" """
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
etuds = _getEtudInfoGroupes([sco_groups.get_default_group(formsemestre_id)]) etuds = _getEtudInfoGroupes([sco_groups.get_default_group(formsemestre_id)])
@ -230,6 +230,6 @@ def formsemestre_poursuite_report(formsemestre_id, format="html"):
title="""<h2 class="formsemestre">Poursuite d'études</h2>""", title="""<h2 class="formsemestre">Poursuite d'études</h2>""",
init_qtip=True, init_qtip=True,
javascripts=["js/etud_info.js"], javascripts=["js/etud_info.js"],
format=format, fmt=fmt,
with_html_headers=True, with_html_headers=True,
) )

View File

@ -609,7 +609,18 @@ class BasePreferences:
"category": "abs", "category": "abs",
}, },
), ),
# Assiduités # Assiduité
(
"assi_limit_annee",
{
"initvalue": 1,
"title": "Ne lister que l'assiduités de l'année",
"explanation": "Limite l'affichage des listes d'assiduité et de justificatifs à l'année en cours",
"input_type": "boolcheckbox",
"labels": ["non", "oui"],
"category": "assi",
},
),
( (
"forcer_module", "forcer_module",
{ {
@ -1750,6 +1761,17 @@ class BasePreferences:
"category": "bul_mail", "category": "bul_mail",
}, },
), ),
(
"bul_mail_list_abs_nb",
{
"initvalue": 10,
"title": "Nombre maximum de dates par catégorie",
"explanation": "dans la liste des absences dans le mail envoyant le bulletin de notes (catégories : abs,abs_just, retard,justificatifs)",
"type": "int",
"size": 3,
"category": "bul_mail",
},
),
( (
"bul_mail_contact_addr", "bul_mail_contact_addr",
{ {

View File

@ -206,14 +206,14 @@ def pvjury_table(
return lines, titles, columns_ids return lines, titles, columns_ids
def formsemestre_pvjury(formsemestre_id, format="html", publish=True): def formsemestre_pvjury(formsemestre_id, fmt="html", publish=True):
"""Page récapitulant les décisions de jury """Page récapitulant les décisions de jury
En classique: table spécifique avec les deux semestres pour le DUT En classique: table spécifique avec les deux semestres pour le DUT
En APC/BUT: renvoie vers table recap, en mode jury. En APC/BUT: renvoie vers table recap, en mode jury.
""" """
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
is_apc = formsemestre.formation.is_apc() is_apc = formsemestre.formation.is_apc()
if format == "html" and is_apc: if fmt == "html" and is_apc:
return redirect( return redirect(
url_for( url_for(
"notes.formsemestre_recapcomplet", "notes.formsemestre_recapcomplet",
@ -227,7 +227,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True) dpv = sco_pv_dict.dict_pvjury(formsemestre_id, with_prev=True)
if not dpv: if not dpv:
if format == "html": if fmt == "html":
return ( return (
html_sco_header.sco_header() html_sco_header.sco_header()
+ "<h2>Aucune information disponible !</h2>" + "<h2>Aucune information disponible !</h2>"
@ -239,7 +239,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
formsemestre_id = sem["formsemestre_id"] formsemestre_id = sem["formsemestre_id"]
rows, titles, columns_ids = pvjury_table(dpv) rows, titles, columns_ids = pvjury_table(dpv)
if format != "html" and format != "pdf": if fmt != "html" and fmt != "pdf":
columns_ids = ["etudid", "code_nip"] + columns_ids columns_ids = ["etudid", "code_nip"] + columns_ids
tab = GenTable( tab = GenTable(
@ -255,9 +255,9 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
html_sortable=True, html_sortable=True,
preferences=sco_preferences.SemPreferences(formsemestre_id), preferences=sco_preferences.SemPreferences(formsemestre_id),
) )
if format != "html": if fmt != "html":
return tab.make_page( return tab.make_page(
format=format, fmt=fmt,
with_html_headers=False, with_html_headers=False,
publish=publish, publish=publish,
) )

View File

@ -205,7 +205,7 @@ def _results_by_category(
bottom_titles["row_title"] = "Total" bottom_titles["row_title"] = "Total"
# ajout titre ligne: # ajout titre ligne:
for (cat, l) in zip(categories, C): for cat, l in zip(categories, C):
l["row_title"] = cat if cat is not None else "?" l["row_title"] = cat if cat is not None else "?"
# #
@ -274,7 +274,7 @@ def formsemestre_report(
return tab return tab
# def formsemestre_report_bacs(formsemestre_id, format='html'): # def formsemestre_report_bacs(formsemestre_id, fmt='html'):
# """ # """
# Tableau sur résultats par type de bac # Tableau sur résultats par type de bac
# """ # """
@ -287,12 +287,12 @@ def formsemestre_report(
# title=title) # title=title)
# return tab.make_page( # return tab.make_page(
# title = """<h2>Résultats de <a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a></h2>""" % sem, # title = """<h2>Résultats de <a href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titreannee)s</a></h2>""" % sem,
# format=format, page_title = title) # fmt=fmt, page_title = title)
def formsemestre_report_counts( def formsemestre_report_counts(
formsemestre_id: int, formsemestre_id: int,
format="html", fmt="html",
category: str = "bac", category: str = "bac",
result: str = None, result: str = None,
allkeys: bool = False, allkeys: bool = False,
@ -397,10 +397,10 @@ def formsemestre_report_counts(
t = tab.make_page( t = tab.make_page(
title="""<h2 class="formsemestre">Comptes croisés</h2>""", title="""<h2 class="formsemestre">Comptes croisés</h2>""",
format=format, fmt=fmt,
with_html_headers=False, with_html_headers=False,
) )
if format != "html": if fmt != "html":
return t return t
H = [ H = [
html_sco_header.sco_header(page_title=title), html_sco_header.sco_header(page_title=title),
@ -734,7 +734,7 @@ def table_suivi_cohorte(
def formsemestre_suivi_cohorte( def formsemestre_suivi_cohorte(
formsemestre_id, formsemestre_id,
format="html", fmt="html",
percent=1, percent=1,
bac="", bac="",
bacspecialite="", bacspecialite="",
@ -774,8 +774,8 @@ def formsemestre_suivi_cohorte(
) )
if only_primo: if only_primo:
tab.base_url += "&only_primo=on" tab.base_url += "&only_primo=on"
t = tab.make_page(format=format, with_html_headers=False) t = tab.make_page(fmt=fmt, with_html_headers=False)
if format != "html": if fmt != "html":
return t return t
base_url = request.base_url base_url = request.base_url
@ -1246,7 +1246,7 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True)
return tab return tab
def tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, format): def tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt):
"""Element de formulaire pour choisir si restriction aux primos entrants et groupement par lycees""" """Element de formulaire pour choisir si restriction aux primos entrants et groupement par lycees"""
F = ["""<form name="f" method="get" action="%s">""" % request.base_url] F = ["""<form name="f" method="get" action="%s">""" % request.base_url]
if only_primo: if only_primo:
@ -1268,14 +1268,14 @@ def tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, format):
F.append( F.append(
'<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id '<input type="hidden" name="formsemestre_id" value="%s"/>' % formsemestre_id
) )
F.append('<input type="hidden" name="format" value="%s"/>' % format) F.append('<input type="hidden" name="fmt" value="%s"/>' % fmt)
F.append("""</form>""") F.append("""</form>""")
return "\n".join(F) return "\n".join(F)
def formsemestre_suivi_cursus( def formsemestre_suivi_cursus(
formsemestre_id, formsemestre_id,
format="html", fmt="html",
only_primo=False, only_primo=False,
no_grouping=False, no_grouping=False,
): ):
@ -1290,10 +1290,10 @@ def formsemestre_suivi_cursus(
tab.base_url += "&only_primo=1" tab.base_url += "&only_primo=1"
if no_grouping: if no_grouping:
tab.base_url += "&no_grouping=1" tab.base_url += "&no_grouping=1"
t = tab.make_page(format=format, with_html_headers=False) t = tab.make_page(fmt=fmt, with_html_headers=False)
if format != "html": if fmt != "html":
return t return t
F = [tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, format)] F = [tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt)]
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
@ -1312,7 +1312,7 @@ def formsemestre_suivi_cursus(
# ------------- # -------------
def graph_cursus( def graph_cursus(
formsemestre_id, formsemestre_id,
format="svg", fmt="svg",
only_primo=False, only_primo=False,
bac="", # selection sur type de bac bac="", # selection sur type de bac
bacspecialite="", bacspecialite="",
@ -1437,7 +1437,7 @@ def graph_cursus(
g.add_node(n) g.add_node(n)
g.set("rankdir", "LR") # left to right g.set("rankdir", "LR") # left to right
g.set_fontname("Helvetica") g.set_fontname("Helvetica")
if format == "svg": if fmt == "svg":
g.set_bgcolor("#fffff0") # ou 'transparent' g.set_bgcolor("#fffff0") # ou 'transparent'
# titres des semestres: # titres des semestres:
for s in sems.values(): for s in sems.values():
@ -1489,7 +1489,7 @@ def graph_cursus(
n.set("label", "Diplome") # bug si accent (pas compris pourquoi) n.set("label", "Diplome") # bug si accent (pas compris pourquoi)
# Arètes: # Arètes:
bubbles = {} # substitue titres pour bulle aides: src_id:dst_id : etud_descr bubbles = {} # substitue titres pour bulle aides: src_id:dst_id : etud_descr
for (src_id, dst_id) in edges.keys(): for src_id, dst_id in edges.keys():
e = g.get_edge(src_id, dst_id)[0] e = g.get_edge(src_id, dst_id)[0]
e.set("arrowhead", "normal") e.set("arrowhead", "normal")
e.set("arrowsize", 1) e.set("arrowsize", 1)
@ -1503,20 +1503,19 @@ def graph_cursus(
e.set_URL(f"__xxxetudlist__?{src_id}:{dst_id}") e.set_URL(f"__xxxetudlist__?{src_id}:{dst_id}")
# Genere graphe # Genere graphe
_, path = tempfile.mkstemp(".gr") _, path = tempfile.mkstemp(".gr")
g.write(path=path, format=format) g.write(path=path, format=fmt)
with open(path, "rb") as f: with open(path, "rb") as f:
data = f.read() data = f.read()
log("dot generated %d bytes in %s format" % (len(data), format)) log("dot generated %d bytes in %s format" % (len(data), fmt))
if not data: if not data:
log("graph.to_string=%s" % g.to_string()) log("graph.to_string=%s" % g.to_string())
raise ValueError( raise ValueError("Erreur lors de la génération du document au format %s" % fmt)
"Erreur lors de la génération du document au format %s" % format
)
os.unlink(path) os.unlink(path)
if format == "svg": if fmt == "svg":
# dot génère un document XML complet, il faut enlever l'en-tête # dot génère un document XML complet, il faut enlever l'en-tête
data_str = data.decode("utf-8") data_str = data.decode("utf-8")
data = "<svg" + "<svg".join(data_str.split("<svg")[1:]) data = "<svg" + "<svg".join(data_str.split("<svg")[1:])
# Substitution des titres des URL des aretes pour bulles aide # Substitution des titres des URL des aretes pour bulles aide
def repl(m): def repl(m):
return '<a title="%s"' % bubbles[m.group("sd")] return '<a title="%s"' % bubbles[m.group("sd")]
@ -1563,7 +1562,7 @@ def graph_cursus(
def formsemestre_graph_cursus( def formsemestre_graph_cursus(
formsemestre_id, formsemestre_id,
format="html", fmt="html",
only_primo=False, only_primo=False,
bac="", # selection sur type de bac bac="", # selection sur type de bac
bacspecialite="", bacspecialite="",
@ -1578,7 +1577,7 @@ def formsemestre_graph_cursus(
annee_admission = str(annee_admission or "") annee_admission = str(annee_admission or "")
# log("formsemestre_graph_cursus") # log("formsemestre_graph_cursus")
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
if format == "pdf": if fmt == "pdf":
( (
doc, doc,
etuds, etuds,
@ -1590,7 +1589,7 @@ def formsemestre_graph_cursus(
statuts, statuts,
) = graph_cursus( ) = graph_cursus(
formsemestre_id, formsemestre_id,
format="pdf", fmt="pdf",
only_primo=only_primo, only_primo=only_primo,
bac=bac, bac=bac,
bacspecialite=bacspecialite, bacspecialite=bacspecialite,
@ -1601,7 +1600,7 @@ def formsemestre_graph_cursus(
) )
filename = scu.make_filename("flux " + sem["titreannee"]) filename = scu.make_filename("flux " + sem["titreannee"])
return scu.sendPDFFile(doc, filename + ".pdf") return scu.sendPDFFile(doc, filename + ".pdf")
elif format == "png": elif fmt == "png":
# #
( (
doc, doc,
@ -1614,7 +1613,7 @@ def formsemestre_graph_cursus(
statuts, statuts,
) = graph_cursus( ) = graph_cursus(
formsemestre_id, formsemestre_id,
format="png", fmt="png",
only_primo=only_primo, only_primo=only_primo,
bac=bac, bac=bac,
bacspecialite=bacspecialite, bacspecialite=bacspecialite,
@ -1630,7 +1629,7 @@ def formsemestre_graph_cursus(
attached=True, attached=True,
mime="image/png", mime="image/png",
) )
elif format == "html": elif fmt == "html":
url_kw = { url_kw = {
"scodoc_dept": g.scodoc_dept, "scodoc_dept": g.scodoc_dept,
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
@ -1689,19 +1688,20 @@ def formsemestre_graph_cursus(
"""<p>Origine et devenir des étudiants inscrits dans %(titreannee)s""" """<p>Origine et devenir des étudiants inscrits dans %(titreannee)s"""
% sem, % sem,
"""(<a href="%s">version pdf</a>""" """(<a href="%s">version pdf</a>"""
% url_for("notes.formsemestre_graph_cursus", format="pdf", **url_kw), % url_for("notes.formsemestre_graph_cursus", fmt="pdf", **url_kw),
""", <a href="%s">image PNG</a>)""" """, <a href="%s">image PNG</a>)"""
% url_for("notes.formsemestre_graph_cursus", format="png", **url_kw), % url_for("notes.formsemestre_graph_cursus", fmt="png", **url_kw),
"""</p>""", f"""
"""<p class="help">Le graphe permet de suivre les étudiants inscrits dans le semestre </p>
<p class="help">Le graphe permet de suivre les étudiants inscrits dans le semestre
sélectionné (dessiné en vert). Chaque rectangle représente un semestre (cliquez dedans sélectionné (dessiné en vert). Chaque rectangle représente un semestre (cliquez dedans
pour afficher son tableau de bord). Les flèches indiquent le nombre d'étudiants passant pour afficher son tableau de bord). Les flèches indiquent le nombre d'étudiants
d'un semestre à l'autre (s'il y en a moins de %s, vous pouvez visualiser leurs noms en passant d'un semestre à l'autre (s'il y en a moins de {MAX_ETUD_IN_DESCR}, vous
passant la souris sur le chiffre). pouvez visualiser leurs noms en passant le curseur sur le chiffre).
</p>""" </p>
% MAX_ETUD_IN_DESCR, """,
html_sco_header.sco_footer(), html_sco_header.sco_footer(),
] ]
return "\n".join(H) return "\n".join(H)
else: else:
raise ValueError("invalid format: %s" % format) raise ValueError(f"invalid format: {fmt}")

View File

@ -67,7 +67,7 @@ INDICATEUR_NAMES = {
} }
def formsemestre_but_indicateurs(formsemestre_id: int, format="html"): def formsemestre_but_indicateurs(formsemestre_id: int, fmt="html"):
"""Page avec tableau indicateurs enquête ADIUT BUT 2022""" """Page avec tableau indicateurs enquête ADIUT BUT 2022"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
@ -100,10 +100,10 @@ def formsemestre_but_indicateurs(formsemestre_id: int, format="html"):
title = "Indicateurs suivi annuel BUT" title = "Indicateurs suivi annuel BUT"
t = tab.make_page( t = tab.make_page(
title=f"""<h2 class="formsemestre">{title}</h2>""", title=f"""<h2 class="formsemestre">{title}</h2>""",
format=format, fmt=fmt,
with_html_headers=False, with_html_headers=False,
) )
if format != "html": if fmt != "html":
return t return t
H = [ H = [
html_sco_header.sco_header(page_title=title), html_sco_header.sco_header(page_title=title),

View File

@ -46,6 +46,7 @@ from app.models import (
Module, Module,
ModuleImpl, ModuleImpl,
ScolarNews, ScolarNews,
Assiduite,
) )
from app.models.etudiants import Identite from app.models.etudiants import Identite
@ -75,6 +76,8 @@ import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import json_error
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
from flask_sqlalchemy.query import Query
def convert_note_from_string( def convert_note_from_string(
note: str, note: str,
@ -1102,29 +1105,21 @@ def _get_sorted_etuds(evaluation: Evaluation, etudids: list, formsemestre_id: in
# Groupes auxquels appartient cet étudiant: # Groupes auxquels appartient cet étudiant:
e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id) e["groups"] = sco_groups.get_etud_groups(etudid, formsemestre_id)
# Information sur absence (tenant compte de la demi-journée) # Information sur absence
jour_iso = ( warn_abs_lst: str = ""
evaluation.date_debut.date().isoformat() if evaluation.date_debut else "" if evaluation.date_debut is not None and evaluation.date_fin is not None:
) assiduites_etud: Query = etud.assiduites.filter(
warn_abs_lst = [] Assiduite.etat == scu.EtatAssiduite.ABSENT,
if evaluation.is_matin(): Assiduite.date_debut <= evaluation.date_fin,
nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=True) Assiduite.date_fin >= evaluation.date_debut,
nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=True) )
if nbabs: premiere_assi: Assiduite = assiduites_etud.first()
if nbabsjust: if premiere_assi is not None:
warn_abs_lst.append("absent justifié le matin !") warn_abs_lst: str = (
else: f"absent {'justifié' if premiere_assi.est_just else ''}"
warn_abs_lst.append("absent le matin !") )
if evaluation.is_apresmidi():
nbabs = 0 # TODO-ASSIDUITE sco_abs.count_abs(etudid, jour_iso, jour_iso, matin=0)
nbabsjust = 0 # TODO-ASSIDUITE sco_abs.count_abs_just(etudid, jour_iso, jour_iso, matin=0)
if nbabs:
if nbabsjust:
warn_abs_lst.append("absent justifié l'après-midi !")
else:
warn_abs_lst.append("absent l'après-midi !")
e["absinfo"] = '<span class="sn_abs">' + " ".join(warn_abs_lst) + "</span> " e["absinfo"] = '<span class="sn_abs">' + warn_abs_lst + "</span> "
# Note actuelle de l'étudiant: # Note actuelle de l'étudiant:
if etudid in notes_db: if etudid in notes_db:

View File

@ -306,9 +306,9 @@ class SemSet(dict):
H.append("</p>") H.append("</p>")
if self["sem_id"] == 1: if self["sem_id"] == 1:
periode = "1re période (S1, S3)" periode = "1re période (S1, S3, S5)"
elif self["sem_id"] == 2: elif self["sem_id"] == 2:
periode = "2de période (S2, S4)" periode = "2de période (S2, S4, S6)"
else: else:
periode = "non semestrialisée (LP, ...). Incompatible avec BUT." periode = "non semestrialisée (LP, ...). Incompatible avec BUT."
@ -465,7 +465,7 @@ def do_semset_remove_sem(semset_id, formsemestre_id):
# ---------------------------------------- # ----------------------------------------
def semset_page(format="html"): def semset_page(fmt="html"):
"""Page avec liste semsets: """Page avec liste semsets:
Table avec : date_debut date_fin titre liste des semestres Table avec : date_debut date_fin titre liste des semestres
""" """
@ -514,8 +514,8 @@ def semset_page(format="html"):
filename="semsets", filename="semsets",
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
if format != "html": if fmt != "html":
return tab.make_page(format=format) return tab.make_page(fmt=fmt)
page_title = "Ensembles de semestres" page_title = "Ensembles de semestres"
H = [ H = [

View File

@ -66,7 +66,7 @@ def trombino(
group_ids=(), # liste des groupes à afficher group_ids=(), # liste des groupes à afficher
formsemestre_id=None, # utilisé si pas de groupes selectionné formsemestre_id=None, # utilisé si pas de groupes selectionné
etat=None, etat=None,
format="html", fmt="html",
dialog_confirmed=False, dialog_confirmed=False,
): ):
"""Trombinoscope""" """Trombinoscope"""
@ -78,18 +78,18 @@ def trombino(
) )
# #
if format != "html" and not dialog_confirmed: if fmt != "html" and not dialog_confirmed:
ok, dialog = check_local_photos_availability(groups_infos, fmt=format) ok, dialog = check_local_photos_availability(groups_infos, fmt=fmt)
if not ok: if not ok:
return dialog return dialog
if format == "zip": if fmt == "zip":
return _trombino_zip(groups_infos) return _trombino_zip(groups_infos)
elif format == "pdf": elif fmt == "pdf":
return _trombino_pdf(groups_infos) return _trombino_pdf(groups_infos)
elif format == "pdflist": elif fmt == "pdflist":
return _listeappel_photos_pdf(groups_infos) return _listeappel_photos_pdf(groups_infos)
elif format == "doc": elif fmt == "doc":
return sco_trombino_doc.trombino_doc(groups_infos) return sco_trombino_doc.trombino_doc(groups_infos)
else: else:
raise Exception("invalid format") raise Exception("invalid format")
@ -110,7 +110,7 @@ def trombino_html(groups_infos):
{ {
"title": "Obtenir archive Zip des photos", "title": "Obtenir archive Zip des photos",
"endpoint": "scolar.trombino", "endpoint": "scolar.trombino",
"args": {"group_ids": groups_infos.group_ids, "format": "zip"}, "args": {"group_ids": groups_infos.group_ids, "fmt": "zip"},
}, },
{ {
"title": "Recopier les photos depuis le portail", "title": "Recopier les photos depuis le portail",
@ -176,10 +176,10 @@ def trombino_html(groups_infos):
H.append( H.append(
f"""<div style="margin-bottom:15px;"> f"""<div style="margin-bottom:15px;">
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept, <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
format='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a> fmt='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a>
&nbsp;&nbsp; &nbsp;&nbsp;
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept, <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
format='doc', group_ids=groups_infos.group_ids)}">Version doc</a> fmt='doc', group_ids=groups_infos.group_ids)}">Version doc</a>
</div>""" </div>"""
) )
return "\n".join(H) return "\n".join(H)
@ -198,14 +198,14 @@ def check_local_photos_availability(groups_infos, fmt=""):
if not sco_photos.etud_photo_is_local(t["photo_filename"]): if not sco_photos.etud_photo_is_local(t["photo_filename"]):
nb_missing += 1 nb_missing += 1
if nb_missing > 0: if nb_missing > 0:
parameters = {"group_ids": groups_infos.group_ids, "format": fmt} parameters = {"group_ids": groups_infos.group_ids, "fmt": fmt}
return ( return (
False, False,
scu.confirm_dialog( scu.confirm_dialog(
f"""<p>Attention: {nb_missing} photos ne sont pas disponibles f"""<p>Attention: {nb_missing} photos ne sont pas disponibles
et ne peuvent pas être exportées.</p> et ne peuvent pas être exportées.</p>
<p>Vous pouvez <a class="stdlink" <p>Vous pouvez <a class="stdlink"
href="{groups_infos.base_url}&dialog_confirmed=1&format={fmt}" href="{groups_infos.base_url}&dialog_confirmed=1&fmt={fmt}"
>exporter seulement les photos existantes</a>""", >exporter seulement les photos existantes</a>""",
dest_url="trombino", dest_url="trombino",
OK="Exporter seulement les photos existantes", OK="Exporter seulement les photos existantes",

View File

@ -173,7 +173,7 @@ def evaluation_list_operations(evaluation_id):
return tab.make_page() return tab.make_page()
def formsemestre_list_saisies_notes(formsemestre_id, format="html"): def formsemestre_list_saisies_notes(formsemestre_id, fmt="html"):
"""Table listant toutes les opérations de saisies de notes, dans toutes """Table listant toutes les opérations de saisies de notes, dans toutes
les évaluations du semestre. les évaluations du semestre.
""" """
@ -194,7 +194,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
) )
# Formate les notes # Formate les notes
keep_numeric = format in scu.FORMATS_NUMERIQUES keep_numeric = fmt in scu.FORMATS_NUMERIQUES
for row in rows: for row in rows:
row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric) row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric)
row["date_evaluation"] = ( row["date_evaluation"] = (
@ -242,7 +242,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id), base_url="%s?formsemestre_id=%s" % (request.base_url, formsemestre_id),
origin=f"Généré par {sco_version.SCONAME} le " + scu.timedate_human_repr() + "", origin=f"Généré par {sco_version.SCONAME} le " + scu.timedate_human_repr() + "",
) )
return tab.make_page(format=format) return tab.make_page(fmt=fmt)
def get_note_history(evaluation_id, etudid, fmt=""): def get_note_history(evaluation_id, etudid, fmt=""):

View File

@ -240,7 +240,7 @@ def list_users(
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
return tab.make_page(format=fmt, with_html_headers=False) return tab.make_page(fmt=fmt, with_html_headers=False)
def get_users_count(dept=None) -> int: def get_users_count(dept=None) -> int:

View File

@ -237,7 +237,7 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
new_date: datetime.datetime = date new_date: datetime.datetime = date
if new_date.tzinfo is None: if new_date.tzinfo is None:
try: try:
new_date = timezone("Europe/Paris").localize(date) new_date = TIME_ZONE.localize(date)
except OverflowError: except OverflowError:
new_date = timezone("UTC").localize(date) new_date = timezone("UTC").localize(date)
return new_date return new_date
@ -670,8 +670,8 @@ def AbsencesURL():
def AssiduitesURL(): def AssiduitesURL():
"""URL of Assiduités""" """URL of Assiduités"""
return url_for("assiduites.index_html", scodoc_dept=g.scodoc_dept)[ return url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)[
: -len("/index_html") : -len("/BilanDept")
] ]
@ -879,10 +879,10 @@ DB_MIN_INT = -(1 << 31)
DB_MAX_INT = (1 << 31) - 1 DB_MAX_INT = (1 << 31) - 1
def bul_filename_old(sem: dict, etud: dict, format): def bul_filename_old(sem: dict, etud: dict, fmt):
"""Build a filename for this bulletin""" """Build a filename for this bulletin"""
dt = time.strftime("%Y-%m-%d") dt = time.strftime("%Y-%m-%d")
filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{format}" filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{fmt}"
filename = make_filename(filename) filename = make_filename(filename)
return filename return filename
@ -952,15 +952,15 @@ def sendXML(
def sendResult( def sendResult(
data, data,
name=None, name=None,
format=None, fmt=None,
force_outer_xml_tag=True, force_outer_xml_tag=True,
attached=False, attached=False,
quote_xml=False, quote_xml=False,
filename=None, filename=None,
): ):
if (format is None) or (format == "html"): if (fmt is None) or (fmt == "html"):
return data return data
elif format == "xml": # name is outer tagname elif fmt == "xml": # name is outer tagname
return sendXML( return sendXML(
data, data,
tagname=name, tagname=name,
@ -969,10 +969,10 @@ def sendResult(
quote=quote_xml, quote=quote_xml,
filename=filename, filename=filename,
) )
elif format == "json": elif fmt == "json":
return sendJSON(data, attached=attached, filename=filename) return sendJSON(data, attached=attached, filename=filename)
else: else:
raise ValueError("invalid format: %s" % format) raise ValueError(f"invalid format: {fmt}")
def send_file(data, filename="", suffix="", mime=None, attached=None): def send_file(data, filename="", suffix="", mime=None, attached=None):
@ -1035,9 +1035,7 @@ def get_request_args():
def json_error(status_code, message=None) -> Response: def json_error(status_code, message=None) -> Response:
"""Simple JSON for errors. """Simple JSON for errors."""
If as-response, returns Flask's Response. Otherwise returns a dict.
"""
payload = { payload = {
"error": HTTP_STATUS_CODES.get(status_code, "Unknown error"), "error": HTTP_STATUS_CODES.get(status_code, "Unknown error"),
"status": status_code, "status": status_code,

View File

@ -136,6 +136,8 @@
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
margin: 0 5%; margin: 0 5%;
cursor: pointer;
} }
.etud_row.def .nom::after, .etud_row.def .nom::after,
@ -268,6 +270,7 @@
background-size: cover; background-size: cover;
} }
.rbtn.present::before { .rbtn.present::before {
background-image: url(../icons/present.svg); background-image: url(../icons/present.svg);
} }
@ -285,8 +288,8 @@
} }
.rbtn:checked:before { .rbtn:checked:before {
outline: 3px solid #7059FF; outline: 5px solid #7059FF;
border-radius: 5px; border-radius: 50%;
} }
.rbtn:focus { .rbtn:focus {
@ -541,6 +544,17 @@
background-image: url(../icons/filter.svg); background-image: url(../icons/filter.svg);
} }
.download {
background-image: url(../icons/download.svg);
}
.iconline {
display: flex;
justify-content: flex-start;
gap: min(2%, 15px);
align-items: center;
}
[name='destroyFile'] { [name='destroyFile'] {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;

View File

@ -5,11 +5,17 @@
} }
} }
div.but_bul_court_links {
margin-left: 16px;
margin-bottom: 16px;
}
div.but_bul_court { div.but_bul_court {
width: 17cm; /* width: 17cm; */
display: grid; display: grid;
grid-template-columns: 6cm 11cm;
font-size: 11pt; font-size: 11pt;
grid-template-columns: 6cm 11cm;
margin-left: 16px;
} }
#infos_etudiant { #infos_etudiant {

View File

@ -28,7 +28,7 @@ main {
; ;
--couleurSurlignage: rgba(255, 253, 110, 0.49); --couleurSurlignage: rgba(255, 253, 110, 0.49);
max-width: 1000px; max-width: 1000px;
margin: auto; margin-left: 16px;
display: none; display: none;
} }

View File

@ -985,17 +985,6 @@ span.linktitresem a:visited {
color: red; color: red;
} }
.listegroupelink a:link {
color: blue;
}
.listegroupelink a:visited {
color: blue;
}
.listegroupelink a:hover {
color: red;
}
a.stdlink, a.stdlink,
a.stdlink:visited { a.stdlink:visited {
@ -1792,10 +1781,6 @@ td.formsemestre_status_inscrits {
text-align: center; text-align: center;
} }
div.formsemestre_status button {
margin-left: 12px;;
}
td.rcp_titre_sem a.jury_link { td.rcp_titre_sem a.jury_link {
margin-left: 8px; margin-left: 8px;
color: red; color: red;
@ -1857,15 +1842,54 @@ ul.ue_inscr_list li.etud {
margin-bottom: 5px; margin-bottom: 5px;
} }
#grouplists h4 { .sem-groups-abs {
background-color: rgb(137,137,137);
border-radius: 16px;
padding: 16px;
width: fit-content;
}
.sem-groups-abs h4 {
font-style: italic; font-style: italic;
margin-bottom: 0px; margin-bottom: 0px;
margin-top: 5px; margin-top: 5px;
} }
#grouplists table { .sem-groups-partition-titre {
/*border: 1px solid black;*/ margin-left: 4px;
border-spacing: 1px; font-size: 110%;
}
.sem-groups-partition {
background-color: rgb(213,203,183);
border-radius: 12px;
margin-bottom: 8px;
padding: 12px;
display: grid;
grid-template-columns: 240px auto;
}
.sem-groups-list, .sem-groups-assi {
background-color: white;
border-radius: 6px;
margin: 4px;
}
.sem-groups-list > div {
margin: 4px;
}
.sem-groups-assi > div {
margin: 6px 8px 6px 8px;
}
.sem-groups-assi {
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: wrap;
align-items: center;
}
.sem-groups-none {
grid-column: 1 / span 2;
} }
/* Tableau de bord module */ /* Tableau de bord module */
@ -3077,7 +3101,7 @@ div.bul_foot {
border-radius: 16px; border-radius: 16px;
border: 1px solid #AAA; border: 1px solid #AAA;
padding: 16px 32px; padding: 16px 32px;
margin: auto; margin-left: 16px;
} }
div.bull_appreciations { div.bull_appreciations {
@ -3182,6 +3206,9 @@ table.abs_form_table tr:hover td {
border: 1px solid red; border: 1px solid red;
} }
.ul_abs button {
margin-bottom: 6px;
}
/* ----- Formulator ------- */ /* ----- Formulator ------- */
ul.tf-msg { ul.tf-msg {

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000"><path d="M6 20h12M12 4v12m0 0l3.5-3.5M12 16l-3.5-3.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path></svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@ -162,6 +162,7 @@ function uniqueCheckBox(box) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec * @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/ */
function sync_get(path, success, errors) { function sync_get(path, success, errors) {
console.log("sync_get " + path);
$.ajax({ $.ajax({
async: false, async: false,
type: "GET", type: "GET",
@ -177,6 +178,7 @@ function sync_get(path, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec * @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/ */
function async_get(path, success, errors) { function async_get(path, success, errors) {
console.log("async_get " + path);
$.ajax({ $.ajax({
async: true, async: true,
type: "GET", type: "GET",
@ -193,6 +195,7 @@ function async_get(path, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec * @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/ */
function sync_post(path, data, success, errors) { function sync_post(path, data, success, errors) {
console.log("sync_post " + path);
$.ajax({ $.ajax({
async: false, async: false,
type: "POST", type: "POST",
@ -210,6 +213,7 @@ function sync_post(path, data, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec * @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/ */
function async_post(path, data, success, errors) { function async_post(path, data, success, errors) {
console.log("sync_post " + path);
return $.ajax({ return $.ajax({
async: true, async: true,
type: "POST", type: "POST",
@ -577,7 +581,7 @@ function updateDate() {
return true; return true;
} else { } else {
const att = document.createTextNode( const att = document.createTextNode(
"Le jour sélectionné n'est pas un jour travaillé." `Le jour sélectionné (${formatDate(date)}) n'est pas un jour travaillé.`
); );
openAlertModal("Erreur", att, "", "crimson"); openAlertModal("Erreur", att, "", "crimson");
dateInput.value = dateInput.getAttribute("value"); dateInput.value = dateInput.getAttribute("value");
@ -611,7 +615,9 @@ function setupDate(onchange = null) {
datestr.addEventListener("click", () => { datestr.addEventListener("click", () => {
if (!input.disabled) { if (!input.disabled) {
input.showPicker(); try {
input.showPicker();
} catch {}
} }
}); });
@ -809,13 +815,10 @@ function numberTimeToDate(nb) {
* - du semestre * - du semestre
* - de la date courant et du jour précédent. * - de la date courant et du jour précédent.
* @param {boolean} clear vidage de l'objet "assiduites" ou non * @param {boolean} clear vidage de l'objet "assiduites" ou non
* @returns {object} l'objets Assiduités {<etudid:str> : [<assiduite>,]} * @returns {object} l'objet Assiduités {<etudid:str> : [<assiduite>,]}
*/ */
function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) { function getAssiduitesFromEtuds(clear, deb, fin) {
const etudIds = Object.keys(etuds).join(","); const etudIds = Object.keys(etuds).join(",");
const formsemestre_id = has_formsemestre
? `formsemestre_id=${getFormSemestreId()}&`
: "";
const date_debut = deb ? deb : toIsoString(getPrevDate()); const date_debut = deb ? deb : toIsoString(getPrevDate());
const date_fin = fin ? fin : toIsoString(getNextDate()); const date_fin = fin ? fin : toIsoString(getNextDate());
@ -826,7 +829,7 @@ function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) {
const url_api = const url_api =
getUrl() + getUrl() +
`/api/assiduites/group/query?date_debut=${date_debut}&${formsemestre_id}&date_fin=${date_fin}&etudids=${etudIds}`; `/api/assiduites/group/query?date_debut=${date_debut}&date_fin=${date_fin}&etudids=${etudIds}`;
sync_get(url_api, (data, status) => { sync_get(url_api, (data, status) => {
if (status === "success") { if (status === "success") {
const dataKeys = Object.keys(data); const dataKeys = Object.keys(data);
@ -924,14 +927,11 @@ function deleteAssiduite(assiduite_id) {
function hasModuleImpl(assiduite) { function hasModuleImpl(assiduite) {
if (assiduite.moduleimpl_id != null) return true; if (assiduite.moduleimpl_id != null) return true;
if ( return (
"external_data" in assiduite && assiduite.hasOwnProperty("external_data") &&
assiduite.external_data instanceof Object && assiduite.external_data != null &&
"module" in assiduite.external_data assiduite.external_data.hasOwnProperty("module")
) );
return true;
return false;
} }
/** /**
@ -942,6 +942,15 @@ function hasModuleImpl(assiduite) {
* TODO : Rendre asynchrone * TODO : Rendre asynchrone
*/ */
function editAssiduite(assiduite_id, etat, assi) { function editAssiduite(assiduite_id, etat, assi) {
if (assi.length != 1 || !assi[0].hasOwnProperty("assiduite_id")) {
const html = `
<h3>Aucune assiduité n'a être éditée</h3>
`;
const div = document.createElement("div");
div.innerHTML = html;
openAlertModal("Erreur", div);
return;
}
let assiduite = { let assiduite = {
etat: etat, etat: etat,
external_data: assi ? assi.external_data : null, external_data: assi ? assi.external_data : null,
@ -1057,16 +1066,13 @@ function getAssiduiteValue(field) {
* Mise à jour des assiduités d'un étudiant * Mise à jour des assiduités d'un étudiant
* @param {String | Number} etudid identifiant de l'étudiant * @param {String | Number} etudid identifiant de l'étudiant
*/ */
function actualizeEtudAssiduite(etudid, has_formsemestre = true) { function actualizeEtudAssiduite(etudid) {
const formsemestre_id = has_formsemestre
? `formsemestre_id=${getFormSemestreId()}&`
: "";
const date_debut = toIsoString(getPrevDate()); const date_debut = toIsoString(getPrevDate());
const date_fin = toIsoString(getNextDate()); const date_fin = toIsoString(getNextDate());
const url_api = const url_api =
getUrl() + getUrl() +
`/api/assiduites/${etudid}/query?${formsemestre_id}date_debut=${date_debut}&date_fin=${date_fin}`; `/api/assiduites/${etudid}/query?date_debut=${date_debut}&date_fin=${date_fin}`;
sync_get(url_api, (data, status) => { sync_get(url_api, (data, status) => {
if (status === "success") { if (status === "success") {
assiduites[etudid] = data; assiduites[etudid] = data;
@ -1074,8 +1080,22 @@ function actualizeEtudAssiduite(etudid, has_formsemestre = true) {
}); });
} }
function getAllAssiduitesFromEtud(etudid, action) { function getAllAssiduitesFromEtud(
const url_api = getUrl() + `/api/assiduites/${etudid}`; etudid,
action,
order = false,
justifs = false,
courant = false
) {
const url_api =
getUrl() +
`/api/assiduites/${etudid}${
order
? "/query?order%°"
.replace("%", justifs ? "&with_justifs" : "")
.replace("°", courant ? "&courant" : "")
: ""
}`;
$.ajax({ $.ajax({
async: true, async: true,
@ -1136,9 +1156,7 @@ function assiduiteAction(element) {
done = editAssiduite( done = editAssiduite(
assiduite_id, assiduite_id,
etat, etat,
assiduites[etudid].reduce((a) => { assiduites[etudid].filter((a) => a.assiduite_id == assiduite_id)
if (a.assiduite_id == assiduite_id) return a;
})
); );
} }
break; break;
@ -1249,12 +1267,10 @@ function generateEtudRow(
<img class="pdp" src="${pdp_url}"> <img class="pdp" src="${pdp_url}">
<div class="name_set"> <a class="name_set" href="BilanEtud?etudid=${etud.id}">
<h4 class="nom">${etud.nom}</h4> <h4 class="nom">${etud.nom}</h4>
<h5 class="prenom">${etud.prenom}</h5> <h5 class="prenom">${etud.prenom}</h5>
</a>
</div>
</div> </div>
<div class="assiduites_bar"> <div class="assiduites_bar">
@ -1331,7 +1347,7 @@ function insertEtudRow(etud, index, output = false) {
* @param {String | Number} etudid l'identifiant de l'étudiant * @param {String | Number} etudid l'identifiant de l'étudiant
*/ */
function actualizeEtud(etudid) { function actualizeEtud(etudid) {
actualizeEtudAssiduite(etudid, !isSingleEtud()); actualizeEtudAssiduite(etudid);
//Actualize row //Actualize row
const etudHolder = document.querySelector(".etud_holder"); const etudHolder = document.querySelector(".etud_holder");
const ancient_row = document.getElementById(`etud_row_${etudid}`); const ancient_row = document.getElementById(`etud_row_${etudid}`);
@ -1412,10 +1428,10 @@ function setModuleImplId(assiduite, module = null) {
const moduleimpl = module == null ? getModuleImplId() : module; const moduleimpl = module == null ? getModuleImplId() : module;
if (moduleimpl === "autre") { if (moduleimpl === "autre") {
if ( if (
"external_data" in assiduite && assiduite.hasOwnProperty("external_data") &&
assiduite.external_data instanceof Object assiduite.external_data != null
) { ) {
if ("module" in assiduite.external_data) { if (assiduite.external_data.hasOwnProperty("module")) {
assiduite.external_data.module = "Autre"; assiduite.external_data.module = "Autre";
} else { } else {
assiduite["external_data"] = { module: "Autre" }; assiduite["external_data"] = { module: "Autre" };
@ -1427,10 +1443,10 @@ function setModuleImplId(assiduite, module = null) {
} else { } else {
assiduite["moduleimpl_id"] = moduleimpl; assiduite["moduleimpl_id"] = moduleimpl;
if ( if (
"external_data" in assiduite && assiduite.hasOwnProperty("external_data") &&
assiduite.external_data instanceof Object assiduite.external_data != null
) { ) {
if ("module" in assiduite.external_data) { if (assiduite.external_data.hasOwnProperty("module")) {
delete assiduite.external_data.module; delete assiduite.external_data.module;
} }
} }
@ -1482,9 +1498,9 @@ function getCurrentAssiduiteModuleImplId() {
let mod = currentAssiduites[0].moduleimpl_id; let mod = currentAssiduites[0].moduleimpl_id;
if ( if (
mod == null && mod == null &&
"external_data" in currentAssiduites[0] && currentAssiduites[0].hasOwnProperty("external_data") &&
currentAssiduites[0].external_data instanceof Object && currentAssiduites[0].external_data != null &&
"module" in currentAssiduites[0].external_data currentAssiduites[0].external_data.hasOwnProperty("module")
) { ) {
mod = currentAssiduites[0].external_data.module; mod = currentAssiduites[0].external_data.module;
} }
@ -1567,20 +1583,18 @@ function fastJustify(assiduite) {
//créer justificatif //créer justificatif
const justif = { const justif = {
date_debut: new moment.tz(assiduite.date_debut, TIMEZONE) date_debut: new moment.tz(assiduite.date_debut, TIMEZONE).format(),
.add(1, "s") date_fin: new moment.tz(assiduite.date_fin, TIMEZONE).format(),
.format(),
date_fin: new moment.tz(assiduite.date_fin, TIMEZONE)
.subtract(1, "s")
.format(),
raison: raison, raison: raison,
etat: etat, etat: etat,
}; };
createJustificatif(justif); createJustificatif(justif);
// justifyAssiduite(assiduite.assiduite_id, true);
generateAllEtudRow(); generateAllEtudRow();
try {
loadAll();
} catch {}
}; };
const content = document.createElement("fieldset"); const content = document.createElement("fieldset");
@ -1643,8 +1657,17 @@ function createJustificatif(justif, success = () => {}) {
}); });
} }
function getAllJustificatifsFromEtud(etudid, action) { function getAllJustificatifsFromEtud(
const url_api = getUrl() + `/api/justificatifs/${etudid}`; etudid,
action,
order = false,
courant = false
) {
const url_api =
getUrl() +
`/api/justificatifs/${etudid}${
order ? "/query?order°".replace("°", courant ? "&courant" : "") : ""
}`;
$.ajax({ $.ajax({
async: true, async: true,
type: "GET", type: "GET",
@ -1696,9 +1719,9 @@ function getModuleImpl(assiduite) {
if (id == null || id == undefined) { if (id == null || id == undefined) {
if ( if (
"external_data" in assiduite && assiduite.hasOwnProperty("external_data") &&
assiduite.external_data instanceof Object && assiduite.external_data != null &&
"module" in assiduite.external_data assiduite.external_data.hasOwnProperty("module")
) { ) {
return assiduite.external_data.module; return assiduite.external_data.module;
} else { } else {
@ -1724,10 +1747,12 @@ function getModuleImpl(assiduite) {
} }
function getUser(obj) { function getUser(obj) {
if ("external_data" in obj && obj.external_data != null) { if (
if ("enseignant" in obj.external_data) { obj.hasOwnProperty("external_data") &&
return obj.external_data.enseignant; obj.external_data != null &&
} obj.external_data.hasOwnProperty("enseignant")
) {
return obj.external_data.enseignant;
} }
return obj.user_id; return obj.user_id;

View File

@ -4,135 +4,165 @@
// console.log('etud_debouche.js loaded'); // console.log('etud_debouche.js loaded');
$(function () { $(function () {
display_itemsuivis(false); display_itemsuivis(false);
}); });
function display_itemsuivis(active) { function display_itemsuivis(active) {
var etudid = $('div#fichedebouche').data("etudid"); var etudid = $("div#fichedebouche").data("etudid");
var readonly = $('div#fichedebouche').data('readonly'); // present ro interface var readonly = $("div#fichedebouche").data("readonly"); // present ro interface
if (!readonly) { if (!readonly) {
$('#adddebouchelink').off("click").click(function (e) { $("#adddebouchelink")
e.preventDefault(); .off("click")
$.post(SCO_URL + "/itemsuivi_create", { etudid: etudid, format: 'json' }).done(item_insert_new); .click(function (e) {
e.preventDefault();
$.post(SCO_URL + "/itemsuivi_create", {
etudid: etudid,
fmt: "json",
}).done(item_insert_new);
return false; return false;
}); });
}
// add existing items
$.get(
SCO_URL + "/itemsuivi_list_etud",
{ etudid: etudid, fmt: "json" },
function (L) {
for (var i in L) {
item_insert(
L[i]["itemsuivi_id"],
L[i]["item_date"],
L[i]["situation"],
L[i]["tags"],
readonly
);
}
} }
// add existing items );
$.get(SCO_URL + "/itemsuivi_list_etud", { etudid: etudid, format: 'json' }, function (L) {
for (var i in L) {
item_insert(L[i]['itemsuivi_id'], L[i]['item_date'], L[i]['situation'], L[i]['tags'], readonly);
}
});
$("div#fichedebouche").accordion({ $("div#fichedebouche").accordion({
heightStyle: "content", heightStyle: "content",
collapsible: true, collapsible: true,
active: active, active: active,
}); });
} }
function item_insert_new(it) { function item_insert_new(it) {
item_insert(it.itemsuivi_id, it.item_date, it.situation, '', false); item_insert(it.itemsuivi_id, it.item_date, it.situation, "", false);
} }
function item_insert(itemsuivi_id, item_date, situation, tags, readonly) { function item_insert(itemsuivi_id, item_date, situation, tags, readonly) {
if (item_date === undefined) if (item_date === undefined) item_date = Date2DMY(new Date());
item_date = Date2DMY(new Date()); if (situation === undefined) situation = "";
if (situation === undefined) if (tags === undefined) tags = "";
situation = '';
if (tags === undefined)
tags = '';
var nodes = item_nodes(itemsuivi_id, item_date, situation, tags, readonly); var nodes = item_nodes(itemsuivi_id, item_date, situation, tags, readonly);
// insert just before last li: // insert just before last li:
if ($('ul.listdebouches li.adddebouche').length > 0) { if ($("ul.listdebouches li.adddebouche").length > 0) {
$('ul.listdebouches').children(':last').before(nodes); $("ul.listdebouches").children(":last").before(nodes);
} else { } else {
// mode readonly, pas de li "ajouter" // mode readonly, pas de li "ajouter"
$('ul.listdebouches').append(nodes); $("ul.listdebouches").append(nodes);
} }
}; }
function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) { function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
// console.log('item_nodes: itemsuivi_id=' + itemsuivi_id); // console.log('item_nodes: itemsuivi_id=' + itemsuivi_id);
var sel_mois = 'Situation à la date du <input type="text" class="itemsuividatepicker" size="10" value="' + item_date + '"/><span class="itemsuivi_suppress" onclick="itemsuivi_suppress(\'' + itemsuivi_id + '\')"><img width="10" height="9" border="0" title="" alt="supprimer cet item" src="/ScoDoc/static/icons/delete_small_img.png"/></span>'; var sel_mois =
'Situation à la date du <input type="text" class="itemsuividatepicker" size="10" value="' +
item_date +
'"/><span class="itemsuivi_suppress" onclick="itemsuivi_suppress(\'' +
itemsuivi_id +
'\')"><img width="10" height="9" border="0" title="" alt="supprimer cet item" src="/ScoDoc/static/icons/delete_small_img.png"/></span>';
var h = sel_mois; var h = sel_mois;
// situation // situation
h += '<div class="itemsituation editable" data-type="textarea" data-url="itemsuivi_set_situation" data-placeholder="<em>décrire situation...</em>" data-object="' + itemsuivi_id + '">' + situation + '</div>'; h +=
// tags: '<div class="itemsituation editable" data-type="textarea" data-url="itemsuivi_set_situation" data-placeholder="<em>décrire situation...</em>" data-object="' +
h += '<div class="itemsuivi_tag_edit"><textarea class="itemsuivi_tag_editor">' + tags + '</textarea></div>'; itemsuivi_id +
'">' +
situation +
"</div>";
// tags:
h +=
'<div class="itemsuivi_tag_edit"><textarea class="itemsuivi_tag_editor">' +
tags +
"</textarea></div>";
var nodes = $($.parseHTML('<li class="itemsuivi">' + h + '</li>')); var nodes = $($.parseHTML('<li class="itemsuivi">' + h + "</li>"));
var dp = nodes.find('.itemsuividatepicker'); var dp = nodes.find(".itemsuividatepicker");
dp.blur(function (e) { dp.blur(function (e) {
var date = this.value; var date = this.value;
// console.log('selected text: ' + date); // console.log('selected text: ' + date);
$.post(SCO_URL + "/itemsuivi_set_date", { item_date: date, itemsuivi_id: itemsuivi_id }); $.post(SCO_URL + "/itemsuivi_set_date", {
item_date: date,
itemsuivi_id: itemsuivi_id,
}); });
dp.datepicker({ });
onSelect: function (date, instance) { dp.datepicker({
// console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id); onSelect: function (date, instance) {
$.post(SCO_URL + "/itemsuivi_set_date", { item_date: date, itemsuivi_id: itemsuivi_id }); // console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id);
}, $.post(SCO_URL + "/itemsuivi_set_date", {
showOn: 'button', item_date: date,
buttonImage: '/ScoDoc/static/icons/calendar_img.png', itemsuivi_id: itemsuivi_id,
buttonImageOnly: true, });
dateFormat: 'dd/mm/yy', },
duration: 'fast', showOn: "button",
disabled: readonly buttonImage: "/ScoDoc/static/icons/calendar_img.png",
}); buttonImageOnly: true,
dp.datepicker('option', $.extend({ showMonthAfterYear: false }, dateFormat: "dd/mm/yy",
$.datepicker.regional['fr'])); duration: "fast",
disabled: readonly,
});
dp.datepicker(
"option",
$.extend({ showMonthAfterYear: false }, $.datepicker.regional["fr"])
);
if (readonly) { if (readonly) {
// show tags read-only // show tags read-only
readOnlyTags(nodes.find('.itemsuivi_tag_editor')); readOnlyTags(nodes.find(".itemsuivi_tag_editor"));
} } else {
else { // bind tag editor
// bind tag editor nodes.find(".itemsuivi_tag_editor").tagEditor({
nodes.find('.itemsuivi_tag_editor').tagEditor({ initialTags: "",
initialTags: '', placeholder: "Tags...",
placeholder: 'Tags...', onChange: function (field, editor, tags) {
onChange: function (field, editor, tags) { $.post("itemsuivi_tag_set", {
$.post('itemsuivi_tag_set', itemsuivi_id: itemsuivi_id,
{ taglist: tags.join(),
itemsuivi_id: itemsuivi_id,
taglist: tags.join()
});
},
autocomplete: {
delay: 200, // ms before suggest
position: { collision: 'flip' }, // automatic menu position up/down
source: "itemsuivi_tag_search"
},
}); });
},
autocomplete: {
delay: 200, // ms before suggest
position: { collision: "flip" }, // automatic menu position up/down
source: "itemsuivi_tag_search",
},
});
// bind inplace editor // bind inplace editor
nodes.find('div.itemsituation').jinplace(); nodes.find("div.itemsituation").jinplace();
} }
return nodes; return nodes;
}; }
function Date2DMY(date) { function Date2DMY(date) {
var year = date.getFullYear(); var year = date.getFullYear();
var month = (1 + date.getMonth()).toString(); var month = (1 + date.getMonth()).toString();
month = month.length > 1 ? month : '0' + month; month = month.length > 1 ? month : "0" + month;
var day = date.getDate().toString(); var day = date.getDate().toString();
day = day.length > 1 ? day : '0' + day; day = day.length > 1 ? day : "0" + day;
return day + '/' + month + '/' + year; return day + "/" + month + "/" + year;
} }
function itemsuivi_suppress(itemsuivi_id) { function itemsuivi_suppress(itemsuivi_id) {
$.post(SCO_URL + "/itemsuivi_suppress", { itemsuivi_id: itemsuivi_id }); $.post(SCO_URL + "/itemsuivi_suppress", { itemsuivi_id: itemsuivi_id });
// Clear items and rebuild: // Clear items and rebuild:
$("ul.listdebouches li.itemsuivi").remove(); $("ul.listdebouches li.itemsuivi").remove();
display_itemsuivis(0); display_itemsuivis(0);
} }

View File

@ -6,12 +6,11 @@
// Ce code utilise d3.js // Ce code utilise d3.js
$().ready(function () { $().ready(function () {
var etudid = $("#etudid")[0].value; var etudid = $("#etudid")[0].value;
var formsemestre_id = $("#formsemestre_id")[0].value; var formsemestre_id = $("#formsemestre_id")[0].value;
get_notes_and_draw(formsemestre_id, etudid); get_notes_and_draw(formsemestre_id, etudid);
}); });
var WIDTH = 460; // taille du canvas SVG var WIDTH = 460; // taille du canvas SVG
var HEIGHT = WIDTH; var HEIGHT = WIDTH;
var CX = WIDTH / 2; // coordonnees centre du cercle var CX = WIDTH / 2; // coordonnees centre du cercle
@ -24,258 +23,314 @@ var R_AXIS_TICS = [4, 6, 8, 10, 12, 14, 16, 18, 20];
var NB_TICS = R_TICS.length; var NB_TICS = R_TICS.length;
function get_notes_and_draw(formsemestre_id, etudid) { function get_notes_and_draw(formsemestre_id, etudid) {
console.log("get_notes(" + formsemestre_id + ", " + etudid + " )"); console.log("get_notes(" + formsemestre_id + ", " + etudid + " )");
/* Recupère le bulletin de note et extrait tableau de notes */ /* Recupère le bulletin de note et extrait tableau de notes */
/* /*
var notes = [ var notes = [
{ 'module' : 'E1', { 'module' : 'E1',
'note' : 13, 'note' : 13,
'moy' : 16 }, 'moy' : 16 },
]; ];
*/ */
var query = SCO_URL + "/Notes/formsemestre_bulletinetud?formsemestre_id=" + formsemestre_id + "&etudid=" + etudid + "&format=json&version=selectedevals&force_publishing=1" var query =
SCO_URL +
"/Notes/formsemestre_bulletinetud?formsemestre_id=" +
formsemestre_id +
"&etudid=" +
etudid +
"&fmt=json&version=selectedevals&force_publishing=1";
$.get(query, '', function (bul) { $.get(query, "", function (bul) {
var notes = []; var notes = [];
bul.ue.forEach( bul.ue.forEach(function (ue, i, ues) {
function (ue, i, ues) { ue["module"].forEach(function (m, i) {
ue['module'].forEach(function (m, i) { notes.push({
notes.push({ code: m["code"],
'code': m['code'], titre: m["titre"],
'titre': m['titre'], note: m["note"]["value"],
'note': m['note']['value'], moy: m["note"]["moy"],
'moy': m['note']['moy'] });
}); });
});
});
draw_radar(notes);
}); });
draw_radar(notes);
});
} }
function draw_radar(notes) { function draw_radar(notes) {
/* Calcul coordonnées des éléments */ /* Calcul coordonnées des éléments */
var nmod = notes.length; var nmod = notes.length;
var angle = 2 * Math.PI / nmod; var angle = (2 * Math.PI) / nmod;
for (var i = 0; i < notes.length; i++) { for (var i = 0; i < notes.length; i++) {
var d = notes[i]; var d = notes[i];
var cx = Math.sin(i * angle); var cx = Math.sin(i * angle);
var cy = - Math.cos(i * angle); var cy = -Math.cos(i * angle);
d["x_v"] = CX + RR * d.note / 20 * cx; d["x_v"] = CX + ((RR * d.note) / 20) * cx;
d["y_v"] = CY + RR * d.note / 20 * cy; d["y_v"] = CY + ((RR * d.note) / 20) * cy;
d["x_moy"] = CX + RR * d.moy / 20 * cx; d["x_moy"] = CX + ((RR * d.moy) / 20) * cx;
d["y_moy"] = CY + RR * d.moy / 20 * cy; d["y_moy"] = CY + ((RR * d.moy) / 20) * cy;
d["x_20"] = CX + RR * cx; d["x_20"] = CX + RR * cx;
d["y_20"] = CY + RR * cy; d["y_20"] = CY + RR * cy;
d["x_label"] = CX + (RR + 25) * cx - 10 d["x_label"] = CX + (RR + 25) * cx - 10;
d["y_label"] = CY + (RR + 25) * cy + 10; d["y_label"] = CY + (RR + 25) * cy + 10;
d["tics"] = []; d["tics"] = [];
// Coords des tics sur chaque axe // Coords des tics sur chaque axe
for (var j = 0; j < NB_TICS; j++) {
var r = R_TICS[j] / 20 * RR;
d["tics"][j] = { "x": CX + r * cx, "y": CY + r * cy };
}
}
var notes_circ = notes.slice(0);
notes_circ.push(notes[0])
var notes_circ_valid = notes_circ.filter(function (e, i, a) { return e.note != 'NA' && e.note != '-'; });
var notes_valid = notes.filter(function (e, i, a) { return e.note != 'NA' && e.note != '-'; })
/* Crée l'élément SVG */
g = d3.select("#radar_bulletin").append("svg")
.attr("class", "radar")
.attr("width", WIDTH + 100)
.attr("height", HEIGHT);
/* Centre */
g.append("circle").attr("cy", CY)
.attr("cx", CX)
.attr("r", 2)
.attr("class", "radar_center_mark");
/* Lignes "tics" */
for (var j = 0; j < NB_TICS; j++) { for (var j = 0; j < NB_TICS; j++) {
var ligne_tics = d3.svg.line() var r = (R_TICS[j] / 20) * RR;
.x(function (d) { return d["tics"][j]["x"]; }) d["tics"][j] = { x: CX + r * cx, y: CY + r * cy };
.y(function (d) { return d["tics"][j]["y"]; });
g.append("svg:path")
.attr("class", "radar_disk_tic")
.attr("id", "radar_disk_tic_" + R_TICS[j])
.attr("d", ligne_tics(notes_circ));
} }
}
/* Lignes radiales pour chaque module */ var notes_circ = notes.slice(0);
g.selectAll("radar_rad") notes_circ.push(notes[0]);
.data(notes) var notes_circ_valid = notes_circ.filter(function (e, i, a) {
.enter().append("line") return e.note != "NA" && e.note != "-";
.attr("x1", CX) });
.attr("y1", CY) var notes_valid = notes.filter(function (e, i, a) {
.attr("x2", function (d) { return d["x_20"]; }) return e.note != "NA" && e.note != "-";
.attr("y2", function (d) { return d["y_20"]; }) });
.attr("class", "radarrad");
/* Crée l'élément SVG */
g = d3
.select("#radar_bulletin")
.append("svg")
.attr("class", "radar")
.attr("width", WIDTH + 100)
.attr("height", HEIGHT);
/* Lignes entre notes */ /* Centre */
var ligne = d3.svg.line() g.append("circle")
.x(function (d) { return d["x_v"]; }) .attr("cy", CY)
.y(function (d) { return d["y_v"]; }); .attr("cx", CX)
.attr("r", 2)
.attr("class", "radar_center_mark");
/* Lignes "tics" */
for (var j = 0; j < NB_TICS; j++) {
var ligne_tics = d3.svg
.line()
.x(function (d) {
return d["tics"][j]["x"];
})
.y(function (d) {
return d["tics"][j]["y"];
});
g.append("svg:path") g.append("svg:path")
.attr("class", "radarnoteslines") .attr("class", "radar_disk_tic")
.attr("d", ligne(notes_circ_valid)); .attr("id", "radar_disk_tic_" + R_TICS[j])
.attr("d", ligne_tics(notes_circ));
}
var ligne_moy = d3.svg.line() /* Lignes radiales pour chaque module */
.x(function (d) { return d["x_moy"]; }) g.selectAll("radar_rad")
.y(function (d) { return d["y_moy"]; }) .data(notes)
.enter()
.append("line")
.attr("x1", CX)
.attr("y1", CY)
.attr("x2", function (d) {
return d["x_20"];
})
.attr("y2", function (d) {
return d["y_20"];
})
.attr("class", "radarrad");
g.append("svg:path") /* Lignes entre notes */
.attr("class", "radarmoylines") var ligne = d3.svg
.attr("d", ligne_moy(notes_circ_valid)); .line()
.x(function (d) {
return d["x_v"];
})
.y(function (d) {
return d["y_v"];
});
/* Points (notes) */ g.append("svg:path")
g.selectAll("circle1") .attr("class", "radarnoteslines")
.data(notes_valid) .attr("d", ligne(notes_circ_valid));
.enter().append("circle")
.attr("cx", function (d) { return d["x_v"]; })
.attr("cy", function (d) { return d["y_v"]; })
.attr("r", function (x, i) { return 3; })
.style("stroke-width", 1)
.style("stroke", "black")
.style("fill", "blue")
.on("mouseover", function (d) {
var rwidth = 310;
var x = d["x_v"];
if ((x - CX) < 0) {
x = x + 5;
if (x + rwidth + 12 > WIDTH) {
x = WIDTH - rwidth - 12;
}
}
else {
if ((x - CX) > 0) {
x = x - rwidth - 5;
if (x < 12) {
x = 12;
}
}
else {
x = CX - rwidth / 2;
}
}
var yrect = d["y_v"];
var ytext = d["y_v"];
if ((yrect - CY) > 0) {
yrect = yrect - 5 - 20;
ytext = ytext - 5 - 20 + 16;
}
else {
yrect = yrect + 5;
ytext = ytext + 5 + 16;
}
var r = g.append("rect")
.attr('class', 'radartip')
.attr("x", x)
.attr("y", yrect);
var txt = g.append("text").text("Note: " + d.note + "/20, moyenne promo: " + d.moy + "/20") var ligne_moy = d3.svg
.attr('class', 'radartip') .line()
.attr("x", x + 5) .x(function (d) {
.attr("y", ytext); return d["x_moy"];
r.attr("width", rwidth).attr("height", 20); })
}) .y(function (d) {
.on("mouseout", function (d) { return d["y_moy"];
d3.selectAll(".radartip").remove() });
});
/* Valeurs des notes */ g.append("svg:path")
g.selectAll("notes_labels") .attr("class", "radarmoylines")
.data(notes_valid) .attr("d", ligne_moy(notes_circ_valid));
.enter().append("text")
.text(function (d) { return d["note"]; })
.attr("x", function (d) {
return d["x_v"];
})
.attr("y", function (d) {
if (d["y_v"] > CY)
return d["y_v"] + 16;
else
return d["y_v"] - 8;
})
.attr("class", "note_label");
/* Petits points sur les moyennes */ /* Points (notes) */
g.selectAll("circle2") g.selectAll("circle1")
.data(notes_valid) .data(notes_valid)
.enter().append("circle") .enter()
.attr("cx", function (d) { return d["x_moy"]; }) .append("circle")
.attr("cy", function (d) { return d["y_moy"]; }) .attr("cx", function (d) {
.attr("r", function (x, i) { return 2; }) return d["x_v"];
.style("stroke-width", 0) })
.style("stroke", "black") .attr("cy", function (d) {
.style("fill", "rgb(20,90,50)"); return d["y_v"];
})
.attr("r", function (x, i) {
return 3;
})
.style("stroke-width", 1)
.style("stroke", "black")
.style("fill", "blue")
.on("mouseover", function (d) {
var rwidth = 310;
var x = d["x_v"];
if (x - CX < 0) {
x = x + 5;
if (x + rwidth + 12 > WIDTH) {
x = WIDTH - rwidth - 12;
}
} else {
if (x - CX > 0) {
x = x - rwidth - 5;
if (x < 12) {
x = 12;
}
} else {
x = CX - rwidth / 2;
}
}
var yrect = d["y_v"];
var ytext = d["y_v"];
if (yrect - CY > 0) {
yrect = yrect - 5 - 20;
ytext = ytext - 5 - 20 + 16;
} else {
yrect = yrect + 5;
ytext = ytext + 5 + 16;
}
var r = g
.append("rect")
.attr("class", "radartip")
.attr("x", x)
.attr("y", yrect);
/* Valeurs sur axe */ var txt = g
g.selectAll("textaxis") .append("text")
.data(R_AXIS_TICS) .text("Note: " + d.note + "/20, moyenne promo: " + d.moy + "/20")
.enter().append("text") .attr("class", "radartip")
.text(String) .attr("x", x + 5)
.attr("x", CX - 10) .attr("y", ytext);
.attr("y", function (x, i) { return CY - x * RR / 20 + 6; }) r.attr("width", rwidth).attr("height", 20);
.attr("class", "textaxis"); })
.on("mouseout", function (d) {
d3.selectAll(".radartip").remove();
});
/* Noms des modules */ /* Valeurs des notes */
g.selectAll("text_modules") g.selectAll("notes_labels")
.data(notes) .data(notes_valid)
.enter().append("text") .enter()
.text(function (d) { return d['code']; }) .append("text")
.attr("x", function (d) { return d['x_label']; }) .text(function (d) {
.attr("y", function (d) { return d['y_label']; }) return d["note"];
.attr("dx", 0) })
.attr("dy", 0) .attr("x", function (d) {
.on("mouseover", function (d) { return d["x_v"];
var x = d["x_label"]; })
var yrect = d["y_label"]; .attr("y", function (d) {
var ytext = d["y_label"]; if (d["y_v"] > CY) return d["y_v"] + 16;
var titre = d['titre'].replace("&apos;", "'").substring(0, 64); else return d["y_v"] - 8;
var rwidth = titre.length * 9; // rough estimate of string width in pixels })
if ((x - CX) < 0) { .attr("class", "note_label");
x = x + 5;
if (x + rwidth + 12 > WIDTH) { /* Petits points sur les moyennes */
x = WIDTH - rwidth - 12; g.selectAll("circle2")
} .data(notes_valid)
} .enter()
else { .append("circle")
if ((x - CX) > 0) { .attr("cx", function (d) {
x = x - rwidth - 5; return d["x_moy"];
if (x < 12) { })
x = 12; .attr("cy", function (d) {
} return d["y_moy"];
} })
else { .attr("r", function (x, i) {
x = CX - rwidth / 2; return 2;
} })
} .style("stroke-width", 0)
if ((yrect - CY) > 0) { .style("stroke", "black")
yrect = yrect - 5 - 20; .style("fill", "rgb(20,90,50)");
ytext = ytext - 5 - 20 + 16;
} /* Valeurs sur axe */
else { g.selectAll("textaxis")
yrect = yrect + 5; .data(R_AXIS_TICS)
ytext = ytext + 5 + 16; .enter()
} .append("text")
var r = g.append("rect") .text(String)
.attr('class', 'radartip') .attr("x", CX - 10)
.attr("x", x) .attr("y", function (x, i) {
.attr("y", yrect) return CY - (x * RR) / 20 + 6;
.attr("height", 20) })
.attr("width", rwidth); .attr("class", "textaxis");
var txt = g.append("text").text(titre)
.attr('class', 'radartip') /* Noms des modules */
.attr("x", x + 5) g.selectAll("text_modules")
.attr("y", ytext); .data(notes)
}) .enter()
.on("mouseout", function (d) { .append("text")
d3.selectAll(".radartip").remove() .text(function (d) {
}); return d["code"];
})
.attr("x", function (d) {
return d["x_label"];
})
.attr("y", function (d) {
return d["y_label"];
})
.attr("dx", 0)
.attr("dy", 0)
.on("mouseover", function (d) {
var x = d["x_label"];
var yrect = d["y_label"];
var ytext = d["y_label"];
var titre = d["titre"].replace("&apos;", "'").substring(0, 64);
var rwidth = titre.length * 9; // rough estimate of string width in pixels
if (x - CX < 0) {
x = x + 5;
if (x + rwidth + 12 > WIDTH) {
x = WIDTH - rwidth - 12;
}
} else {
if (x - CX > 0) {
x = x - rwidth - 5;
if (x < 12) {
x = 12;
}
} else {
x = CX - rwidth / 2;
}
}
if (yrect - CY > 0) {
yrect = yrect - 5 - 20;
ytext = ytext - 5 - 20 + 16;
} else {
yrect = yrect + 5;
ytext = ytext + 5 + 16;
}
var r = g
.append("rect")
.attr("class", "radartip")
.attr("x", x)
.attr("y", yrect)
.attr("height", 20)
.attr("width", rwidth);
var txt = g
.append("text")
.text(titre)
.attr("class", "radartip")
.attr("x", x + 5)
.attr("y", ytext);
})
.on("mouseout", function (d) {
d3.selectAll(".radartip").remove();
});
} }

View File

@ -114,9 +114,18 @@ class RowAssi(tb.Row):
compte_justificatifs = scass.filter_by_date( compte_justificatifs = scass.filter_by_date(
etud.justificatifs, Justificatif, self.dates[0], self.dates[1] etud.justificatifs, Justificatif, self.dates[0], self.dates[1]
).count() )
self.add_cell("justificatifs", "Justificatifs", f"{compte_justificatifs}") compte_justificatifs_att = compte_justificatifs.filter(Justificatif.etat == 2)
self.add_cell(
"justificatifs_att",
"Justificatifs en Attente",
f"{compte_justificatifs_att.count()}",
)
self.add_cell(
"justificatifs", "Justificatifs", f"{compte_justificatifs.count()}"
)
def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]: def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]:
retour: dict[str, tuple[str, float, float]] = { retour: dict[str, tuple[str, float, float]] = {

View File

@ -1,14 +1,14 @@
{% include "assiduites/widgets/toast.j2" %} {% include "assiduites/widgets/toast.j2" %}
{% block pageContent %} {% block pageContent %}
<div class="pageContent"> <div class="pageContent">
<h3>Justifier des assiduités</h3> <h3>Justifier des absences ou retards</h3>
{% include "assiduites/widgets/tableau_base.j2" %} {% include "assiduites/widgets/tableau_base.j2" %}
<section class="liste"> <section class="liste">
<a class="icon filter" onclick="filter(false)"></a> <a class="icon filter" onclick="filterJusti()"></a>
{% include "assiduites/widgets/tableau_justi.j2" %} {% include "assiduites/widgets/tableau_justi.j2" %}
</section> </section>
<section class="justi-form"> <section class="justi-form page">
<fieldset> <fieldset>
<div class="justi-row"> <div class="justi-row">
@ -19,8 +19,9 @@
<div class="justi-label"> <div class="justi-label">
<legend for="justi_date_debut" required>Date de début</legend> <legend for="justi_date_debut" required>Date de début</legend>
<input type="datetime-local" name="justi_date_debut" id="justi_date_debut"> <input type="datetime-local" name="justi_date_debut" id="justi_date_debut">
<span>Journée(s) entière(s)</span> <input type="checkbox" name="justi_journee" id="justi_journee">
</div> </div>
<div class="justi-label"> <div class="justi-label" id="date_fin">
<legend for="justi_date_fin" required>Date de fin</legend> <legend for="justi_date_fin" required>Date de fin</legend>
<input type="datetime-local" name="justi_date_fin" id="justi_date_fin"> <input type="datetime-local" name="justi_date_fin" id="justi_date_fin">
</div> </div>
@ -110,16 +111,15 @@
function validateFields() { function validateFields() {
const field = document.querySelector('.justi-form') const field = document.querySelector('.justi-form')
const in_date_debut = field.querySelector('#justi_date_debut'); const { deb, fin } = getDates()
const in_date_fin = field.querySelector('#justi_date_fin');
if (in_date_debut.value == "" || in_date_fin.value == "") { if (deb == "" || fin == "") {
openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin."), "", color = "crimson"); openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin valide."), "", color = "crimson");
return false; return false;
} }
const date_debut = moment.tz(in_date_debut.value, TIMEZONE); const date_debut = moment.tz(deb, TIMEZONE);
const date_fin = moment.tz(in_date_fin.value, TIMEZONE); const date_fin = moment.tz(fin, TIMEZONE);
if (date_fin.isBefore(date_debut)) { if (date_fin.isBefore(date_debut)) {
openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson"); openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson");
@ -130,16 +130,16 @@
} }
function fieldsToJustificatif() { function fieldsToJustificatif() {
const field = document.querySelector('.justi-form') const field = document.querySelector('.justi-form.page')
const { deb, fin } = getDates()
const date_debut = field.querySelector('#justi_date_debut').value;
const date_fin = field.querySelector('#justi_date_fin').value;
const etat = field.querySelector('#justi_etat').value; const etat = field.querySelector('#justi_etat').value;
const raison = field.querySelector('#justi_raison').value; const raison = field.querySelector('#justi_raison').value;
return { return {
date_debut: date_debut, date_debut: moment.tz(deb, TIMEZONE).format(),
date_fin: date_fin, date_fin: moment.tz(fin, TIMEZONE).format(),
etat: etat, etat: etat,
raison: raison, raison: raison,
} }
@ -218,11 +218,46 @@
} }
function dayOnly() {
if (document.getElementById('justi_journee').checked) {
document.getElementById("justi_date_debut").type = "date"
document.getElementById("justi_date_fin").type = "date"
} else {
document.getElementById("justi_date_debut").type = "datetime-local"
document.getElementById("justi_date_fin").type = "datetime-local"
}
}
function getDates() {
if (document.querySelector('.page #justi_journee').checked) {
const date_str_deb = document.querySelector(".page #justi_date_debut").value
const date_str_fin = document.querySelector(".page #justi_date_debut").value
return {
"deb": date_str_deb ? `${date_str_deb}T${assi_morning}` : "",
"fin": date_str_fin ? `${date_str_fin}T${assi_evening}` : "",
}
}
return {
"deb": document.querySelector(".page #justi_date_debut").value,
"fin": document.querySelector(".page #justi_date_fin").value,
}
}
const etudid = {{ sco.etud.id }}; const etudid = {{ sco.etud.id }};
const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false;
const assi_morning = '{{assi_morning}}';
const assi_evening = '{{assi_evening}}';
window.onload = () => { window.onload = () => {
loadAll(); loadAll();
document.getElementById('justi_journee').addEventListener('click', () => { dayOnly() });
dayOnly()
} }
</script> </script>
{% endblock pageContent %} {% endblock pageContent %}

View File

@ -6,6 +6,10 @@
<section class="nonvalide"> <section class="nonvalide">
<!-- Tableaux des justificatifs à valider (attente / modifié ) --> <!-- Tableaux des justificatifs à valider (attente / modifié ) -->
<h4>Justificatifs en attente (ou modifiés)</h4> <h4>Justificatifs en attente (ou modifiés)</h4>
<span class="iconline">
<a class="icon filter" onclick="filterJusti(true)"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %} {% include "assiduites/widgets/tableau_justi.j2" %}
</section> </section>
@ -29,18 +33,21 @@
</div> </div>
<script> <script>
function loadAll() { let formsemestre_id = "{{formsemestre_id}}"
generate(defAnnee) let group_id = "{{group_id}}"
}
function getDeptJustificatifsFromPeriod(action) { function getDeptJustificatifsFromPeriod(action) {
const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}&etat=attente,modifie` const formsemestre = formsemestre_id ? `&formsemestre_id=${formsemestre_id}` : ""
const group = group_id ? `&group_id=${group_id}` : ""
const path = getUrl() + `/api/justificatifs/dept/${dept_id}/query?date_debut=${bornes.deb}&date_fin=${bornes.fin}${formsemestre}${group}`
async_get( async_get(
path, path,
(data, status) => { (data, status) => {
console.log(data); if (action) {
justificatifCallBack(data); action(data)
} else {
justificatifCallBack(data);
}
}, },
(data, status) => { (data, status) => {
console.error(data, status) console.error(data, status)
@ -57,15 +64,19 @@
} }
bornes = { bornes = {
deb: `${annee}-09-01T00:00`, deb: `${annee}-09-01T00:00`,
fin: `${annee + 1}-06-30T23:59` fin: `${annee + 1}-08-31T23:59`
} }
defAnnee = annee; defAnnee = annee;
getDeptJustificatifsFromPeriod() loadAll();
} }
function getJusti(action) {
try { getDeptJustificatifsFromPeriod(action) } catch (_) { }
}
function setterAnnee(annee) { function setterAnnee(annee) {
annee = parseInt(annee); annee = parseInt(annee);
document.querySelector('.annee span').textContent = `Année scolaire ${annee}-${annee + 1} Changer année: ` document.querySelector('.annee span').textContent = `Année scolaire ${annee}-${annee + 1} Changer année: `
@ -75,14 +86,19 @@
let defAnnee = {{ annee }}; let defAnnee = {{ annee }};
let bornes = { let bornes = {
deb: `${defAnnee}-09-01T00:00`, deb: `${defAnnee}-09-01T00:00`,
fin: `${defAnnee + 1}-06-30T23:59` fin: `${defAnnee + 1}-08-31T23:59`
} }
const dept_id = {{ dept_id }}; const dept_id = {{ dept_id }};
let annees = {{ annees | safe}}
annees = annees.filter((x, i) => annees.indexOf(x) === i)
window.addEventListener('load', () => { window.addEventListener('load', () => {
filterJustificatifs = { filterJustificatifs = {
"columns": [ "columns": [
"formsemestre",
"etudid", "etudid",
"entry_date", "entry_date",
"date_debut", "date_debut",
@ -95,19 +111,20 @@
"etat": [ "etat": [
"attente", "attente",
"modifie" "modifie"
] ],
} }
} }
const select = document.querySelector('#annee'); const select = document.querySelector('#annee');
for (let i = defAnnee + 1; i > defAnnee - 6; i--) {
annees.forEach((a) => {
const opt = document.createElement("option"); const opt = document.createElement("option");
opt.value = i + "", opt.value = a + "",
opt.textContent = i + ""; opt.textContent = `${a} - ${a + 1}`;
if (i === defAnnee) { if (a === defAnnee) {
opt.selected = true; opt.selected = true;
} }
select.appendChild(opt) select.appendChild(opt)
} })
setterAnnee(defAnnee) setterAnnee(defAnnee)
}) })

View File

@ -26,10 +26,18 @@
<section class="nonvalide"> <section class="nonvalide">
<!-- Tableaux des assiduités (retard/abs) non justifiées --> <!-- Tableaux des assiduités (retard/abs) non justifiées -->
<h4>Assiduités non justifiées (Uniquement les retards et les absences)</h4> <h4>Absences et retards non justifiés</h4>
<span class="iconline">
<a class="icon filter" onclick="filterAssi()"></a>
<a class="icon download" onclick="downloadAssi()"></a>
</span>
{% include "assiduites/widgets/tableau_assi.j2" %} {% include "assiduites/widgets/tableau_assi.j2" %}
<!-- Tableaux des justificatifs à valider (attente / modifié ) --> <!-- Tableaux des justificatifs à valider (attente / modifié ) -->
<h4>Justificatifs en attente (ou modifiés)</h4> <h4>Justificatifs en attente (ou modifiés)</h4>
<span class="iconline">
<a class="icon filter" onclick="filterJusti()"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %} {% include "assiduites/widgets/tableau_justi.j2" %}
</section> </section>
@ -44,7 +52,7 @@
<h3>Statistiques</h3> <h3>Statistiques</h3>
<p>Un message d'alerte apparait si le nombre d'absence dépasse le seuil (indiqué dans les préférences du <p>Un message d'alerte apparait si le nombre d'absence dépasse le seuil (indiqué dans les préférences du
département)</p> département)</p>
<p>Les statistiques sont effectuées entre les deux dates séléctionnées. Si vous modifier les dates il faudra <p>Les statistiques sont calculées entre les deux dates sélectionnées. Après modification des dates,
appuyer sur le bouton "Actualiser"</p> appuyer sur le bouton "Actualiser"</p>
<h3>Gestion des justificatifs</h3> <h3>Gestion des justificatifs</h3>
<p> <p>
@ -53,21 +61,21 @@
contextuel : contextuel :
</p> </p>
<ul> <ul>
<li>Détails : Affiche les détails du justificatif sélectionné</li> <li>Détails : affiche les détails du justificatif sélectionné</li>
<li>Editer : Permet de modifier le justificatif (dates, etat, ajouter/supprimer fichier etc)</li> <li>Éditer : modifie le justificatif (dates, état, ajouter/supprimer fichier, etc.)</li>
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li> <li>Supprimer : supprime le justificatif (action irréversible)</li>
</ul> </ul>
<h3>Gestion des Assiduités</h3> <h3>Gestion de l'assiduité</h3>
<p> <p>
Faites Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu <span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
contextuel : contextuel :
</p> </p>
<ul> <ul>
<li>Détails : Affiche les détails de l'assiduité sélectionnée</li> <li>Détails : affiche les détails de l'élément sélectionnée</li>
<li>Editer : Permet de modifier l'assiduité (moduleimpl, etat)</li> <li>Editer : modifie l'élément (module, état)</li>
<li>Supprimer : Permet de supprimer l'assiduité (Action Irréversible)</li> <li>Supprimer : supprime l'élément (action irréversible)</li>
</ul> </ul>
</div> </div>
@ -181,9 +189,9 @@
function removeAllAssiduites() { function removeAllAssiduites() {
openPromptModal( openPromptModal(
"Suppression des assiduités", "Suppression de l'assiduité",
document.createTextNode( document.createTextNode(
'Souhaitez vous réelement supprimer toutes les assiduités de cet étudiant ? Cette supression est irréversible.') 'Souhaitez vous réellement supprimer toutes les informations sur l\'assiduité de cet étudiant ? Cette suppression est irréversible.')
, ,
() => { () => {
getAllAssiduitesFromEtud(etudid, (data) => { getAllAssiduitesFromEtud(etudid, (data) => {
@ -266,6 +274,9 @@
const assi_date_debut = "{{date_debut}}"; const assi_date_debut = "{{date_debut}}";
const assi_date_fin = "{{date_fin}}"; const assi_date_fin = "{{date_fin}}";
const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false;
window.addEventListener('load', () => { window.addEventListener('load', () => {
filterAssiduites = { filterAssiduites = {
"columns": [ "columns": [

View File

@ -3,7 +3,7 @@
<div class="pageContent"> <div class="pageContent">
{{minitimeline | safe }} {{minitimeline | safe }}
<h2>Assiduités de {{sco.etud.nomprenom}}</h2> <h2>Assiduité de {{sco.etud.nomprenom}}</h2>
<div class="calendrier"> <div class="calendrier">
</div> </div>
@ -13,22 +13,22 @@
</select> </select>
</div> </div>
<div class="legende"> <div class="help">
<h3>Calendrier</h3> <h3>Calendrier</h3>
<p>Les jours non travaillés sont affiché en violet</p> <p>Les jours non travaillés sont affiché en violet</p>
<p>Les jours possèdant une bordure "bleu" sont des jours où des assiduités ont été justifiées par un <p>Les jours possèdant une bordure "bleu" sont des jours où des absences/retards ont été justifiées par un
justificatif valide</p> justificatif valide</p>
<p>Les jours possèdant une bordure "rouge" sont des jours où des assiduités ont été justifiées par un <p>Les jours possèdant une bordure "rouge" sont des jours où des absences/retards ont été justifiées par un
justificatif non valide</p> justificatif non valide</p>
<p>Le jour sera affiché en : </p> <p>Le jour sera affiché en : </p>
<ul> <ul>
<li>Rouge : S'il y a une assiduité "Absent"</li> <li>Rouge : s'il y a une absence enregistrée</li>
<li>Orange : S'il y a une assiduité "Retard" et pas d'assiduité "Absent"</li> <li>Orange : s'il y a un retard et pas d'absence</li>
<li>Vert : S'il y a une assiduité "Present" et pas d'assiduité "Absent" ni "Retard"</li> <li>Vert : s'il y a une présence enregistrée mais pas d'absence ni de retard</li>
<li>Blanc : S'il n'y a pas d'assiduité</li> <li>Blanc : s'il n'y a rien d'enregistré</li>
</ul> </ul>
<p>Vous pouvez passer votre curseur sur les jours colorés afin de voir les assiduités de cette journée.</p> <p>Vous pouvez passer le curseur sur les jours colorés afin de voir les informations de cette journée.</p>
</div> </div>
</div> </div>
@ -354,5 +354,7 @@
setterAnnee(defAnnee) setterAnnee(defAnnee)
}; };
function isCalendrier() { return true }
</script> </script>
{% endblock pageContent %} {% endblock pageContent %}

View File

@ -3,11 +3,17 @@
<h2>Liste de l'assiduité et des justificatifs de <span class="rouge">{{sco.etud.nomprenom}}</span></h2> <h2>Liste de l'assiduité et des justificatifs de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
{% include "assiduites/widgets/tableau_base.j2" %} {% include "assiduites/widgets/tableau_base.j2" %}
<h3>Assiduités :</h3> <h3>Assiduité :</h3>
<a class="icon filter" onclick="filter()"></a> <span class="iconline">
<a class="icon filter" onclick="filterAssi()"></a>
<a class="icon download" onclick="downloadAssi()"></a>
</span>
{% include "assiduites/widgets/tableau_assi.j2" %} {% include "assiduites/widgets/tableau_assi.j2" %}
<h3>Justificatifs :</h3> <h3>Justificatifs :</h3>
<a class="icon filter" onclick="filter(false)"></a> <span class="iconline">
<a class="icon filter" onclick="filterJusti()"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %} {% include "assiduites/widgets/tableau_justi.j2" %}
<ul id="contextMenu" class="context-menu"> <ul id="contextMenu" class="context-menu">
<li id="detailOption">Detail</li> <li id="detailOption">Detail</li>
@ -27,20 +33,20 @@
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li> <li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
</ul> </ul>
<p>Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonoir sous le titre du tableau.</p> <p>Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonnoir sous le titre du tableau.</p>
<h3>Gestion des Assiduités</h3> <h3>Gestion de l'assiduité</h3>
<p> <p>
Faites Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu <span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
contextuel : contextuel :
</p> </p>
<ul> <ul>
<li>Détails : Affiche les détails de l'assiduité sélectionnée</li> <li>Détails : affiche les détails de l'assiduité sélectionnée</li>
<li>Editer : Permet de modifier l'assiduité (moduleimpl, etat)</li> <li>Éditer : modifier l'élément (module, état)</li>
<li>Supprimer : Permet de supprimer l'assiduité (Action Irréversible)</li> <li>Supprimer : supprimer l'élément (action irréversible)</li>
</ul> </ul>
<p>Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonoir sous le titre du tableau.</p> <p>Vous pouvez filtrer le tableau en cliquant sur l'icone d'entonnoir sous le titre du tableau.</p>
</div> </div>
</div> </div>
@ -48,8 +54,43 @@
<script> <script>
const etudid = {{ sco.etud.id }} const etudid = {{ sco.etud.id }}
const assiduite_unique_id = {{ assi_id }};
const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false;
function wayForFilter() {
if (typeof assiduites[etudid] !== "undefined") {
console.log("Done")
let assiduite = assiduites[etudid].filter((a) => { return a.assiduite_id == assiduite_unique_id });
if (assiduite) {
assiduite = assiduite[0]
filterAssiduites["filters"] = {
"obj_id": [
assiduite.assiduite_id,
]
}
const obj_ids = assiduite.justificatifs ? assiduite.justificatifs.map((j) => { return j.justif_id }) : []
filterJustificatifs["filters"] = {
"obj_id": obj_ids
}
loadAll();
}
} else {
setTimeout(wayForFilter, 250)
}
}
window.onload = () => { window.onload = () => {
loadAll(); loadAll();
if (assiduite_unique_id != -1) {
wayForFilter()
}
} }
</script> </script>

View File

@ -0,0 +1,94 @@
{% block pageContent %}
<div class="pageContent">
<h3>Assiduites et justificatifs de <span class="rouge">{{sem}}</span> </h3>
{% include "assiduites/widgets/tableau_base.j2" %}
<h4>Assiduité :</h4>
<span class="iconline">
<a class="icon filter" onclick="filterAssi()"></a>
<a class="icon download" onclick="downloadAssi()"></a>
</span>
{% include "assiduites/widgets/tableau_assi.j2" %}
<h4>Justificatifs :</h4>
<span class="iconline">
<a class="icon filter" onclick="filterJusti()"></a>
<a class="icon download" onclick="downloadJusti()"></a>
</span>
{% include "assiduites/widgets/tableau_justi.j2" %}
</div>
<script>
const formsemestre_id = {{ formsemestre_id }};
function getFormSemestreAssiduites(action) {
const path = getUrl() + `/api/assiduites/formsemestre/${formsemestre_id}`
async_get(
path,
(data, status) => {
if (action) {
action(data)
} else {
assiduiteCallBack(data);
}
},
(data, status) => {
console.error(data, status)
errorAlert();
}
)
}
function getFormSemestreJustificatifs(action) {
const path = getUrl() + `/api/justificatifs/formsemestre/${formsemestre_id}`
async_get(
path,
(data, status) => {
if (action) {
action(data)
} else {
justificatifCallBack(data);
}
},
(data, status) => {
console.error(data, status)
errorAlert();
}
)
}
function getAssi(action) {
try { getFormSemestreAssiduites(action) } catch (_) { }
}
function getJusti(action) {
try { getFormSemestreJustificatifs(action) } catch (_) { }
}
window.addEventListener('load', () => {
filterJustificatifs = {
"columns": [
"etudid",
"entry_date",
"date_debut",
"date_fin",
"etat",
"raison",
"fichier"
],
"filters": {
}
}
filterAssiduites = {
columns: [
"etudid", "entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"
],
"filters": {
}
}
loadAll();
})
</script>
{% endblock pageContent %}

View File

@ -1,16 +1,16 @@
<h2>Signalement différé des assiduités {{gr |safe}}</h2> <h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
<div class="legende"> <div class="help">
<h3>Explication de la saisie différée</h3> <h3>Explication de la saisie différée</h3>
<p>Si la colonne n'est pas valide elle sera affichée en rouge, passez votre curseur sur la colonne pour afficher <p>Si la colonne n'est pas valide elle sera affichée en rouge, passez le curseur sur la colonne pour afficher
le message d'erreur</p> le message d'erreur</p>
<p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance <p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance
(préférence de département)</p> (préférence de département)</p>
<p>Modifier le moduleimpl alors que des assiduités sont déjà enregistrées pour la période changera leur <p>Modifier le module alors que des informations d'assiduité sont déjà enregistrées pour la période changera leur
moduleimpl.</p> module.</p>
<p>Il y a 4 boutons d'assiduités sur la colonne permettant de mettre l'assiduités à tous les étudiants</p> <p>Il y a 4 boutons sur la colonne permettant d'enregistrer l'information pour tous les étudiants</p>
<p>Le dernier des boutons retire l'assiduité.</p> <p>Le dernier des boutons retire l'information présente.</p>
<p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p> <p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p>
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne <p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne.
</p> </p>
</div> </div>
<h3>{{sem | safe }}</h3> <h3>{{sem | safe }}</h3>

View File

@ -32,6 +32,18 @@
</div> </div>
</div> </div>
<hr> <hr>
{% if saisie_eval %}
<div id="saisie_eval">
<br>
<h3>
La saisie de l'assiduité a été préconfigurée en fonction de l'évaluation. <br>
Une fois la saisie finie, cliquez sur le lien si dessous pour revenir sur la gestion de l'évaluation
</h3>
<a href="{{redirect_url}}">retourner sur la page de l'évaluation</a>
</div>
{% endif %}
{{diff | safe}} {{diff | safe}}
<div class="legende"> <div class="legende">
@ -62,16 +74,16 @@
<p>Vous pouvez justifier rapidement une assiduité en saisisant l'assiduité puis en appuyant sur "Justifier"</p> <p>Vous pouvez justifier rapidement une assiduité en saisisant l'assiduité puis en appuyant sur "Justifier"</p>
<h3>Explication de la saisie différée</h3> <h3>Explication de la saisie différée</h3>
<p>Si la colonne n'est pas valide elle sera affichée en rouge, passez votre curseur sur la colonne pour afficher <p>Si la colonne n'est pas valide elle sera affichée en rouge, passez le curseur sur la colonne pour afficher
le message d'erreur</p> le message d'erreur</p>
<p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance <p>Sélectionner la date de début de la colonne mettra automatiquement la date de fin à la durée d'une séance
(préférence de département)</p> (préférence de département)</p>
<p>Modifier le moduleimpl alors que des assiduités sont déjà enregistrées pour la période changera leur <p>Modifier le module alors que des informations sont déjà enregistrées pour la période changera leur
moduleimpl.</p> module.</p>
<p>Il y a 4 boutons d'assiduités sur la colonne permettant de mettre l'assiduités à tous les étudiants</p> <p>Il y a 4 boutons sur la colonne permettant d'enregistrer l'information pour tous les étudiants</p>
<p>Le dernier des boutons retire l'assiduité.</p> <p>Le dernier des boutons retire l'information présente.</p>
<p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p> <p>Vous pouvez ajouter des colonnes en appuyant sur le bouton + </p>
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne <p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne.
</p> </p>
</div> </div>
@ -118,7 +130,20 @@
window.forceModule = "{{ forcer_module }}" window.forceModule = "{{ forcer_module }}"
window.forceModule = window.forceModule == "True" ? true : false window.forceModule = window.forceModule == "True" ? true : false
const date_deb = "{{date_deb}}";
const date_fin = "{{date_fin}}";
{% if saisie_eval %}
createColumn(
date_deb,
date_fin,
{{ moduleimpl_id }}
);
window.location.href = "#saisie_eval"
getAndUpdateCol(1)
{% else %}
createColumn(); createColumn();
{% endif %}
</script> </script>

View File

@ -16,13 +16,13 @@
value="{{date_fin}}"></label> value="{{date_fin}}"></label>
<button onclick="stats()">Changer</button> <button onclick="stats()">Changer</button>
<a style="margin-left:32px;" href="{{request.url}}&format=xlsx">{{scu.ICON_XLS|safe}}</a> <a style="margin-left:32px;" href="{{request.url}}&fmt=xlsx">{{scu.ICON_XLS|safe}}</a>
</div> </div>
{{tableau | safe}} {{tableau | safe}}
<div class=""help"> <div class=""help">
Les comptes sont exprimés en {{ assi_metric }}. Les comptes sont exprimés en {{ assi_metric | lower}}s.
</div> </div>
<script> <script>

View File

@ -129,7 +129,7 @@
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<button id="finish" class="btnPrompt">Terminer la résolution</button> <button id="finish" class="btnPrompt">Quitter</button>
<button id="delete" class="btnPrompt" disabled>Supprimer</button> <button id="delete" class="btnPrompt" disabled>Supprimer</button>
<button id="split" class="btnPrompt" disabled>Séparer</button> <button id="split" class="btnPrompt" disabled>Séparer</button>
<button id="edit" class="btnPrompt" disabled>Modifier l'état</button> <button id="edit" class="btnPrompt" disabled>Modifier l'état</button>
@ -348,7 +348,7 @@
// Actualiser l'affichage // Actualiser l'affichage
editAssiduite(this.selectedAssiduite.assiduite_id, newState); editAssiduite(this.selectedAssiduite.assiduite_id, newState, [this.selectedAssiduite]);
this.callbacks.edit(this.selectedAssiduite) this.callbacks.edit(this.selectedAssiduite)
this.refresh(assiduites[this.selectedAssiduite.etudid]); this.refresh(assiduites[this.selectedAssiduite.etudid]);

View File

@ -96,6 +96,15 @@
display: flex; display: flex;
} }
.td[assiduite_id='insc'] * {
display: none;
}
.td[assiduite_id='insc']::after {
content: "non inscrit au module";
font-style: italic;
}
.sticky { .sticky {
position: sticky; position: sticky;
left: 0; left: 0;
@ -278,7 +287,9 @@
currentDate = moment(currentDate).tz(TIMEZONE).format("YYYY-MM-DDTHH:mm"); currentDate = moment(currentDate).tz(TIMEZONE).format("YYYY-MM-DDTHH:mm");
} }
function createColumn(dateStart = "", dateEnd = "") { const inscriptionsModule = {};
function createColumn(dateStart = "", dateEnd = "", moduleimpl_id = "") {
let table = document.getElementById("studentTable"); let table = document.getElementById("studentTable");
let th = document.createElement("div"); let th = document.createElement("div");
th.classList.add("th", "error"); th.classList.add("th", "error");
@ -343,6 +354,10 @@
editModuleImpl(sl); editModuleImpl(sl);
}) })
if (moduleimpl_id != "") {
sl.value = moduleimpl_id;
}
let rows = table.querySelector(".tbody").querySelectorAll(".tr"); let rows = table.querySelector(".tbody").querySelectorAll(".tr");
for (let i = 0; i < rows.length; i++) { for (let i = 0; i < rows.length; i++) {
let td = document.createElement("div"); let td = document.createElement("div");
@ -533,7 +548,7 @@
} }
if (get) { if (get) {
getAssiduitesFromEtuds(false, false, d_debut.format(), d_fin.format()) getAssiduitesFromEtuds(false, d_debut.format(), d_fin.format())
return 0x0; return 0x0;
} }
@ -557,6 +572,8 @@
const d_debut = moment(inputDeb).tz(TIMEZONE); const d_debut = moment(inputDeb).tz(TIMEZONE);
const d_fin = moment(inputFin).tz(TIMEZONE); const d_fin = moment(inputFin).tz(TIMEZONE);
const moduleimpl_id = col.querySelector("#moduleimpl_select").value;
const periode = { const periode = {
deb: d_debut, deb: d_debut,
fin: d_fin, fin: d_fin,
@ -569,9 +586,12 @@
}); });
setEtatLine(td, "") setEtatLine(td, "")
const etu = td.parentElement.getAttribute('etudid'); const etu = td.parentElement.getAttribute('etudid');
const inscriptionModule = ["", "autre"].indexOf(moduleimpl_id) !== -1 ? true : checkInscriptionModule(moduleimpl_id, etu);
const conflits = getAssiduitesConflict(etu, periode); const conflits = getAssiduitesConflict(etu, periode);
if (!inscriptionModule) {
if (conflits.length == 0) { td.setAttribute('assiduite_id', "insc");
}
else if (conflits.length == 0) {
td.setAttribute('assiduite_id', "-1"); td.setAttribute('assiduite_id', "-1");
} else if (conflits.length == 1 && isConflictSameAsPeriod(conflits[0], periode)) { } else if (conflits.length == 1 && isConflictSameAsPeriod(conflits[0], periode)) {
const assi = conflits[0]; const assi = conflits[0];
@ -583,7 +603,6 @@
const inputs = [...td.querySelectorAll('input')]; const inputs = [...td.querySelectorAll('input')];
inputs.forEach((i) => { inputs.forEach((i) => {
i.disabled = true; i.disabled = true;
}) })
} }
}) })
@ -867,7 +886,7 @@
const { moduleimpl, deb, fin } = getAssiduitesCol(colid, false); const { moduleimpl, deb, fin } = getAssiduitesCol(colid, false);
const lines = [...document.querySelectorAll(`[assiduite_id][colid='${colid}']`)].filter((el) => { const lines = [...document.querySelectorAll(`[assiduite_id][colid='${colid}']`)].filter((el) => {
return el.getAttribute('assiduite_id') != "conflit"; return ["conflit", "insc"].indexOf(el.getAttribute('assiduite_id')) == -1;
}) })
const toCreate = lines.filter((el) => { return el.getAttribute('assiduite_id') == '-1' }) const toCreate = lines.filter((el) => { return el.getAttribute('assiduite_id') == '-1' })
@ -1015,6 +1034,25 @@
}) })
} }
function checkInscriptionModule(moduleimpl_id, etudid) {
if (!inscriptionsModule.hasOwnProperty(moduleimpl_id)) {
const path = getUrl() + `/api/moduleimpl/${moduleimpl_id}/inscriptions`;
sync_get(
path,
(data, status) => {
inscriptionsModule[moduleimpl_id] = data;
},
(data, status) => {
//error
console.error(data, status);
errorAlert();
}
);
}
const etudsInscrits = inscriptionsModule[moduleimpl_id].map((i) => i.etudid);
return etudsInscrits.indexOf(Number(etudid)) !== -1;
}
window.addEventListener('load', () => { window.addEventListener('load', () => {
document.getElementById("addColumn").addEventListener("click", () => { document.getElementById("addColumn").addEventListener("click", () => {
createColumn(); createColumn();

View File

@ -0,0 +1,16 @@
<=== Assiduité ===>
--- Absences non justifiées {{stats.absent[metric] - stats.absent.justifie[metric]}} ({{metrique}}) ---
{% for assi in abs_nj %}- Absence non just. {{assi.date}}
{% endfor %}
--- Absences justifiées {{stats.absent.justifie[metric]}} ({{metrique}}) ---
{% for assi in abs_j %}- Absence just. {{assi.date}}
{% endfor %}
--- Retard {{stats.retard[metric]}} ({{metrique}}) ---
{% for assi in retards %}- Retard {{assi.date}}
{% endfor %}
--- Justificatif ---
{% for justi in justifs %}- Justificatif {{justi.date}} {{justi.raison}} : {{justi.etat}}
{% endfor %}

View File

@ -71,6 +71,11 @@
updateSelectedSelect(getCurrentAssiduiteModuleImplId()); updateSelectedSelect(getCurrentAssiduiteModuleImplId());
updateJustifyBtn(); updateJustifyBtn();
} }
try {
if (isCalendrier()) {
window.location = `ListeAssiduitesEtud?etudid=${etudid}&assiduite_id=${assiduité.assiduite_id}`
}
} catch { }
}); });
//ajouter affichage assiduites on over //ajouter affichage assiduites on over
setupAssiduiteBuble(block, assiduité); setupAssiduiteBuble(block, assiduité);

View File

@ -117,10 +117,12 @@
} }
}) })
const conflicts = getAssiduitesConflict(etudid); try {
if (conflicts.length > 0) { const conflicts = getAssiduitesConflict(etudid);
updateSelectedSelect(conflicts[0].moduleimpl_id); if (conflicts.length > 0) {
} updateSelectedSelect(conflicts[0].moduleimpl_id);
}
} catch { }
}, { once: true }); }, { once: true });

View File

@ -88,6 +88,10 @@
td.textContent = getModuleImpl(assiduite); td.textContent = getModuleImpl(assiduite);
} else if (k.indexOf('est_just') != -1) { } else if (k.indexOf('est_just') != -1) {
td.textContent = assiduite[k] ? "Oui" : "Non" td.textContent = assiduite[k] ? "Oui" : "Non"
} else if (k.indexOf('etudid') != -1) {
const e = getEtudiant(assiduite.etudid);
td.innerHTML = `<a class="etudinfo" id="line-${assiduite.etudid}" href="BilanEtud?etudid=${assiduite.etudid}">${e.prenom.capitalize()} ${e.nom.toUpperCase()}</a>`;
} else { } else {
td.textContent = assiduite[k].capitalize() td.textContent = assiduite[k].capitalize()
} }
@ -147,7 +151,7 @@
<span class="obj-content">${etat}</span> <span class="obj-content">${etat}</span>
</div> </div>
<div id="user" class="obj-part"> <div id="user" class="obj-part">
<span class="obj-title">Créer par</span> <span class="obj-title">Créée par</span>
<span class="obj-content">${user}</span> <span class="obj-content">${user}</span>
</div> </div>
</div> </div>
@ -184,8 +188,11 @@
path, path,
(data) => { (data) => {
let module = data.moduleimpl_id; let module = data.moduleimpl_id;
if (
if (module == null && "external_data" in data && "module" in data.external_data) { module == null && data.hasOwnProperty("external_data") &&
data.external_data != null &&
data.external_data.hasOwnProperty('module')
) {
module = data.external_data.module.toLowerCase(); module = data.external_data.module.toLowerCase();
} }
@ -220,7 +227,7 @@
assiEdit.querySelector('#etat').value = etat.toLowerCase(); assiEdit.querySelector('#etat').value = etat.toLowerCase();
assiEdit.querySelector('#desc').value = desc != null ? desc : ""; assiEdit.querySelector('#desc').value = desc != null ? desc : "";
updateSelect(module, '#moduleimpl_select', "2022-09-04") updateSelect(module, '#moduleimpl_select', data.date_debut.split('T')[0])
assiEdit.querySelector('#module').replaceWith(document.querySelector('#moduleimpl_select').cloneNode(true)); assiEdit.querySelector('#module').replaceWith(document.querySelector('#moduleimpl_select').cloneNode(true));
openPromptModal("Modification de l'assiduité", assiEdit, () => { openPromptModal("Modification de l'assiduité", assiEdit, () => {
const prompt = document.querySelector('.assi-edit'); const prompt = document.querySelector('.assi-edit');
@ -236,7 +243,7 @@
edit = setModuleImplId(edit, module); edit = setModuleImplId(edit, module);
fullEditAssiduites(data.assiduite_id, edit, () => { fullEditAssiduites(data.assiduite_id, edit, () => {
try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack) } catch (_) { } loadAll();
}) })
@ -258,4 +265,202 @@
} }
); );
} }
function filterAssi() {
let html = `
<div class="filter-body">
<h3>Affichage des colonnes:</h3>
<div class="filter-head">
<label>
Date de saisie
<input class="chk" type="checkbox" name="entry_date" id="entry_date">
</label>
<label>
Date de Début
<input class="chk" type="checkbox" name="date_debut" id="date_debut" checked>
</label>
<label>
Date de Fin
<input class="chk" type="checkbox" name="date_fin" id="date_fin" checked>
</label>
<label>
Etat
<input class="chk" type="checkbox" name="etat" id="etat" checked>
</label>
<label>
Module
<input class="chk" type="checkbox" name="moduleimpl_id" id="moduleimpl_id" checked>
</label>
<label>
Justifiée
<input class="chk" type="checkbox" name="est_just" id="est_just" checked>
</label>
</div>
<hr>
<h3>Filtrage des colonnes:</h3>
<span class="filter-line">
<span class="filter-title" for="entry_date">Date de saisie</span>
<select name="entry_date_pref" id="entry_date_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="entry_date_time" id="entry_date_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_debut">Date de début</span>
<select name="date_debut_pref" id="date_debut_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_debut_time" id="date_debut_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_fin">Date de fin</span>
<select name="date_fin_pref" id="date_fin_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_fin_time" id="date_fin_time">
</span>
<span class="filter-line">
<span class="filter-title" for="etat">Etat</span>
<input checked type="checkbox" name="etat_present" id="etat_present" class="rbtn present" value="present">
<input checked type="checkbox" name="etat_retard" id="etat_retard" class="rbtn retard" value="retard">
<input checked type="checkbox" name="etat_absent" id="etat_absent" class="rbtn absent" value="absent">
</span>
<span class="filter-line">
<span class="filter-title" for="moduleimpl_id">Module</span>
<select id="moduleimpl_id">
<option value="">Pas de filtre</option>
</select>
</span>
<span class="filter-line">
<span class="filter-title" for="est_just">Est Justifiée</span>
<select id="est_just">
<option value="">Pas de filtre</option>
<option value="true">Oui</option>
<option value="false">Non</option>
</select>
</span>
<span class="filter-line">
<span class="filter-title" for="etud">Rechercher dans les étudiants</span>
<input type="text" name="etud" id="etud" placeholder="Anne Onymous" >
</span>
</div>
`;
const span = document.createElement('span');
span.innerHTML = html
html = span.firstElementChild
const filterHead = html.querySelector('.filter-head');
filterHead.innerHTML = ""
let cols = ["etudid", "entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"];
cols.forEach((k) => {
const label = document.createElement('label')
label.classList.add('f-label')
const s = document.createElement('span');
s.textContent = columnTranslator(k);
const input = document.createElement('input');
input.classList.add('chk')
input.type = "checkbox"
input.name = k
input.id = k;
input.checked = filterAssiduites.columns.includes(k)
label.appendChild(s)
label.appendChild(input)
filterHead.appendChild(label)
})
const sl = html.querySelector('.filter-line #moduleimpl_id');
let opts = []
Object.keys(moduleimpls).forEach((k) => {
const opt = document.createElement('option');
opt.value = k == null ? "null" : k;
opt.textContent = moduleimpls[k];
opts.push(opt);
})
opts = opts.sort((a, b) => {
return a.value < b.value
})
sl.append(...opts);
// Mise à jour des filtres
Object.keys(filterAssiduites.filters).forEach((key) => {
const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement;
if (key.indexOf('date') != -1) {
l.querySelector(`#${key}_pref`).value = filterAssiduites.filters[key].pref;
l.querySelector(`#${key}_time`).value = filterAssiduites.filters[key].time.format("YYYY-MM-DDTHH:mm");
} else if (key.indexOf('etat') != -1) {
l.querySelectorAll('input').forEach((e) => {
e.checked = filterAssiduites.filters[key].includes(e.value)
})
} else if (key.indexOf("module") != -1) {
l.querySelector('#moduleimpl_id').value = filterAssiduites.filters[key];
} else if (key.indexOf("est_just") != -1) {
l.querySelector('#est_just').value = filterAssiduites.filters[key];
} else if (key == "etud") {
l.querySelector('#etud').value = filterAssiduites.filters["etud"];
}
})
openPromptModal("Filtrage des assiduités", html, () => {
const columns = [...document.querySelectorAll('.chk')]
.map((el) => { if (el.checked) return el.id })
.filter((el) => el)
filterAssiduites.columns = columns
filterAssiduites.filters = {}
//reste des filtres
const lines = [...document.querySelectorAll('.filter-line')];
lines.forEach((l) => {
const key = l.querySelector('.filter-title').getAttribute('for');
if (key.indexOf('date') != -1) {
const pref = l.querySelector(`#${key}_pref`).value;
const time = l.querySelector(`#${key}_time`).value;
if (l.querySelector(`#${key}_time`).value != "") {
filterAssiduites.filters[key] = {
pref: pref,
time: new moment.tz(time, TIMEZONE)
}
}
} else if (key.indexOf('etat') != -1) {
filterAssiduites.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value);
} else if (key.indexOf("module") != -1) {
filterAssiduites.filters[key] = l.querySelector('#moduleimpl_id').value;
} else if (key.indexOf("est_just") != -1) {
filterAssiduites.filters[key] = l.querySelector('#est_just').value;
} else if (key == "etud") {
filterAssiduites.filters["etud"] = l.querySelector('#etud').value;
}
})
getAssi(assiduiteCallBack)
}, () => { }, "#7059FF");
}
function downloadAssi() {
getAssi((d) => { toCSV(d, filterAssiduites) })
}
function getAssi(action) {
try { getAllAssiduitesFromEtud(etudid, action, true, true, assi_limit_annee) } catch (_) { }
}
</script> </script>

View File

@ -18,6 +18,9 @@
document.addEventListener("click", () => { document.addEventListener("click", () => {
contextMenu.style.display = "none"; contextMenu.style.display = "none";
if (contextMenu.childElementCount > 3) {
contextMenu.removeChild(contextMenu.lastElementChild)
}
}); });
editOption.addEventListener("click", () => { editOption.addEventListener("click", () => {
@ -57,8 +60,6 @@
deleteJustificatif(obj_id); deleteJustificatif(obj_id);
} }
loadAll(); loadAll();
} }
}); });
@ -94,6 +95,22 @@
} }
} }
if (k == "obj_id") {
const obj_id = el.assiduite_id || el.justif_id;
return f.obj_id.includes(obj_id)
}
if (k == "formsemestre") {
return f.formsemestre === "" || (el.hasOwnProperty("formsemestre") && el.formsemestre.title.replaceAll('-', ' ').indexOf(f.formsemestre) != -1);
}
if (k == "etud") {
const e = getEtudiant(el.etudid);
const str = `${e.prenom.capitalize()} ${e.nom.toUpperCase()}`
return f.etud === "" || str.indexOf(f.etud) != -1;
}
return true; return true;
}) })
@ -150,7 +167,7 @@
paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => { paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => {
if (currentPageAssiduites > 1) { if (currentPageAssiduites > 1) {
currentPageAssiduites--; currentPageAssiduites--;
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + ""
assiduiteCallBack(array); assiduiteCallBack(array);
} }
@ -159,7 +176,7 @@
paginationContainerAssiduites.querySelector('.pagination_plus').addEventListener('click', () => { paginationContainerAssiduites.querySelector('.pagination_plus').addEventListener('click', () => {
if (currentPageAssiduites < totalPages) { if (currentPageAssiduites < totalPages) {
currentPageAssiduites++; currentPageAssiduites++;
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + ""
assiduiteCallBack(array); assiduiteCallBack(array);
} }
}) })
@ -199,8 +216,12 @@
if (assi) { if (assi) {
paginationContainerAssiduites.querySelector('#paginationAssi').appendChild(paginationButton) paginationContainerAssiduites.querySelector('#paginationAssi').appendChild(paginationButton)
if (i == currentPageAssiduites)
paginationContainerAssiduites.querySelector('#paginationAssi').value = i + "";
} else { } else {
paginationContainerJustificatifs.querySelector('#paginationJusti').appendChild(paginationButton) paginationContainerJustificatifs.querySelector('#paginationJusti').appendChild(paginationButton)
if (i == currentPageJustificatifs)
paginationContainerJustificatifs.querySelector('#paginationJusti').value = i + "";
} }
} }
updateActivePaginationButton(assi); updateActivePaginationButton(assi);
@ -230,8 +251,8 @@
} }
function loadAll() { function loadAll() {
try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack) } catch (_) { } try { getAssi(assiduiteCallBack) } catch { }
try { getAllJustificatifsFromEtud(etudid, justificatifCallBack) } catch (_) { } try { getJusti(justificatifCallBack) } catch { }
} }
function order(keyword, callback = () => { }, el, assi = true) { function order(keyword, callback = () => { }, el, assi = true) {
@ -249,6 +270,13 @@
keyValueA = getModuleImpl(a); keyValueA = getModuleImpl(a);
keyValueB = getModuleImpl(b); keyValueB = getModuleImpl(b);
} }
if (keyword.indexOf("etudid") != -1) {
keyValueA = getEtudiant(a.etudid);
keyValueB = getEtudiant(b.etudid);
keyValueA = `${keyValueA.prenom.capitalize()} ${keyValueA.nom.toUpperCase()}`
keyValueB = `${keyValueB.prenom.capitalize()} ${keyValueB.nom.toUpperCase()}`
}
let orderDertermined = keyValueA > keyValueB; let orderDertermined = keyValueA > keyValueB;
@ -266,351 +294,14 @@
if (assi) { if (assi) {
orderAssiduites = !orderAssiduites; orderAssiduites = !orderAssiduites;
getAllAssiduitesFromEtud(etudid, (a) => { call(a, orderAssiduites) }) getAssi((a) => { call(a, orderAssiduites) });
} else { } else {
orderJustificatifs = !orderJustificatifs; orderJustificatifs = !orderJustificatifs;
getAllJustificatifsFromEtud(etudid, (a) => { call(a, orderJustificatifs) }) getJusti((a) => { call(a, orderJustificatifs) });
} }
} }
function filter(assi = true) {
if (assi) {
let html = `
<div class="filter-body">
<h3>Affichage des colonnes:</h3>
<div class="filter-head">
<label>
Date de saisie
<input class="chk" type="checkbox" name="entry_date" id="entry_date">
</label>
<label>
Date de Début
<input class="chk" type="checkbox" name="date_debut" id="date_debut" checked>
</label>
<label>
Date de Fin
<input class="chk" type="checkbox" name="date_fin" id="date_fin" checked>
</label>
<label>
Etat
<input class="chk" type="checkbox" name="etat" id="etat" checked>
</label>
<label>
Module
<input class="chk" type="checkbox" name="moduleimpl_id" id="moduleimpl_id" checked>
</label>
<label>
Justifiée
<input class="chk" type="checkbox" name="est_just" id="est_just" checked>
</label>
</div>
<hr>
<h3>Filtrage des colonnes:</h3>
<span class="filter-line">
<span class="filter-title" for="entry_date">Date de saisie</span>
<select name="entry_date_pref" id="entry_date_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="entry_date_time" id="entry_date_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_debut">Date de début</span>
<select name="date_debut_pref" id="date_debut_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_debut_time" id="date_debut_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_fin">Date de fin</span>
<select name="date_fin_pref" id="date_fin_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_fin_time" id="date_fin_time">
</span>
<span class="filter-line">
<span class="filter-title" for="etat">Etat</span>
<input checked type="checkbox" name="etat_present" id="etat_present" class="rbtn present" value="present">
<input checked type="checkbox" name="etat_retard" id="etat_retard" class="rbtn retard" value="retard">
<input checked type="checkbox" name="etat_absent" id="etat_absent" class="rbtn absent" value="absent">
</span>
<span class="filter-line">
<span class="filter-title" for="moduleimpl_id">Module</span>
<select id="moduleimpl_id">
<option value="">Pas de filtre</option>
</select>
</span>
<span class="filter-line">
<span class="filter-title" for="est_just">Est Justifiée</span>
<select id="est_just">
<option value="">Pas de filtre</option>
<option value="true">Oui</option>
<option value="false">Non</option>
</select>
</span>
</div>
`;
const span = document.createElement('span');
span.innerHTML = html
html = span.firstElementChild
const filterHead = html.querySelector('.filter-head');
filterHead.innerHTML = ""
let cols = ["entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"];
cols.forEach((k) => {
const label = document.createElement('label')
label.classList.add('f-label')
const s = document.createElement('span');
s.textContent = columnTranslator(k);
const input = document.createElement('input');
input.classList.add('chk')
input.type = "checkbox"
input.name = k
input.id = k;
input.checked = filterAssiduites.columns.includes(k)
label.appendChild(s)
label.appendChild(input)
filterHead.appendChild(label)
})
const sl = html.querySelector('.filter-line #moduleimpl_id');
let opts = []
Object.keys(moduleimpls).forEach((k) => {
const opt = document.createElement('option');
opt.value = k == null ? "null" : k;
opt.textContent = moduleimpls[k];
opts.push(opt);
})
opts = opts.sort((a, b) => {
return a.value < b.value
})
sl.append(...opts);
// Mise à jour des filtres
Object.keys(filterAssiduites.filters).forEach((key) => {
const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement;
if (key.indexOf('date') != -1) {
l.querySelector(`#${key}_pref`).value = filterAssiduites.filters[key].pref;
l.querySelector(`#${key}_time`).value = filterAssiduites.filters[key].time.format("YYYY-MM-DDTHH:mm");
} else if (key.indexOf('etat') != -1) {
l.querySelectorAll('input').forEach((e) => {
e.checked = filterAssiduites.filters[key].includes(e.value)
})
} else if (key.indexOf("module") != -1) {
l.querySelector('#moduleimpl_id').value = filterAssiduites.filters[key];
} else if (key.indexOf("est_just") != -1) {
l.querySelector('#est_just').value = filterAssiduites.filters[key];
}
})
openPromptModal("Filtrage des assiduités", html, () => {
const columns = [...document.querySelectorAll('.chk')]
.map((el) => { if (el.checked) return el.id })
.filter((el) => el)
filterAssiduites.columns = columns
filterAssiduites.filters = {}
//reste des filtres
const lines = [...document.querySelectorAll('.filter-line')];
lines.forEach((l) => {
const key = l.querySelector('.filter-title').getAttribute('for');
if (key.indexOf('date') != -1) {
const pref = l.querySelector(`#${key}_pref`).value;
const time = l.querySelector(`#${key}_time`).value;
if (l.querySelector(`#${key}_time`).value != "") {
filterAssiduites.filters[key] = {
pref: pref,
time: new moment.tz(time, TIMEZONE)
}
}
} else if (key.indexOf('etat') != -1) {
filterAssiduites.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value);
} else if (key.indexOf("module") != -1) {
filterAssiduites.filters[key] = l.querySelector('#moduleimpl_id').value;
} else if (key.indexOf("est_just") != -1) {
filterAssiduites.filters[key] = l.querySelector('#est_just').value;
}
})
getAllAssiduitesFromEtud(etudid, assiduiteCallBack)
}, () => { }, "#7059FF");
} else {
let html = `
<div class="filter-body">
<h3>Affichage des colonnes:</h3>
<div class="filter-head">
<label>
Date de saisie
<input class="chk" type="checkbox" name="entry_date" id="entry_date">
</label>
<label>
Date de Début
<input class="chk" type="checkbox" name="date_debut" id="date_debut" checked>
</label>
<label>
Date de Fin
<input class="chk" type="checkbox" name="date_fin" id="date_fin" checked>
</label>
<label>
Etat
<input class="chk" type="checkbox" name="etat" id="etat" checked>
</label>
<label>
Raison
<input class="chk" type="checkbox" name="raison" id="raison" checked>
</label>
<label>
Fichier
<input class="chk" type="checkbox" name="fichier" id="fichier" checked>
</label>
</div>
<hr>
<h3>Filtrage des colonnes:</h3>
<span class="filter-line">
<span class="filter-title" for="entry_date">Date de saisie</span>
<select name="entry_date_pref" id="entry_date_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="entry_date_time" id="entry_date_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_debut">Date de début</span>
<select name="date_debut_pref" id="date_debut_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_debut_time" id="date_debut_time">
</span>
<span class="filter-line">
<span class="filter-title" for="date_fin">Date de fin</span>
<select name="date_fin_pref" id="date_fin_pref">
<option value="-1">Avant</option>
<option value="0">Égal</option>
<option value="1">Après</option>
</select>
<input type="datetime-local" name="date_fin_time" id="date_fin_time">
</span>
<span class="filter-line">
<span class="filter-title" for="etat">Etat</span>
<label>
Valide
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="valide">
</label>
<label>
Non Valide
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="non_valide">
</label>
<label>
En Attente
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="attente">
</label>
<label>
Modifié
<input checked type="checkbox" name="etat_valide" id="etat_valide" class="" value="modifie">
</label>
</span>
</div>
`;
const span = document.createElement('span');
span.innerHTML = html
html = span.firstElementChild
const filterHead = html.querySelector('.filter-head');
filterHead.innerHTML = ""
let cols = ["entry_date", "date_debut", "date_fin", "etat", "raison", "fichier"];
cols.forEach((k) => {
const label = document.createElement('label')
label.classList.add('f-label')
const s = document.createElement('span');
s.textContent = columnTranslator(k);
const input = document.createElement('input');
input.classList.add('chk')
input.type = "checkbox"
input.name = k
input.id = k;
input.checked = filterJustificatifs.columns.includes(k)
label.appendChild(s)
label.appendChild(input)
filterHead.appendChild(label)
})
// Mise à jour des filtres
Object.keys(filterJustificatifs.filters).forEach((key) => {
const l = html.querySelector(`.filter-title[for="${key}"]`).parentElement;
if (key.indexOf('date') != -1) {
l.querySelector(`#${key}_pref`).value = filterJustificatifs.filters[key].pref;
l.querySelector(`#${key}_time`).value = filterJustificatifs.filters[key].time.format("YYYY-MM-DDTHH:mm");
} else if (key.indexOf('etat') != -1) {
l.querySelectorAll('input').forEach((e) => {
e.checked = filterJustificatifs.filters[key].includes(e.value)
})
}
})
openPromptModal("Filtrage des Justificatifs", html, () => {
const columns = [...document.querySelectorAll('.chk')]
.map((el) => { if (el.checked) return el.id })
.filter((el) => el)
filterJustificatifs.columns = columns
filterJustificatifs.filters = {}
//reste des filtres
const lines = [...document.querySelectorAll('.filter-line')];
lines.forEach((l) => {
const key = l.querySelector('.filter-title').getAttribute('for');
if (key.indexOf('date') != -1) {
const pref = l.querySelector(`#${key}_pref`).value;
const time = l.querySelector(`#${key}_time`).value;
if (l.querySelector(`#${key}_time`).value != "") {
filterJustificatifs.filters[key] = {
pref: pref,
time: new moment.tz(time, TIMEZONE)
}
}
} else if (key.indexOf('etat') != -1) {
filterJustificatifs.filters[key] = [...l.querySelectorAll("input:checked")].map((e) => e.value);
}
})
getAllJustificatifsFromEtud(etudid, justificatifCallBack)
}, () => { }, "#7059FF");
}
}
function columnTranslator(colName) { function columnTranslator(colName) {
switch (colName) { switch (colName) {
@ -632,6 +323,8 @@
return "Fichier"; return "Fichier";
case "etudid": case "etudid":
return "Etudiant"; return "Etudiant";
case "formsemestre":
return "Semestre";
} }
} }
@ -641,6 +334,103 @@
contextMenu.style.top = `${e.clientY - contextMenu.offsetHeight}px`; contextMenu.style.top = `${e.clientY - contextMenu.offsetHeight}px`;
contextMenu.style.left = `${e.clientX}px`; contextMenu.style.left = `${e.clientX}px`;
contextMenu.style.display = "block"; contextMenu.style.display = "block";
if (contextMenu.childElementCount > 3) {
contextMenu.removeChild(contextMenu.lastElementChild)
}
if (selectedRow.getAttribute('type') == "assiduite") {
const li = document.createElement('li')
li.textContent = "Justifier"
li.addEventListener('click', () => {
let obj_id = selectedRow.getAttribute('obj_id');
assiduite = Object.values(assiduites).flat().filter((a) => { return a.assiduite_id == obj_id })
if (assiduite && !assiduite[0].est_just && assiduite[0].etat != "PRESENT") {
fastJustify(assiduite[0])
} else {
openAlertModal("Erreur", document.createTextNode("L'assiduité est déjà justifiée ou ne peut pas l'être."))
}
})
contextMenu.appendChild(li)
}
}
function downloadStr(data, name) {
const blob = new Blob([data], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.setAttribute('href', url)
a.setAttribute('download', name);
a.click()
a.remove()
}
function askDownload(data) {
const div = document.createElement('div');
const head = document.createElement('h3');
const input = document.createElement('input');
head.textContent = "Veuillez nommer le fichier qui sera téléchargé (sera au format CSV)"
input.type = "text";
input.placeholder = "liste.csv"
div.appendChild(head)
div.appendChild(input)
openPromptModal("Préparation du téléchargement", div, () => {
downloadStr(data, input.value ? input.value : "download.csv")
}, () => { }, "green");
}
function toCSV(array, filters) {
array = filterArray(array, filters.filters)
let csv = filters.columns.map((c) => columnTranslator(c)).join(',') + "\n";
array.forEach((a) => {
let line = ""
filters.columns.forEach((c) => {
switch (c) {
case "fichier":
line += a[c] ? "Oui," : "Non,"
break;
case "etudid":
const e = getEtudiant(a.etudid);
line += `${e.nom.toUpperCase()} ${e.prenom.capitalize()},`
break;
case "formsemestre":
line += a.hasOwnProperty("formsemestre") ? a.formsemestre.title : ""
line += ","
break;
case "est_just":
line += a[c] ? "Oui," : "Non,"
break;
case "moduleimpl_id":
line += `${getModuleImpl(a)},`
break;
default:
line += `${a[c]},`;
break;
}
})
line = line.substring(0, line.lastIndexOf(',')) + "\n"
csv += line;
})
askDownload(csv);
}
function getEtudiant(id) {
if (id in etuds) {
return etuds[id];
}
getSingleEtud(id);
return etuds[id];
} }
</script> </script>

Some files were not shown because too many files have changed in this diff Show More