Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
128 changed files with 3646 additions and 2226 deletions
Showing only changes of commit d8fbedb96d - Show all commits

View File

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

View File

@ -359,7 +359,7 @@ def bulletin(
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
code_type : "etudid", "nip" ou "ine"
@ -376,7 +376,7 @@ def bulletin(
formsemestre = FormSemestre.query.filter_by(id=formsemestre_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:
return json_error(404, "formsemestre inexistant", as_response=True)
return json_error(404, "formsemestre inexistant")
app.set_sco_dept(dept.acronym)
if code_type == "nip":
@ -399,7 +399,7 @@ def bulletin(
formsemestre,
etud,
version=version,
format="pdf",
fmt="pdf",
with_img_signatures_pdf=with_img_signatures_pdf,
)
return pdf_response

View File

@ -8,8 +8,9 @@
from datetime import datetime
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_sqlalchemy.query import Query
import app.scodoc.sco_assiduites as scass
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 get_model_api_object, tools
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 (
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_permissions import Permission
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
@ -130,6 +137,8 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
@api_web_bp.route(
"/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
@scodoc
@as_json
@ -143,9 +152,77 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
if with_query:
justificatifs_query = _filter_manager(request, justificatifs_query)
data_set: list[dict] = []
for just in justificatifs_query.all():
data = just.to_dict(format_api=True)
for just in justificatifs_query:
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)
return data_set
@ -380,7 +457,7 @@ def justif_edit(justif_id: int):
"après": compute_assiduites_justified(
justificatif_unique.etudid,
[justificatif_unique],
False,
True,
),
}
}
@ -436,7 +513,7 @@ def _delete_singular(justif_id: int, database):
if archive_name is not None:
archiver: JustificatifArchiver = JustificatifArchiver()
try:
archiver.delete_justificatif(justificatif_unique.etudid, archive_name)
archiver.delete_justificatif(justificatif_unique.etudiant, archive_name)
except ValueError:
pass
@ -481,7 +558,7 @@ def justif_import(justif_id: int = None):
try:
fname: str
archive_name, fname = archiver.save_justificatif(
etudid=justificatif_unique.etudid,
justificatif_unique.etudiant,
filename=file.filename,
data=file.stream.read(),
archive_name=archive_name,
@ -512,7 +589,7 @@ def justif_export(justif_id: int = None, filename: str = None):
if g.scodoc_dept:
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
if archive_name is None:
@ -522,7 +599,7 @@ def justif_export(justif_id: int = None, filename: str = None):
try:
return archiver.get_justificatif_file(
archive_name, justificatif_unique.etudid, filename
archive_name, justificatif_unique.etudiant, filename
)
except ScoValueError as err:
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"):
return json_error(404, "param 'remove': Valeur invalide")
archiver: JustificatifArchiver = JustificatifArchiver()
etudid: int = justificatif_unique.etudid
etud = justificatif_unique.etudiant
try:
if remove == "all":
archiver.delete_justificatif(etudid=etudid, archive_name=archive_name)
archiver.delete_justificatif(etud, archive_name=archive_name)
justificatif_unique.fichier = None
db.session.add(justificatif_unique)
db.session.commit()
@ -575,13 +652,13 @@ def justif_remove(justif_id: int = None):
else:
for fname in data.get("filenames", []):
archiver.delete_justificatif(
etudid=etudid,
etud,
archive_name=archive_name,
filename=fname,
)
if len(archiver.list_justificatifs(archive_name, etudid)) == 0:
archiver.delete_justificatif(etudid, archive_name)
if len(archiver.list_justificatifs(archive_name, etud)) == 0:
archiver.delete_justificatif(etud, archive_name)
justificatif_unique.fichier = None
db.session.add(justificatif_unique)
db.session.commit()
@ -616,16 +693,16 @@ def justif_list(justif_id: int = None):
archiver: JustificatifArchiver = JustificatifArchiver()
if archive_name is not None:
filenames = archiver.list_justificatifs(
archive_name, justificatif_unique.etudid
archive_name, justificatif_unique.etudiant
)
retour = {"total": len(filenames), "filenames": []}
for fi in filenames:
if int(fi[1]) == current_user.id or current_user.has_permission(
for filename in filenames:
if int(filename[1]) == current_user.id or current_user.has_permission(
Permission.ScoJustifView
):
retour["filenames"].append(fi[0])
retour["filenames"].append(filename[0])
return retour
@ -688,12 +765,41 @@ def _filter_manager(requested, justificatifs_query):
# cas 5 : 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
try:
formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first()
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

View File

@ -67,3 +67,28 @@ def moduleimpl(moduleimpl_id: int):
query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
modimpl: ModuleImpl = query.first_or_404()
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)
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):
"Un jeton pour cet user. Stocké en base, non commité."
now = datetime.utcnow()

View File

@ -512,10 +512,10 @@ class BulletinBUT:
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
# --- Decision Jury
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
infos, _ = sco_bulletins.etud_descr_situation_semestre(
etud.id,
self.res.formsemestre,
format="html",
fmt="html",
show_date_inscr=self.prefs["bul_show_date_inscr"],
show_decisions=self.prefs["bul_show_decision"],
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":
bul: dict = bulletins_sem.bulletin_etud_complet(etud)
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 = (
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
if "semestre" in bul and "decision_ue" in bul["semestre"]
else {}
)
if not "ues" in bul:
if "ues" not in bul:
raise ScoValueError("Aucune UE à afficher")
cursus = cursus_but.EtudCursusBUT(etud, formsemestre.formation)
refcomp = formsemestre.formation.referentiel_competence

View File

@ -50,7 +50,7 @@ def make_bulletin_but_court_pdf(
try:
PDFLOCK.acquire()
bul_generator = BulletinGeneratorBUTCourt(**locals())
bul_pdf = bul_generator.generate(format="pdf")
bul_pdf = bul_generator.generate(fmt="pdf")
finally:
PDFLOCK.release()
return bul_pdf
@ -499,14 +499,15 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
def boite_decisions_jury(self):
"""La boite en bas à droite avec jury"""
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"""
Jury tenu le {
Décision saisie le {
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/>
"""
if self.bul["semestre"]["autorisation_inscription"]:
if self.bul["semestre"].get("autorisation_inscription", None):
txt += (
"<br/>Autorisé à s'inscrire en <b>"
+ ", ".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()
- instance de BulletinGeneratorStandardBUT
- BulletinGeneratorStandardBUT.generate(format="pdf")
- BulletinGeneratorStandardBUT.generate(fmt="pdf")
sco_bulletins_generator.BulletinGenerator.generate()
.generate_pdf()
.bul_table() (ci-dessous)
@ -24,6 +24,7 @@ from reportlab.lib.colors import blue
from reportlab.lib.units import cm, mm
from reportlab.platypus import Paragraph, Spacer
from app.models import ScoDocSiteConfig
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc import gen_tables
from app.scodoc.codes_cursus import UE_SPORT
@ -48,6 +49,8 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
- en HTML: une chaine
- 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 = [
# ---- TABLE SYNTHESE UES
self.but_table_synthese_ues(),
@ -71,7 +74,7 @@ class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
html_class_ignore_default=True,
html_with_td_classes=True,
)
table_objects = table.gen(format=fmt)
table_objects = table.gen(fmt=fmt)
objects += table_objects
# objects += [KeepInFrame(0, 0, table_objects, mode="shrink")]
if i != 2:

View File

@ -258,7 +258,7 @@ def bulletin_but_xml_compat(
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etudid,
formsemestre,
format="xml",
fmt="xml",
show_uevalid=sco_preferences.get_preference(
"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
import html
import re
from typing import Union
import numpy as np
from flask import flash, g, url_for
from app import db
from app import log
from app.but import cursus_but
from app.but.cursus_but import EtudCursusBUT
from app.but.rcue import RegroupementCoherentUE
from app.comp.res_but import ResultatsSemestreBUT
@ -150,7 +148,7 @@ class DecisionsProposees:
def __init__(
self,
etud: Identite = None,
code: Union[str, list[str]] = None,
code: str | list[str] | None = None,
explanation="",
code_valide=None,
include_communs=True,

View File

@ -94,7 +94,7 @@ def pvjury_page_but(formsemestre_id: int, fmt="html"):
},
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(

View File

@ -6,7 +6,6 @@
"""Jury BUT: un RCUE, ou Regroupe Cohérent d'UEs
"""
from typing import Union
from flask_sqlalchemy.query import Query
from app.comp.res_but import ResultatsSemestreBUT
@ -205,7 +204,7 @@ class RegroupementCoherentUE:
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"
validation = self.query_validations().first()
if (validation is not None) and (

View File

@ -344,8 +344,12 @@ def compute_ue_moys_classic(
pd.Series(
[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(columns=[ue.id for ue in ues], index=modimpl_inscr_df.index),
pd.DataFrame(
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:
sem_matrix = sem_matrix[:, modimpl_mask]
@ -400,6 +404,7 @@ def compute_ue_moys_classic(
},
index=modimpl_inscr_df.index,
columns=[ue.id for ue in ues],
dtype=float,
)
# 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)
@ -415,6 +420,7 @@ def compute_ue_moys_classic(
coefs.sum(axis=2).T,
index=modimpl_inscr_df.index, # etudids
columns=[ue.id for ue in ues],
dtype=float,
)
with np.errstate(invalid="ignore"): # ignore les 0/0 (-> NaN)
etud_moy_gen = np.sum(

View File

@ -186,7 +186,10 @@ def scodoc7func(func):
arg_names = argspec.args
for arg_name in arg_names: # pour chaque arg de la fonction vue
# peut produire une KeyError s'il manque un argument attendu:
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
# necessary for db ids and boolean values
try:

View File

@ -76,7 +76,7 @@ class TimeField(StringField):
class ConfigAssiduitesForm(FlaskForm):
"Formulaire paramétrage Module Assiduités"
"Formulaire paramétrage Module Assiduité"
morning_time = TimeField("Début de la journée")
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 wtforms import BooleanField, SubmitField
from wtforms import BooleanField, SubmitField, ValidationError
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):
@ -50,7 +59,8 @@ class ConfigCASForm(FlaskForm):
)
cas_login_route = StringField(
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",
)
cas_logout_route = StringField(
@ -70,6 +80,18 @@ class ConfigCASForm(FlaskForm):
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_certificate_file = FileField(
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.""",
validators=[Optional(), Email()],
)
disable_bul_pdf = BooleanField("empêcher les exports des bulletins en PDF")
submit_scodoc = SubmitField("Valider")
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_periode2": ScoDocSiteConfig.get_month_debut_periode2(),
"email_from_addr": ScoDocSiteConfig.get("email_from_addr"),
"disable_bul_pdf": ScoDocSiteConfig.is_bul_pdf_disabled(),
}
)
if request.method == "POST" and (
@ -139,6 +141,13 @@ def configuration():
)
if ScoDocSiteConfig.set("email_from_addr", form_scodoc.data["email_from_addr"]):
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 render_template(

View File

@ -123,7 +123,7 @@ class Assiduite(db.Model):
user_id: int = None,
est_just: bool = False,
external_data: dict = None,
) -> object or int:
) -> "Assiduite":
"""Créer une nouvelle assiduité pour l'étudiant"""
# Vérification de non duplication des périodes
assiduites: Query = etud.assiduites
@ -134,7 +134,10 @@ class Assiduite(db.Model):
if not 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:
@ -153,7 +156,7 @@ class Assiduite(db.Model):
external_data=external_data,
)
else:
raise ScoValueError("L'étudiant n'est pas inscrit au moduleimpl")
raise ScoValueError("L'étudiant n'est pas inscrit au module")
else:
nouv_assiduite = Assiduite(
date_debut=date_debut,
@ -282,7 +285,7 @@ class Justificatif(db.Model):
entry_date: datetime = None,
user_id: int = None,
external_data: dict = None,
) -> object or int:
) -> "Justificatif":
"""Créer un nouveau justificatif pour l'étudiant"""
nouv_justificatif = Justificatif(
date_debut=date_debut,
@ -310,7 +313,7 @@ def is_period_conflicting(
date_debut: datetime,
date_fin: datetime,
collection: Query,
collection_cls: Assiduite or Justificatif,
collection_cls: Assiduite | Justificatif,
) -> bool:
"""
Vérifie si une date n'entre pas en collision
@ -350,14 +353,26 @@ def compute_assiduites_justified(
if justificatifs is None:
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_justifiees: list[int] = []
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(
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
assiduites_justifiees.append(assi.assiduite_id)
@ -371,16 +386,23 @@ def compute_assiduites_justified(
def get_assiduites_justif(assiduite_id: int, long: bool):
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(
etudid: int, date_debut: datetime, date_fin: datetime, long: bool = False
def get_justifs_from_date(
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.date_debut <= date_debut,
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]

View File

@ -214,10 +214,12 @@ def dict_decision_jury(etud: Identite, formsemestre: FormSemestre) -> dict:
decisions["decision_rcue"] = []
decisions["descr_decisions_rcue"] = ""
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(
etudid=etud.id,
annee_scolaire=formsemestre.annee_scolaire(),
ordre=annee_but,
referentiel_competence_id=formsemestre.formation.referentiel_competence_id,
).first()
if validation:

View File

@ -5,6 +5,7 @@
import json
import urllib.parse
import re
from flask import flash
from app import current_app, db, log
@ -95,6 +96,7 @@ class ScoDocSiteConfig(db.Model):
"enable_entreprises": bool,
"month_debut_annee_scolaire": int,
"month_debut_periode2": int,
"disable_bul_pdf": bool,
# CAS
"cas_enable": bool,
"cas_server": str,
@ -102,7 +104,8 @@ class ScoDocSiteConfig(db.Model):
"cas_logout_route": str,
"cas_validate_route": str,
"cas_attribute_id": str,
# Assiduités
"cas_uid_from_mail_regexp": str,
# Assiduité
"morning_time": str,
"lunch_time": str,
"afternoon_time": str,
@ -235,6 +238,12 @@ class ScoDocSiteConfig(db.Model):
cfg = ScoDocSiteConfig.query.filter_by(name="enable_entreprises").first()
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
def enable_entreprises(cls, enabled=True) -> bool:
"""Active (ou déactive) le module entreprises. True si changement."""
@ -251,6 +260,22 @@ class ScoDocSiteConfig(db.Model):
return True
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
def get(cls, name: str, default: str = "") -> str:
"Get configuration param; empty string or specified default if unset"
@ -360,7 +385,7 @@ class ScoDocSiteConfig(db.Model):
cls.set("personalized_links", "")
raise ScoValueError(
"Attention: liens personnalisés erronés: ils ont été effacés."
)
) from exc
return [PersonalizedLink(**item) for item in links_dict]
@classmethod
@ -372,6 +397,59 @@ class ScoDocSiteConfig(db.Model):
data_links = json.dumps(links_dict)
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:
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
assiduites = db.relationship("Assiduite", back_populates="etudiant", lazy="dynamic")
assiduites = db.relationship(
"Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
)
justificatifs = db.relationship(
"Justificatif", back_populates="etudiant", lazy="dynamic"
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
)
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)")
data["note_max"] = note_max
# --- coefficient
coef = data.get("coefficient", 1.0) or 1.0
coef = data.get("coefficient", None)
if coef is None:
coef = 1.0
try:
coef = float(coef)
except ValueError as exc:

View File

@ -18,6 +18,7 @@ from flask_login import current_user
from flask import flash, g, url_for
from sqlalchemy.sql import text
from sqlalchemy import func
import app.scodoc.sco_utils as scu
from app import db, log
@ -138,6 +139,7 @@ class FormSemestre(db.Model):
secondary="notes_formsemestre_responsables",
lazy=True,
backref=db.backref("formsemestres", lazy=True),
order_by=func.upper(User.nom),
)
partitions = db.relationship(
"Partition",
@ -195,6 +197,7 @@ class FormSemestre(db.Model):
"""
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
d.pop("groups_auto_assignment_data", None)
# ScoDoc7 output_formators: (backward compat)
d["formsemestre_id"] = self.id
d["titre_num"] = self.titre_num()
@ -226,6 +229,7 @@ class FormSemestre(db.Model):
"""
d = dict(self.__dict__)
d.pop("_sa_instance_state", None)
d.pop("groups_auto_assignment_data", None)
d["annee_scolaire"] = self.annee_scolaire()
if self.date_debut:
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)
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
def etudids_actifs(self) -> set:
"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.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_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
@ -50,7 +51,7 @@ class Partition(db.Model):
backref=db.backref("partition", lazy=True),
lazy="dynamic",
cascade="all, delete-orphan",
order_by="GroupDescr.numero",
order_by="GroupDescr.numero, GroupDescr.group_name",
)
def __init__(self, **kwargs):
@ -240,6 +241,21 @@ class GroupDescr(db.Model):
d["partition"] = self.partition.to_dict(with_groups=False)
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
def check_name(
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"),
)
def to_dict(self) -> dict:
"dict repr."
return {
"id": self.id,
"etudid": self.etudid,
"moduleimpl_id": self.moduleimpl_id,
}
@classmethod
def etud_modimpls_in_ue(
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.
"""
if self.formation.has_locked_sems(self.ue.semestre_idx):
current_app.logguer.info(
f"set_ue_coef_dict: locked formation, ignoring request"
current_app.logger.info(
"set_ue_coef_dict: locked formation, ignoring request"
)
raise ScoValueError("Formation verrouillée")
changed = False
@ -213,8 +213,8 @@ class Module(db.Model):
def update_ue_coef_dict(self, ue_coef_dict: dict):
"""update coefs vers UE (ajoute aux existants)"""
if self.formation.has_locked_sems(self.ue.semestre_idx):
current_app.logguer.info(
f"update_ue_coef_dict: locked formation, ignoring request"
current_app.logger.info(
"update_ue_coef_dict: locked formation, ignoring request"
)
raise ScoValueError("Formation verrouillée")
current = self.get_ue_coef_dict()
@ -232,7 +232,7 @@ class Module(db.Model):
def delete_ue_coef(self, ue):
"""delete coef"""
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"
)
raise ScoValueError("Formation verrouillée")

View File

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

View File

@ -58,7 +58,7 @@ def sidebar_common():
]
if current_user.has_permission(Permission.ScoAbsChange):
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(
Permission.ScoUsersAdmin

View File

@ -47,7 +47,6 @@
nommé _description.txt qui est une description (humaine, format libre) de l'archive.
"""
from typing import Union
import datetime
import glob
import json
@ -81,7 +80,7 @@ from app.scodoc import sco_pv_pdf
from app.scodoc.sco_exceptions import ScoValueError
class BaseArchiver(object):
class BaseArchiver:
def __init__(self, archive_type=""):
self.archive_type = archive_type
self.initialized = False
@ -92,14 +91,17 @@ class BaseArchiver(object):
"set dept"
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:
return
dirs = [Config.SCODOC_VAR_DIR, "archives"]
if 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)
path = dirs[0]
for directory in dirs[1:]:
@ -112,15 +114,13 @@ class BaseArchiver(object):
finally:
scu.GSL.release()
self.initialized = True
if self.dept_id is None:
self.dept_id = getattr(g, "scodoc_dept_id")
def get_obj_dir(self, oid: int):
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).
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))
try:
scu.GSL.acquire()
@ -141,21 +141,21 @@ class BaseArchiver(object):
scu.GSL.release()
return obj_dir
def list_oids(self):
def list_oids(self, dept_id: int = None):
"""
:return: list of archive oids
"""
self.initialize()
self.initialize(dept_id)
base = os.path.join(self.root, str(self.dept_id)) + os.path.sep
dirs = glob.glob(base + "*")
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
:return: list of archive identifiers for this object (paths to non empty dirs)
"""
self.initialize()
base = self.get_obj_dir(oid) + os.path.sep
self.initialize(dept_id)
base = self.get_obj_dir(oid, dept_id=dept_id) + os.path.sep
dirs = glob.glob(
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]"
@ -165,9 +165,9 @@ class BaseArchiver(object):
dirs.sort()
return dirs
def delete_archive(self, archive_id: str):
def delete_archive(self, archive_id: str, dept_id: int = None):
"""Delete (forever) this archive"""
self.initialize()
self.initialize(dept_id)
try:
scu.GSL.acquire()
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("-")]
)
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"""
self.initialize()
self.initialize(dept_id)
try:
scu.GSL.acquire()
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
)
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)"""
self.initialize()
self.initialize(dept_id)
if not self.is_valid_archive_name(archive_name):
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):
log(
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")
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"""
self.initialize()
self.initialize(dept_id)
filename = os.path.join(archive_id, "_description.txt")
try:
with open(filename, encoding=scu.SCO_ENCODING) as f:
@ -229,11 +229,11 @@ class BaseArchiver(object):
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."""
# id suffixé par YYYY-MM-DD-hh-mm-ss
archive_id = (
self.get_obj_dir(oid)
self.get_obj_dir(oid, dept_id=dept_id)
+ os.path.sep
+ "-".join([f"{x:02d}" for x in time.localtime()[:6]])
)
@ -248,7 +248,13 @@ class BaseArchiver(object):
self.store(archive_id, "_description.txt", description)
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.
Filename may be modified (sanitized): return used filename
The file is created or replaced.
@ -256,7 +262,7 @@ class BaseArchiver(object):
"""
if isinstance(data, str):
data = data.encode(scu.SCO_ENCODING)
self.initialize()
self.initialize(dept_id)
filename = scu.sanitize_filename(filename)
log(f"storing {filename} ({len(data)} bytes) in {archive_id}")
try:
@ -264,27 +270,36 @@ class BaseArchiver(object):
fname = os.path.join(archive_id, filename)
with open(fname, "wb") as f:
f.write(data)
except FileNotFoundError as exc:
raise ScoValueError(
f"Erreur stockage archive (dossier inexistant, chemin {fname})"
) from exc
finally:
scu.GSL.release()
return filename
def get(self, archive_id: str, filename: str):
def get(self, archive_id: str, filename: str, dept_id: int = None):
"""Retreive data"""
self.initialize()
self.initialize(dept_id)
if not scu.is_valid_filename(filename):
log(f"""Archiver.get: invalid filename '{filename}'""")
raise ScoValueError("archive introuvable (déjà supprimée ?)")
fname = os.path.join(archive_id, filename)
log(f"reading archive file {fname}")
try:
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
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.
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)
mime = mimetypes.guess_type(filename)[0]
if mime is None:
@ -298,7 +313,7 @@ class SemsArchiver(BaseArchiver):
BaseArchiver.__init__(self, archive_type="")
PVArchive = SemsArchiver()
PV_ARCHIVER = SemsArchiver()
# ----------------------------------------------------------------------------
@ -332,8 +347,10 @@ def do_formsemestre_archive(
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem_archive_id = formsemestre_id
archive_id = PVArchive.create_obj_archive(sem_archive_id, description)
date = PVArchive.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
archive_id = PV_ARCHIVER.create_obj_archive(
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:
# 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)
data, _ = gen_formsemestre_recapcomplet_excel(res, include_evaluations=True)
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)
table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
formsemestre, res, include_evaluations=True
@ -367,33 +389,43 @@ def do_formsemestre_archive(
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
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
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
if formsemestre.formation.is_apc():
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls")
data = response.get_data()
else: # formations classiques
data = sco_pv_forms.formsemestre_pvjury(
formsemestre_id, format="xls", publish=False
formsemestre_id, fmt="xls", publish=False
)
if data:
PVArchive.store(
PV_ARCHIVER.store(
archive_id,
"Decisions_Jury" + scu.XLSX_SUFFIX,
data,
dept_id=formsemestre.dept_id,
)
# Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=bul_version
)
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):
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
formsemestre_id,
@ -403,7 +435,12 @@ def do_formsemestre_archive(
signature=signature,
)
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):
data = sco_pv_pdf.pvjury_pdf(
@ -419,7 +456,12 @@ def do_formsemestre_archive(
anonymous=anonymous,
)
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):
@ -558,14 +600,21 @@ enregistrés et non modifiables, on peut les retrouver ultérieurement.
def formsemestre_list_archives(formsemestre_id):
"""Page listing archives"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre_id
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 = {
"archive_id": archive_id,
"description": PVArchive.get_archive_description(archive_id),
"date": PVArchive.get_archive_date(archive_id),
"content": PVArchive.list_archive(archive_id),
"description": PV_ARCHIVER.get_archive_description(
archive_id, dept_id=formsemestre.dept_id
),
"date": PV_ARCHIVER.get_archive_date(archive_id),
"content": PV_ARCHIVER.list_archive(
archive_id, dept_id=formsemestre.dept_id
),
}
L.append(a)
@ -575,7 +624,7 @@ def formsemestre_list_archives(formsemestre_id):
else:
H.append("<ul>")
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(
'<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."""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(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):
@ -617,7 +668,9 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=
)
)
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(
"notes.formsemestre_list_archives",
@ -628,7 +681,7 @@ def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=
if not dialog_confirmed:
return scu.confirm_dialog(
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>
<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")
return flask.redirect(dest_url)

View File

@ -52,7 +52,8 @@ class EtudsArchiver(sco_archives.BaseArchiver):
sco_archives.BaseArchiver.__init__(self, archive_type="docetuds")
EtudsArchive = EtudsArchiver()
# Global au processus, attention !
ETUDS_ARCHIVER = EtudsArchiver()
def can_edit_etud_archive(authuser):
@ -60,21 +61,21 @@ def can_edit_etud_archive(authuser):
return authuser.has_permission(Permission.ScoEtudAddAnnotations)
def etud_list_archives_html(etudid):
def etud_list_archives_html(etud: Identite):
"""HTML snippet listing archives"""
can_edit = can_edit_etud_archive(current_user)
etuds = sco_etud.get_etud_info(etudid=etudid)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etudid
etud_archive_id = etud.id
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 = {
"archive_id": archive_id,
"description": EtudsArchive.get_archive_description(archive_id),
"date": EtudsArchive.get_archive_date(archive_id),
"content": EtudsArchive.list_archive(archive_id),
"description": ETUDS_ARCHIVER.get_archive_description(
archive_id, dept_id=etud.dept_id
),
"date": ETUDS_ARCHIVER.get_archive_date(archive_id),
"content": ETUDS_ARCHIVER.list_archive(archive_id, dept_id=etud.dept_id),
}
L.append(a)
delete_icon = scu.icontag(
@ -85,7 +86,7 @@ def etud_list_archives_html(etudid):
)
H = ['<div class="etudarchive"><ul>']
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(
"""<li><span class ="etudarchive_descr" title="%s">%s</span>"""
% (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"]:
H.append(
"""<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"]:
H.append("<em>aucun fichier !</em>")
if can_edit:
H.append(
'<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:
H.append('<span class="deletudarchive">' + delete_disabled_icon + "</span>")
@ -108,7 +109,7 @@ def etud_list_archives_html(etudid):
if can_edit:
H.append(
'<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>")
return "".join(H)
@ -121,12 +122,13 @@ def add_archives_info_to_etud_list(etuds):
for etud in etuds:
l = []
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(
"%s (%s)"
% (
EtudsArchive.get_archive_description(archive_id),
EtudsArchive.list_archive(archive_id)[0],
ETUDS_ARCHIVER.get_archive_description(archive_id),
ETUDS_ARCHIVER.list_archive(archive_id)[0],
)
)
etud["etudarchive"] = ", ".join(l)
@ -197,8 +199,8 @@ def _store_etud_file_to_new_archive(
filesize = len(data)
if filesize < 10 or filesize > scu.CONFIG.ETUD_MAX_FILE_SIZE:
return False, f"Fichier archive '{filename}' de taille invalide ! ({filesize})"
archive_id = EtudsArchive.create_obj_archive(etud_archive_id, description)
EtudsArchive.store(archive_id, filename, data)
archive_id = ETUDS_ARCHIVER.create_obj_archive(etud_archive_id, description)
ETUDS_ARCHIVER.store(archive_id, filename, data)
return True, "ok"
@ -212,14 +214,16 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
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:
return scu.confirm_dialog(
"""<h2>Confirmer la suppression des fichiers ?</h2>
<p>Fichier associé le %s à l'étudiant %s</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"],
),
dest_url="",
@ -232,7 +236,7 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
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")
return flask.redirect(
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")
etud = etuds[0]
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)

View File

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

View File

@ -4,9 +4,9 @@ Ecrit par Matthias Hartmann.
from datetime import date, datetime, time, timedelta
from pytz import UTC
from app import log
from app import log, db
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.formsemestre import FormSemestre, FormSemestreInscription
from app.scodoc import sco_formsemestre_inscriptions
@ -141,12 +141,9 @@ class CountCalculator:
self.hours += finish_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"""
assi: Assiduite
assiduites: list[Assiduite] = (
assiduites.all() if isinstance(assiduites, Assiduite) else assiduites
)
for assi in assiduites:
self.count += 1
delta: timedelta = assi.date_fin - assi.date_debut
@ -167,7 +164,7 @@ class CountCalculator:
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"""
return {
"compte": self.count,
@ -179,7 +176,7 @@ class CountCalculator:
def get_assiduites_stats(
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"""
if filtered is not None:
@ -212,7 +209,7 @@ def get_assiduites_stats(
output: dict = {}
calculator: CountCalculator = CountCalculator()
if "split" not in filtered:
if filtered is None or "split" not in filtered:
calculator.compute_assiduites(assiduites)
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(
collection: Assiduite or Justificatif,
collection: Assiduite | Justificatif,
user_id: int,
) -> Query:
"""
@ -286,8 +283,8 @@ def filter_by_user_id(
def filter_by_date(
collection: Assiduite or Justificatif,
collection_cls: Assiduite or Justificatif,
collection: Assiduite | Justificatif,
collection_cls: Assiduite | Justificatif,
date_deb: datetime = None,
date_fin: datetime = None,
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
"""
@ -320,7 +317,7 @@ def filter_justificatifs_by_etat(justificatifs: Justificatif, etat: str) -> Quer
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
"""
@ -328,8 +325,8 @@ def filter_by_module_impl(assiduites: Assiduite, module_impl_id: int or None) ->
def filter_by_formsemestre(
collection_query: Assiduite or Justificatif,
collection_class: Assiduite or Justificatif,
collection_query: Assiduite | Justificatif,
collection_class: Assiduite | Justificatif,
formsemestre: FormSemestre,
) -> Query:
"""
@ -358,7 +355,7 @@ def filter_by_formsemestre(
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
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(
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:
"""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_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(
justified,
Assiduite,
@ -403,6 +405,42 @@ def get_all_justified(
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
def get_assiduites_count(etudid: int, sem: dict) -> tuple[int, int]:
"""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(
etudid: int, formsemestre: FormSemestre
etudid: int, formsemestre: FormSemestre, moduleimpl_id: int = None
) -> tuple[int, int]:
"""Les comptes d'absences de cet étudiant dans ce semestre:
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)
return get_assiduites_count_in_interval(
etudid,
date_debut=formsemestre.date_debut,
date_fin=formsemestre.date_fin,
date_debut=scu.localize_datetime(
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),
moduleimpl_id=moduleimpl_id,
)
@ -441,6 +484,7 @@ def get_assiduites_count_in_interval(
metrique="demi",
date_debut: datetime = None,
date_fin: datetime = None,
moduleimpl_id: int = None,
):
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs, nb abs justifiées)
@ -452,30 +496,36 @@ def get_assiduites_count_in_interval(
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites"
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_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)
justificatifs: Justificatif = Justificatif.query.filter_by(etudid=etudid)
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, Justificatif, date_debut, date_fin
)
calculator: CountCalculator = CountCalculator()
calculator.compute_assiduites(assiduites)
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.compute_assiduites(abs_just)
nb_abs_just: dict = calculator.to_dict()[metrique]
r = (nb_abs, nb_abs_just)
if moduleimpl_id is None:
ans = sco_cache.AbsSemEtudCache.set(key, r)
if not ans:
log("warning: get_assiduites_count failed to cache")
@ -544,7 +594,7 @@ def invalidate_assiduites_etud_date(etudid, date: datetime):
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"""
date_debut = (
obj["date_debut"]

View File

@ -95,7 +95,7 @@ def get_formsemestre_bulletin_etud_json(
return formsemestre_bulletinetud(
etud,
formsemestre_id=formsemestre.id,
format="json",
fmt="json",
version=version,
xml_with_decisions=True,
force_publishing=force_publishing,
@ -201,7 +201,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
infos, dpv = etud_descr_situation_semestre(
etudid,
formsemestre,
format="html",
fmt="html",
show_date_inscr=prefs["bul_show_date_inscr"],
show_decisions=prefs["bul_show_decision"],
show_uevalid=prefs["bul_show_uevalid"],
@ -582,7 +582,7 @@ def _ue_mod_bulletin(
"notes.evaluation_listenotes",
scodoc_dept=g.scodoc_dept,
evaluation_id=e.id,
format="html",
fmt="html",
tf_submitted=1,
)
e_dict[
@ -679,14 +679,14 @@ def etud_descr_situation_semestre(
etudid,
formsemestre: FormSemestre,
ne="",
format="html", # currently unused
fmt="html", # currently unused
show_decisions=True,
show_uevalid=True,
show_date_inscr=True,
show_mention=False,
):
"""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.
Par ex. "Inscrit le 31/12/1999. Décision jury: Validé. ..."
@ -889,7 +889,7 @@ def _format_situation_fields(
def formsemestre_bulletinetud(
etud: Identite = None,
formsemestre_id=None,
format=None,
fmt=None,
version="long",
xml_with_decisions=False,
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é.
"""
format = format or "html"
fmt = fmt or "html"
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
if not formsemestre:
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
@ -918,21 +918,21 @@ def formsemestre_bulletinetud(
bulletin = do_formsemestre_bulletinetud(
formsemestre,
etud,
format=format,
fmt=fmt,
version=version,
xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing,
prefer_mail_perso=prefer_mail_perso,
)[0]
if format not in {"html", "pdfmail"}:
if fmt not in {"html", "pdfmail"}:
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)
elif format == "pdfmail":
elif fmt == "pdfmail":
return ""
H = [
_formsemestre_bulletinetud_header_html(etud, formsemestre, format, version),
_formsemestre_bulletinetud_header_html(etud, formsemestre, fmt, version),
bulletin,
render_template(
"bul_foot.j2",
@ -963,7 +963,7 @@ def do_formsemestre_bulletinetud(
formsemestre: FormSemestre,
etud: Identite,
version="long", # short, long, selectedevals
format=None,
fmt=None,
xml_with_decisions: bool = False,
force_publishing: 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)
et filigranne est un message à placer en "filigranne" (eg "Provisoire").
"""
format = format or "html"
if format == "xml":
fmt = fmt or "html"
if fmt == "xml":
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
formsemestre.id,
etud.id,
@ -997,7 +997,7 @@ def do_formsemestre_bulletinetud(
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(
formsemestre.id,
etud.id,
@ -1015,23 +1015,23 @@ def do_formsemestre_bulletinetud(
else:
bul_dict = formsemestre_bulletinetud_dict(formsemestre.id, etud.id)
if format == "html":
if fmt == "html":
htm, _ = sco_bulletins_generator.make_formsemestre_bulletin_etud(
bul_dict, etud=etud, formsemestre=formsemestre, version=version, fmt="html"
)
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_dict,
etud=etud,
formsemestre=formsemestre,
version=version,
fmt="pdf",
stand_alone=(format != "pdfpart"),
stand_alone=(fmt != "pdfpart"),
with_img_signatures_pdf=with_img_signatures_pdf,
)
if format == "pdf":
if fmt == "pdf":
return (
scu.sendPDFFile(bul, filename),
bul_dict["filigranne"],
@ -1039,7 +1039,7 @@ def do_formsemestre_bulletinetud(
else:
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
# check permission
if not can_send_bulletin_by_mail(formsemestre.id):
@ -1067,7 +1067,7 @@ def do_formsemestre_bulletinetud(
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):
@ -1097,10 +1097,12 @@ def mail_bulletin(formsemestre_id, infos, pdfdata, filename, recipient_addr):
hea = ""
if sco_preferences.get_preference("bul_mail_list_abs"):
hea += "\n\n" + "(LISTE D'ABSENCES NON DISPONIBLE)" # XXX TODO-ASSIDUITE
# sco_abs_views.ListeAbsEtud(
# etud["etudid"], with_evals=False, format="text"
# )
from app.views.assiduites import generate_bul_list
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"]}"""
recipients = [recipient_addr]
@ -1154,7 +1156,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "pdf",
"fmt": "pdf",
},
},
{
@ -1164,7 +1166,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "pdfmail",
"fmt": "pdfmail",
},
# possible slt si on a un mail...
"enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id),
@ -1176,7 +1178,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "pdfmail",
"fmt": "pdfmail",
"prefer_mail_perso": 1,
},
# possible slt si on a un mail...
@ -1189,7 +1191,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "json",
"fmt": "json",
},
},
{
@ -1199,7 +1201,7 @@ def make_menu_autres_operations(
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "xml",
"fmt": "xml",
},
},
{
@ -1267,7 +1269,7 @@ def make_menu_autres_operations(
def _formsemestre_bulletinetud_header_html(
etud,
formsemestre: FormSemestre,
format=None,
fmt=None,
version=None,
):
H = [
@ -1283,7 +1285,7 @@ def _formsemestre_bulletinetud_header_html(
render_template(
"bul_head.j2",
etud=etud,
format=format,
fmt=fmt,
formsemestre=formsemestre,
menu_autres_operations=make_menu_autres_operations(
etud=etud,

View File

@ -35,7 +35,7 @@ class BulletinGenerator:
.bul_part_below(fmt)
.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_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_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.sco_exceptions import NoteProcessError
from app import log
@ -139,18 +139,18 @@ class BulletinGenerator:
sem = sco_formsemestre.get_formsemestre(self.bul_dict["formsemestre_id"])
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"""
if not format in self.supported_formats:
raise ValueError(f"unsupported bulletin format ({format})")
if not fmt in self.supported_formats:
raise ValueError(f"unsupported bulletin format ({fmt})")
try:
PDFLOCK.acquire() # this lock is necessary since reportlab is not re-entrant
if format == "html":
if fmt == "html":
return self.generate_html()
elif format == "pdf":
elif fmt == "pdf":
return self.generate_pdf(stand_alone=stand_alone)
else:
raise ValueError(f"invalid bulletin format ({format})")
raise ValueError(f"invalid bulletin format ({fmt})")
finally:
PDFLOCK.release()
@ -197,6 +197,10 @@ class BulletinGenerator:
else:
# Insere notre marqueur qui permet de générer les bookmarks et filigrannes:
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())
if not stand_alone:
@ -288,8 +292,10 @@ def make_formsemestre_bulletin_etud(
):
if bul_dict.get("type") == "BUT" and fmt.startswith("pdf"):
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)
if gen_class is not None:
break
if gen_class is None:
raise ValueError(f"Type de bulletin PDF invalide (paramètre: {bul_class_name})")
@ -324,7 +330,7 @@ def make_formsemestre_bulletin_etud(
version=version,
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:
PDFLOCK.release()

View File

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

View File

@ -252,7 +252,7 @@ class BulletinGeneratorLegacy(sco_bulletins_generator.BulletinGenerator):
elif fmt == "html":
return self.bul_part_below_html()
else:
raise ValueError("invalid bulletin format (%s)" % fmt)
raise ValueError(f"invalid bulletin format ({fmt})")
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
):
"""Process a field given in preferences, returns
- if format = 'pdf': a list of Platypus objects
- if format = 'html' : a string
- if fmt = 'pdf': a list of Platypus objects
- if fmt = 'html' : a string
Substitutes all %()s markup
Remove potentialy harmful <img> tags
Replaces <logo name="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:
# 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(
formsemestre,
etud,
format="pdfpart",
fmt="pdfpart",
version=version,
)
fragments += frag
@ -270,7 +270,7 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre,
etud,
format="pdfpart",
fmt="pdfpart",
version=version,
)
fragments += frag

View File

@ -116,7 +116,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
html_with_td_classes=True,
)
return T.gen(format=fmt)
return T.gen(fmt=fmt)
def bul_part_below(self, fmt="html"):
"""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(
etudid,
formsemestre,
format="xml",
fmt="xml",
show_uevalid=sco_preferences.get_preference(
"bul_show_uevalid", formsemestre_id
),

View File

@ -152,7 +152,7 @@ def formsemestre_estim_cost(
n_group_tp=1,
coef_tp=1,
coef_cours=1.5,
format="html",
fmt="html",
):
"""Page (formulaire) estimation coûts"""
@ -192,4 +192,4 @@ def formsemestre_estim_cost(
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
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
à 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"
)
if format == "xls":
if fmt == "xls":
keep_numeric = True # pas de conversion des notes en strings
else:
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>""",
init_qtip=True,
javascripts=["js/etud_info.js"],
format=format,
fmt=fmt,
with_html_headers=True,
)
@ -276,7 +276,7 @@ def itemsuivi_suppress(itemsuivi_id):
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"""
if not sco_permissions_check.can_edit_suivi():
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)
log("created itemsuivi %s for %s" % (itemsuivi_id, etudid))
item = itemsuivi_get(cnx, itemsuivi_id)
if format == "json":
if fmt == "json":
return scu.sendJSON(item)
return item
@ -320,13 +320,13 @@ def itemsuivi_set_situation(object, value):
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"""
cnx = ndb.GetDBConnexion()
items = _itemsuivi_list(cnx, {"etudid": etudid})
for it in items:
it["tags"] = ", ".join(itemsuivi_tag_list(it["itemsuivi_id"]))
if format == "json":
if fmt == "json":
return scu.sendJSON(items)
return items

View File

@ -979,18 +979,18 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</li>
<li><a class="stdlink" href="{
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
<a class="stdlink" href="{
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>
(permet de l'enregistrer pour l'échanger avec un autre site)
</li>
<li><a class="stdlink" href="{
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>
</li>

View File

@ -85,7 +85,7 @@ class ApoCSVArchiver(sco_archives.BaseArchiver):
sco_archives.BaseArchiver.__init__(self, archive_type="apo_csv")
ApoCSVArchive = ApoCSVArchiver()
APO_CSV_ARCHIVER = ApoCSVArchiver()
# 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}"
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)
ApoCSVArchive.store(archive_id, filename, csv_data_bytes)
APO_CSV_ARCHIVER.store(archive_id, filename, csv_data_bytes)
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
[ { } ]
"""
oids = ApoCSVArchive.list_oids() # [ '2016-1', ... ]
oids = APO_CSV_ARCHIVER.list_oids() # [ '2016-1', ... ]
# filter
if 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
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:
description = ApoCSVArchive.get_archive_description(archive_id)
description = APO_CSV_ARCHIVER.get_archive_description(archive_id)
fs = tuple(description.split(";"))
if len(fs) == 3:
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),
"sem_id": int(arch_sem_id),
"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"])
@ -185,7 +185,7 @@ def apo_csv_list_stored_etapes(annee_scolaire, sem_id=None, etapes=None):
def apo_csv_delete(archive_id):
"""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=""):
@ -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)
)
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
# son encodage est donc 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
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
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,
etuds=list(etuds.values()),
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"""
if not isinstance(nip_list, str):
nip_list = str(nip_list)
@ -553,12 +553,12 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", format="html"):
title=title,
etuds=etuds,
keys=("code_nip", "nom", "prenom"),
format=format,
fmt=fmt,
)
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:
"Affiche les étudiants indiqués"
# Tri les étudiants par nom:
@ -581,8 +581,8 @@ def _view_etuds_page(
filename="students_apo",
preferences=sco_preferences.SemPreferences(),
)
if format != "html":
return tab.make_page(format=format)
if fmt != "html":
return tab.make_page(fmt=fmt)
return f"""
{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)
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
Si format="raw", renvoie le fichier maquette tel quel
Si fmt="raw", renvoie le fichier maquette tel quel
"""
if not 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"]
sem_id = semset["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)
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(),
)
if format != "html":
return tab.make_page(format=format)
if fmt != "html":
return tab.make_page(fmt=fmt)
H += [
f"""
@ -807,7 +807,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
<p><a class="stdlink" href="{
url_for("notes.view_apo_csv",
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>
</p>
<div>

View File

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

View File

@ -30,16 +30,17 @@
from flask import url_for, g
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
from app.scodoc import html_sco_header
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc.sco_exceptions import ScoValueError
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):
"""Vérifie les absences au moment de cette évaluation.
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
Ramene 5 listes d'etudid
"""
raise ScoValueError("Fonction non disponible, patience !") # XXX TODO-ASSIDUITE
if not evaluation.date_debut:
if not evaluation.date_debut or not evaluation.date_fin:
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:
absences = sco_abs.list_abs_jour(evaluation.date_debut, am=am, pm=pm)
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
deb, fin = scu.localize_datetime(evaluation.date_debut), scu.localize_datetime(
evaluation.date_fin
)
abs_nj_etudids = set(
[x["etudid"] for x in abs_non_just]
) # ensemble des etudiants absents non justifies
justifs = sco_abs.list_abs_jour(
evaluation.date_debut.date(), am=am, pm=pm, is_abs=None, is_just=True
assiduites: Query = Assiduite.query.filter(
Assiduite.etudid.in_(etudids),
Assiduite.etat == scu.EtatAssiduite.ABSENT,
fin >= Assiduite.date_debut,
deb <= Assiduite.date_fin,
)
just_etudids = set(
[x["etudid"] for x in justifs]
) # ensemble des etudiants avec justif
abs_etudids = set(assi.etudid for assi in assiduites)
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:
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
ExcNonJust = [] # note EXC mais absent non justifie
AbsButExc = [] # note ABS mais justifié
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
evaluation.id, getallstudents=True
):
for etudid in etudids:
if etudid in notes_db:
val = notes_db[etudid]["value"]
if (
@ -108,9 +109,10 @@ def evaluation_check_absences(evaluation: Evaluation):
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"""
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
am, pm = evaluation.is_matin(), evaluation.is_apresmidi()
# 1 si matin, 0 si apres midi, 2 si toute la journee:
match am, pm:
@ -169,14 +171,10 @@ def evaluation_check_absences_html(evaluation_id, with_header=True, show_ok=True
)
if linkabs:
url = url_for(
"absences.doSignaleAbsence", # XXX TODO-ASSIDUITE
scodoc_dept=g.scodoc_dept,
"assiduites.signal_evaluation_abs",
etudid=etudid,
# par defaut signale le jour du début de l'éval
datedebut=evaluation.date_debut.strftime("%d/%m/%Y"),
datefin=evaluation.date_debut.strftime("%d/%m/%Y"),
demijournee=demijournee,
moduleimpl_id=evaluation.moduleimpl_id,
evaluation_id=evaluation.id,
scodoc_dept=g.scodoc_dept,
)
H.append(
f"""<a class="stdlink" href="{url}">signaler cette absence</a>"""
@ -249,7 +247,7 @@ def formsemestre_check_absences_html(formsemestre_id):
):
H.append(
evaluation_check_absences_html(
evaluation.id, # XXX TODO-ASSIDUITE remplacer par evaluation ...
evaluation,
with_header=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 import html_sco_header
from app.scodoc import sco_evaluations
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_moduleimpl
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())
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
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()}""",
filename=scu.make_filename("evaluations_delais_" + formsemestre.titre_annee()),
)
return tab.make_page(format=format)
return tab.make_page(fmt=fmt)
# -------------- VIEWS

View File

@ -220,7 +220,7 @@ def get_set_formsemestre_id_dates(start_date, end_date) -> set:
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
Les dates sont en dd/mm/yyyy (datepicker javascript)
@ -248,8 +248,8 @@ def scodoc_table_results(
end_date,
"&types_parcours=".join([str(x) for x in types_parcours]),
)
if format != "html":
return tab.make_page(format=format, with_html_headers=False)
if fmt != "html":
return tab.make_page(fmt=fmt, with_html_headers=False)
tab_html = tab.html()
nb_rows = tab.get_nb_rows()
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
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)
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
def formation_table_recap(formation_id, format="html") -> Response:
def formation_table_recap(formation_id, fmt="html") -> Response:
"""Table recapitulant formation."""
T = []
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(),
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):
@ -179,7 +179,7 @@ def export_recap_formations_annee_scolaire(annee_scolaire):
formation_ids = {formsemestre.formation.id for formsemestre in formsemestres}
for formation_id in formation_ids:
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 = (
scu.sanitize_filename(formation.get_titre_version()) + scu.XLSX_SUFFIX
)

View File

@ -212,7 +212,7 @@ def formation_export(
export_tags=True,
export_external_ues=False,
export_codes_apo=True,
format=None,
fmt=None,
) -> flask.Response:
"""Get a formation, with UE, matieres, modules
in desired format
@ -224,13 +224,13 @@ def formation_export(
export_tags=export_tags,
export_external_ues=export_external_ues,
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}"
return scu.sendResult(
f_dict,
name="formation",
format=format,
fmt=fmt,
force_outer_xml_tag=False,
attached=True,
filename=filename,
@ -283,7 +283,7 @@ def _formation_retreive_apc_niveau(
def formation_import_xml(doc: str, import_tags=True, use_local_refcomp=False):
"""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
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"
formation = Formation.query.get_or_404(formation_id)
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)
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
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"""
if etape_apo:
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>""",
)
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):

View File

@ -171,6 +171,13 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"enabled": True,
"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})",
"endpoint": "notes.ue_table",
@ -218,14 +225,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"enabled": True,
"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",
"endpoint": "notes.formsemestre_enseignants_list",
@ -326,7 +325,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"title": "Exporter table des étudiants",
"endpoint": "scolar.groups_view",
"args": {
"format": "allxls",
"fmt": "allxls",
"group_ids": sco_groups.get_default_group(
formsemestre_id, fix_if_missing=True
),
@ -448,7 +447,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"title": "Documents archivés",
"endpoint": "notes.formsemestre_list_archives",
"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,11 +502,8 @@ def retreive_formsemestre_from_request() -> int:
group = sco_groups.get_group(args["group_id"])
formsemestre_id = group["formsemestre_id"]
elif group_ids:
if group_ids:
if isinstance(group_ids, str):
group_id = group_ids
else:
# prend le semestre du 1er groupe de la liste:
group_ids = group_ids.split(",")
group_id = group_ids[0]
group = sco_groups.get_group(group_id)
formsemestre_id = group["formsemestre_id"]
@ -788,7 +784,7 @@ def formsemestre_description_table(
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
Liste des modules et de leurs coefficients
@ -808,112 +804,124 @@ def formsemestre_description(
>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
def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
# construit l'URL "destination"
# (a laquelle on revient apres saisie absences)
destination = url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
)
#
def _make_listes_sem(formsemestre: FormSemestre) -> str:
"""La section avec les groupes et l'assiduité"""
H = []
# pas de menu absences si pas autorise:
if with_absences and not current_user.has_permission(Permission.ScoAbsChange):
with_absences = False
can_edit_abs = current_user.has_permission(Permission.ScoAbsChange)
#
H.append(
f"""<h3>Listes de {formsemestre.titre}
<span class="infostitresem">({formsemestre.mois_debut()} - {formsemestre.mois_fin()})</span></h3>"""
f"""<h3>Groupes et absences de {formsemestre.titre}
<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)
for partition in sco_groups.get_partitions_list(formsemestre.id):
if not partition["partition_name"]:
H.append("<h4>Tous les étudiants</h4>")
else:
H.append("<h4>Groupes de %(partition_name)s</h4>" % partition)
partition_is_empty = True
groups = sco_groups.get_partition_groups(partition)
for partition in formsemestre.get_partitions_list():
groups = partition.groups.all()
effectifs = {g.id: g.get_nb_inscrits() for g in groups}
partition_is_empty = sum(effectifs.values()) == 0
H.append(
f"""
<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:
H.append("<table>")
for group in groups:
n_members = len(sco_groups.get_group_members(group["group_id"]))
n_members = effectifs[group.id]
if n_members == 0:
continue # skip empty groups
partition_is_empty = False
group["url_etat"] = url_for(
"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"
group_label = f"{group.group_name}" if group.group_name else "liste"
H.append(
f"""
<tr class="listegroupelink">
<td>
<div class="sem-groups-list">
<div>
<a href="{
url_for("scolar.groups_view",
group_ids=group["group_id"],
group_ids=group.id,
scodoc_dept=g.scodoc_dept,
)
}">{group["label"]}</a>
</td><td>
</td>
<td>({n_members} étudiants)</td>
}">{group_label}
- {n_members} étudiants</a>
</div>
</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("</tr>")
H.append("</table>")
H.append("</div>") # /sem-groups-assi
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():
H.append(
f""" (<a href="{url_for("scolar.partition_editor",
@ -922,7 +930,9 @@ def _make_listes_sem(formsemestre: FormSemestre, with_absences=True):
edit_partition=1)
}" class="stdlink">créer</a>)"""
)
H.append("</p>")
H.append("</div>")
H.append("</div>") # /sem-groups-partition
if formsemestre.can_change_groups():
H.append(
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."""
)
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:
warnings.append("Calcul des moyennes bloqué !")
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
H += [
'<div id="groupes">',
'<div class="formsemestre-groupes">',
_make_listes_sem(formsemestre),
"</div>",
]
@ -1230,7 +1240,11 @@ def formsemestre_tableau_modules(
mod_descr = "Module " + (mod.titre or "")
if mod.is_apc():
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:
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]:
"""Liste des partitions pour ce semestre (list of dicts),
triées par numéro, avec la partition par défaut en fin de liste.
OBSOLETE: utiliser FormSemestre.get_partitions_list
"""
partitions = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.*
@ -515,7 +516,7 @@ def get_etud_groups_in_partition(partition_id):
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
Supported formats: xml, json
"""
@ -523,7 +524,7 @@ def formsemestre_partition_list(formsemestre_id, format="xml"):
# Ajoute les groupes
for p in partitions:
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
@ -1377,20 +1378,18 @@ def group_rename(group_id):
return group_set_name(group, tf[2]["group_name"])
def groups_auto_repartition(partition_id=None):
"""Reparti les etudiants dans des groupes dans une partition, en respectant le niveau
def groups_auto_repartition(partition: Partition):
"""Réparti les etudiants dans des groupes dans une partition, en respectant le niveau
et la mixité.
"""
partition: Partition = Partition.query.get_or_404(partition_id)
if not partition.groups_editable:
raise AccessDenied("Partition non éditable")
formsemestre_id = partition.formsemestre_id
formsemestre = partition.formsemestre
# renvoie sur page édition partitions et groupes
dest_url = url_for(
"scolar.partition_editor",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
formsemestre_id=formsemestre.id,
)
if not formsemestre.can_change_groups():
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 = [
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>
<p>Semestre {formsemestre.titre_annee()}</p>
<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 = {}
for civilite in civilites:
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()
if x["civilite"] == civilite
]

View File

@ -60,7 +60,7 @@ def groups_list_annotation(group_ids: list[int]) -> list[dict]:
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"""
groups_infos = sco_groups_view.DisplayedGroupsInfos(
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)
for annotation in annotations:
annotation["date_str"] = annotation["date"].strftime("%d/%m/%Y à %Hh%M")
if format == "xls":
if fmt == "xls":
columns_ids = ("etudid", "nom", "prenom", "date", "comment")
else:
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",
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:
def groups_view(
group_ids=(),
format="html",
fmt="html",
# Options pour listes:
with_codes=0,
etat=None,
@ -82,7 +82,7 @@ def groups_view(
):
"""Affichage des étudiants des groupes indiqués
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:
groups_infos = DisplayedGroupsInfos(
@ -92,10 +92,10 @@ def groups_view(
select_all_when_unspecified=True,
)
# Formats spéciaux: download direct
if format != "html":
if fmt != "html":
return groups_table(
groups_infos=groups_infos,
format=format,
fmt=fmt,
with_codes=with_codes,
etat=etat,
with_paiement=with_paiement,
@ -135,7 +135,7 @@ def groups_view(
""",
groups_table(
groups_infos=groups_infos,
format=format,
fmt=fmt,
with_codes=with_codes,
etat=etat,
with_paiement=with_paiement,
@ -324,7 +324,9 @@ class DisplayedGroupsInfos:
if not formsemestre_id:
raise Exception("missing parameter formsemestre_id or group_ids")
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:
# selectionne le premier groupe trouvé, s'il y en a un
partition = sco_groups.get_partitions_list(
@ -437,14 +439,14 @@ def groups_table(
groups_infos: DisplayedGroupsInfos = None,
with_codes=0,
etat=None,
format="html",
fmt="html",
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_annotations=0,
with_bourse=0,
):
"""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
"""
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
}
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("email")
columns_ids.append("emailperso")
if format == "moodlecsv":
if fmt == "moodlecsv":
columns_ids = ["email", "semestre_groupe"]
if with_codes:
@ -579,7 +581,7 @@ def groups_table(
else:
s = ""
if format == "moodlecsv":
if fmt == "moodlecsv":
# de la forme S1-[FI][FA]-groupe.csv
if not moodle_groupenames:
moodle_groupenames = {"tous"}
@ -612,7 +614,7 @@ def groups_table(
preferences=prefs,
)
#
if format == "html":
if fmt == "html":
amail_inst = [
x["email"] for x in groups_infos.members if x["email"] and x["etat"] != "D"
]
@ -683,11 +685,11 @@ def groups_table(
[
tab.html(),
"<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,),
'<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,),
'<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,),
"""<li>
<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>"
elif (
format == "pdf"
or format == "xml"
or format == "json"
or format == "xls"
or format == "moodlecsv"
fmt == "pdf"
or fmt == "xml"
or fmt == "json"
or fmt == "xls"
or fmt == "moodlecsv"
):
if format == "moodlecsv":
format = "csv"
return tab.make_page(format=format)
if fmt == "moodlecsv":
fmt = "csv"
return tab.make_page(fmt=fmt)
elif format == "xlsappel":
elif fmt == "xlsappel":
xls = sco_excel.excel_feuille_listeappel(
groups_infos.formsemestre,
groups_infos.groups_titles,
@ -745,7 +747,7 @@ def groups_table(
)
filename = "liste_%s" % groups_infos.groups_filename
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
if not groups_infos.members:
return ""
@ -825,7 +827,7 @@ def tab_absences_html(groups_infos, etat=None):
H.extend(
[
"<h3>Assiduités</h3>",
"<h3>Assiduité</h3>",
'<ul class="ul_abs">',
"<li>",
form_choix_saisie_semaine(groups_infos), # Ajout Le Havre
@ -833,21 +835,25 @@ def tab_absences_html(groups_infos, etat=None):
"<li>",
form_choix_jour_saisie_hebdo(groups_infos),
"</li>",
f"""<li><a 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())
}">État des assiduités du groupe</a></li>""",
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()
)
}">État de l'assiduité du groupe</a></li>""",
"</ul>",
"<h3>Feuilles</h3>",
'<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),
"""<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,
"""<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,
"""<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,
"""<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,
"""<li><a class="stdlink" href="groups_export_annotations?%s">Liste des annotations</a></li>"""
% groups_infos.groups_query_args,
@ -890,76 +896,38 @@ def form_choix_jour_saisie_hebdo(groups_infos, moduleimpl_id=None):
authuser = current_user
if not authuser.has_permission(Permission.ScoAbsChange):
return ""
sem = groups_infos.formsemestre
first_monday = sco_cal.ddmmyyyy(sem["date_debut"]).prev_monday()
today_idx = datetime.date.today().weekday()
FA = [] # formulaire avec menu saisi absences
FA.append(
# TODO-ASSIDUITE et utiliser url_for... (was Absences/SignaleAbsenceGrSemestre)
'<form id="form_choix_jour_saisie_hebdo" action="XXX" method="get">'
return f"""
<button onclick="window.location='{
url_for(
"assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept,
group_ids=",".join(map(str,groups_infos.group_ids)),
jour=datetime.date.today().isoformat(),
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())
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)
}';">Saisie du jour ({datetime.date.today().strftime('%d/%m/%Y')})</button>
"""
# Ajout Le Havre
# Formulaire saisie absences semaine
# Saisie de l'assiduité par semaine
def form_choix_saisie_semaine(groups_infos):
authuser = current_user
if not authuser.has_permission(Permission.ScoAbsChange):
return ""
# construit l'URL "destination"
# (a laquelle on revient apres saisie absences)
query_args = parse_qs(request.query_string)
moduleimpl_id = query_args.get("moduleimpl_id", [""])[0]
if "head_message" in query_args:
del query_args["head_message"]
destination = "%s?%s" % (
request.base_url,
urllib.parse.urlencode(query_args, True),
)
destination = destination.replace(
"%", "%%"
) # car ici utilisee dans un format string !
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)
moduleimpl_id = query_args.get("moduleimpl_id", [None])[0]
semaine = datetime.date.today().isocalendar().week
return f"""
<button onclick="window.location='{url_for(
"assiduites.signal_assiduites_diff",
group_ids=",".join(map(str,groups_infos.group_ids)),
semaine=semaine,
scodoc_dept=g.scodoc_dept,
formsemestre_id=groups_infos.formsemestre_id,
moduleimpl_id=moduleimpl_id
)}';">Saisie à la semaine</button>
"""
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"],
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(
evaluation_id=None, moduleimpl_id=None, format="html"
evaluation_id=None, moduleimpl_id=None, fmt="html"
) -> tuple[str, str]:
"""
Affichage des notes d'une évaluation (si evaluation_id)
@ -220,7 +220,7 @@ def do_evaluation_listenotes(
_make_table_notes(
tf[1],
evals,
fmt=format,
fmt=fmt,
note_sur_20=note_sur_20,
anonymous_listing=anonymous_listing,
group_ids=group_ids,
@ -424,7 +424,7 @@ def _make_table_notes(
key_mgr,
note_sur_20,
keep_numeric,
format=fmt,
fmt=fmt,
)
columns_ids.append(e["evaluation_id"])
#
@ -596,7 +596,7 @@ def _make_table_notes(
)
if fmt == "bordereau":
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":
return t
@ -622,7 +622,7 @@ def _make_table_notes(
histo = histogram_notes(notes)
# 2 colonnes: histo, comments
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>"
+ histo
+ "</div></td>\n",
@ -670,7 +670,7 @@ def _add_eval_columns(
K,
note_sur_20,
keep_numeric,
format="html",
fmt="html",
):
"""Add eval e"""
nb_notes = 0
@ -774,7 +774,7 @@ def _add_eval_columns(
row_coefs[evaluation_id] = "coef. %s" % e["coefficient"]
if is_apc:
if format == "html":
if fmt == "html":
row_poids[evaluation_id] = _mini_table_eval_ue_poids(
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
de _tous_ les départements.
"""
@ -71,7 +71,7 @@ def scodoc_table_etuds_lycees(format="html"):
semdepts = sco_formsemestre.scodoc_get_all_unlocked_sems()
etuds = []
try:
for (sem, dept) in semdepts:
for sem, dept in semdepts:
app.set_sco_dept(dept.acronym)
etuds += sco_report.tsp_etud_list(sem["formsemestre_id"])[0]
finally:
@ -85,8 +85,8 @@ def scodoc_table_etuds_lycees(format="html"):
no_links=True,
)
tab.base_url = request.base_url
t = tab.make_page(format=format, with_html_headers=False)
if format != "html":
t = tab.make_page(fmt=fmt, with_html_headers=False)
if fmt != "html":
return t
H = [
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(
formsemestre_id,
format="html",
fmt="html",
only_primo=False,
no_grouping=False,
):
@ -191,14 +191,10 @@ def formsemestre_etuds_lycees(
tab.base_url += "&only_primo=1"
if no_grouping:
tab.base_url += "&no_grouping=1"
t = tab.make_page(format=format, with_html_headers=False)
if format != "html":
t = tab.make_page(fmt=fmt, with_html_headers=False)
if fmt != "html":
return t
F = [
sco_report.tsp_form_primo_group(
only_primo, no_grouping, formsemestre_id, format
)
]
F = [sco_report.tsp_form_primo_group(only_primo, no_grouping, formsemestre_id, fmt)]
H = [
html_sco_header.sco_header(
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)
if has_expression:
H.append(
f"""<tr>
"""<tr>
<td class="fichetitre2" colspan="4">Règle de calcul:
<span class="formula" title="mode de calcul de la moyenne du module"
>moyenne=<tt>{modimpl.computation_expr}</tt>
</span>"""
<span class="warning">inutilisée dans cette version de ScoDoc</span>
</td>
</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:
H.append(
'<tr><td colspan="4">'
# <em title="mode de calcul de la moyenne du module">règle de calcul standard</em>'
)
H.append('<tr><td colspan="4">')
H.append("</td></tr>")
H.append(
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}
&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>
"""
)

View File

@ -441,7 +441,7 @@ def ficheEtud(etudid=None):
# Fichiers archivés:
info["fichiers_archive_htm"] = (
'<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:

View File

@ -27,7 +27,7 @@ _SCO_PERMISSIONS = (
(1 << 13, "ScoAbsChange", "Saisir des absences"),
(1 << 14, "ScoAbsAddBillet", "Saisir des billets d'absences"),
# 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,
"APIEditGroups",

View File

@ -383,7 +383,7 @@ class PlacementRunner:
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):
cells = [

View File

@ -178,7 +178,7 @@ def _getEtudInfoGroupes(group_ids, etat=None):
return etuds
def formsemestre_poursuite_report(formsemestre_id, format="html"):
def formsemestre_poursuite_report(formsemestre_id, fmt="html"):
"""Table avec informations "poursuite" """
sem = sco_formsemestre.get_formsemestre(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>""",
init_qtip=True,
javascripts=["js/etud_info.js"],
format=format,
fmt=fmt,
with_html_headers=True,
)

View File

@ -609,7 +609,18 @@ class BasePreferences:
"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",
{
@ -1750,6 +1761,17 @@ class BasePreferences:
"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",
{

View File

@ -206,14 +206,14 @@ def pvjury_table(
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
En classique: table spécifique avec les deux semestres pour le DUT
En APC/BUT: renvoie vers table recap, en mode jury.
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
is_apc = formsemestre.formation.is_apc()
if format == "html" and is_apc:
if fmt == "html" and is_apc:
return redirect(
url_for(
"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)
if not dpv:
if format == "html":
if fmt == "html":
return (
html_sco_header.sco_header()
+ "<h2>Aucune information disponible !</h2>"
@ -239,7 +239,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
formsemestre_id = sem["formsemestre_id"]
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
tab = GenTable(
@ -255,9 +255,9 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
html_sortable=True,
preferences=sco_preferences.SemPreferences(formsemestre_id),
)
if format != "html":
if fmt != "html":
return tab.make_page(
format=format,
fmt=fmt,
with_html_headers=False,
publish=publish,
)

View File

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

View File

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

View File

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

View File

@ -66,7 +66,7 @@ def trombino(
group_ids=(), # liste des groupes à afficher
formsemestre_id=None, # utilisé si pas de groupes selectionné
etat=None,
format="html",
fmt="html",
dialog_confirmed=False,
):
"""Trombinoscope"""
@ -78,18 +78,18 @@ def trombino(
)
#
if format != "html" and not dialog_confirmed:
ok, dialog = check_local_photos_availability(groups_infos, fmt=format)
if fmt != "html" and not dialog_confirmed:
ok, dialog = check_local_photos_availability(groups_infos, fmt=fmt)
if not ok:
return dialog
if format == "zip":
if fmt == "zip":
return _trombino_zip(groups_infos)
elif format == "pdf":
elif fmt == "pdf":
return _trombino_pdf(groups_infos)
elif format == "pdflist":
elif fmt == "pdflist":
return _listeappel_photos_pdf(groups_infos)
elif format == "doc":
elif fmt == "doc":
return sco_trombino_doc.trombino_doc(groups_infos)
else:
raise Exception("invalid format")
@ -110,7 +110,7 @@ def trombino_html(groups_infos):
{
"title": "Obtenir archive Zip des photos",
"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",
@ -176,10 +176,10 @@ def trombino_html(groups_infos):
H.append(
f"""<div style="margin-bottom:15px;">
<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;
<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>"""
)
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"]):
nb_missing += 1
if nb_missing > 0:
parameters = {"group_ids": groups_infos.group_ids, "format": fmt}
parameters = {"group_ids": groups_infos.group_ids, "fmt": fmt}
return (
False,
scu.confirm_dialog(
f"""<p>Attention: {nb_missing} photos ne sont pas disponibles
et ne peuvent pas être exportées.</p>
<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>""",
dest_url="trombino",
OK="Exporter seulement les photos existantes",

View File

@ -173,7 +173,7 @@ def evaluation_list_operations(evaluation_id):
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
les évaluations du semestre.
"""
@ -194,7 +194,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
{"formsemestre_id": formsemestre_id},
)
# Formate les notes
keep_numeric = format in scu.FORMATS_NUMERIQUES
keep_numeric = fmt in scu.FORMATS_NUMERIQUES
for row in rows:
row["value"] = scu.fmt_note(row["value"], keep_numeric=keep_numeric)
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),
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=""):

View File

@ -240,7 +240,7 @@ def list_users(
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:

View File

@ -237,7 +237,7 @@ def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
new_date: datetime.datetime = date
if new_date.tzinfo is None:
try:
new_date = timezone("Europe/Paris").localize(date)
new_date = TIME_ZONE.localize(date)
except OverflowError:
new_date = timezone("UTC").localize(date)
return new_date
@ -670,8 +670,8 @@ def AbsencesURL():
def AssiduitesURL():
"""URL of Assiduités"""
return url_for("assiduites.index_html", scodoc_dept=g.scodoc_dept)[
: -len("/index_html")
return url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)[
: -len("/BilanDept")
]
@ -879,10 +879,10 @@ DB_MIN_INT = -(1 << 31)
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"""
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)
return filename
@ -952,15 +952,15 @@ def sendXML(
def sendResult(
data,
name=None,
format=None,
fmt=None,
force_outer_xml_tag=True,
attached=False,
quote_xml=False,
filename=None,
):
if (format is None) or (format == "html"):
if (fmt is None) or (fmt == "html"):
return data
elif format == "xml": # name is outer tagname
elif fmt == "xml": # name is outer tagname
return sendXML(
data,
tagname=name,
@ -969,10 +969,10 @@ def sendResult(
quote=quote_xml,
filename=filename,
)
elif format == "json":
elif fmt == "json":
return sendJSON(data, attached=attached, filename=filename)
else:
raise ValueError("invalid format: %s" % format)
raise ValueError(f"invalid format: {fmt}")
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:
"""Simple JSON for errors.
If as-response, returns Flask's Response. Otherwise returns a dict.
"""
"""Simple JSON for errors."""
payload = {
"error": HTTP_STATUS_CODES.get(status_code, "Unknown error"),
"status": status_code,

View File

@ -136,6 +136,8 @@
flex-direction: column;
align-items: flex-start;
margin: 0 5%;
cursor: pointer;
}
.etud_row.def .nom::after,
@ -268,6 +270,7 @@
background-size: cover;
}
.rbtn.present::before {
background-image: url(../icons/present.svg);
}
@ -285,8 +288,8 @@
}
.rbtn:checked:before {
outline: 3px solid #7059FF;
border-radius: 5px;
outline: 5px solid #7059FF;
border-radius: 50%;
}
.rbtn:focus {
@ -541,6 +544,17 @@
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'] {
-webkit-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 {
width: 17cm;
/* width: 17cm; */
display: grid;
grid-template-columns: 6cm 11cm;
font-size: 11pt;
grid-template-columns: 6cm 11cm;
margin-left: 16px;
}
#infos_etudiant {

View File

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

View File

@ -985,17 +985,6 @@ span.linktitresem a:visited {
color: red;
}
.listegroupelink a:link {
color: blue;
}
.listegroupelink a:visited {
color: blue;
}
.listegroupelink a:hover {
color: red;
}
a.stdlink,
a.stdlink:visited {
@ -1792,10 +1781,6 @@ td.formsemestre_status_inscrits {
text-align: center;
}
div.formsemestre_status button {
margin-left: 12px;;
}
td.rcp_titre_sem a.jury_link {
margin-left: 8px;
color: red;
@ -1857,15 +1842,54 @@ ul.ue_inscr_list li.etud {
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;
margin-bottom: 0px;
margin-top: 5px;
}
#grouplists table {
/*border: 1px solid black;*/
border-spacing: 1px;
.sem-groups-partition-titre {
margin-left: 4px;
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 */
@ -3077,7 +3101,7 @@ div.bul_foot {
border-radius: 16px;
border: 1px solid #AAA;
padding: 16px 32px;
margin: auto;
margin-left: 16px;
}
div.bull_appreciations {
@ -3182,6 +3206,9 @@ table.abs_form_table tr:hover td {
border: 1px solid red;
}
.ul_abs button {
margin-bottom: 6px;
}
/* ----- Formulator ------- */
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
*/
function sync_get(path, success, errors) {
console.log("sync_get " + path);
$.ajax({
async: false,
type: "GET",
@ -177,6 +178,7 @@ function sync_get(path, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
function async_get(path, success, errors) {
console.log("async_get " + path);
$.ajax({
async: true,
type: "GET",
@ -193,6 +195,7 @@ function async_get(path, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
function sync_post(path, data, success, errors) {
console.log("sync_post " + path);
$.ajax({
async: false,
type: "POST",
@ -210,6 +213,7 @@ function sync_post(path, data, success, errors) {
* @param {CallableFunction} errors fonction à effectuer en cas d'échec
*/
function async_post(path, data, success, errors) {
console.log("sync_post " + path);
return $.ajax({
async: true,
type: "POST",
@ -577,7 +581,7 @@ function updateDate() {
return true;
} else {
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");
dateInput.value = dateInput.getAttribute("value");
@ -611,7 +615,9 @@ function setupDate(onchange = null) {
datestr.addEventListener("click", () => {
if (!input.disabled) {
try {
input.showPicker();
} catch {}
}
});
@ -809,13 +815,10 @@ function numberTimeToDate(nb) {
* - du semestre
* - de la date courant et du jour précédent.
* @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 formsemestre_id = has_formsemestre
? `formsemestre_id=${getFormSemestreId()}&`
: "";
const date_debut = deb ? deb : toIsoString(getPrevDate());
const date_fin = fin ? fin : toIsoString(getNextDate());
@ -826,7 +829,7 @@ function getAssiduitesFromEtuds(clear, has_formsemestre = true, deb, fin) {
const url_api =
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) => {
if (status === "success") {
const dataKeys = Object.keys(data);
@ -924,14 +927,11 @@ function deleteAssiduite(assiduite_id) {
function hasModuleImpl(assiduite) {
if (assiduite.moduleimpl_id != null) return true;
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object &&
"module" in assiduite.external_data
)
return true;
return false;
return (
assiduite.hasOwnProperty("external_data") &&
assiduite.external_data != null &&
assiduite.external_data.hasOwnProperty("module")
);
}
/**
@ -942,6 +942,15 @@ function hasModuleImpl(assiduite) {
* TODO : Rendre asynchrone
*/
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 = {
etat: etat,
external_data: assi ? assi.external_data : null,
@ -1057,16 +1066,13 @@ function getAssiduiteValue(field) {
* Mise à jour des assiduités d'un étudiant
* @param {String | Number} etudid identifiant de l'étudiant
*/
function actualizeEtudAssiduite(etudid, has_formsemestre = true) {
const formsemestre_id = has_formsemestre
? `formsemestre_id=${getFormSemestreId()}&`
: "";
function actualizeEtudAssiduite(etudid) {
const date_debut = toIsoString(getPrevDate());
const date_fin = toIsoString(getNextDate());
const url_api =
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) => {
if (status === "success") {
assiduites[etudid] = data;
@ -1074,8 +1080,22 @@ function actualizeEtudAssiduite(etudid, has_formsemestre = true) {
});
}
function getAllAssiduitesFromEtud(etudid, action) {
const url_api = getUrl() + `/api/assiduites/${etudid}`;
function getAllAssiduitesFromEtud(
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({
async: true,
@ -1136,9 +1156,7 @@ function assiduiteAction(element) {
done = editAssiduite(
assiduite_id,
etat,
assiduites[etudid].reduce((a) => {
if (a.assiduite_id == assiduite_id) return a;
})
assiduites[etudid].filter((a) => a.assiduite_id == assiduite_id)
);
}
break;
@ -1249,12 +1267,10 @@ function generateEtudRow(
<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>
<h5 class="prenom">${etud.prenom}</h5>
</div>
</a>
</div>
<div class="assiduites_bar">
@ -1331,7 +1347,7 @@ function insertEtudRow(etud, index, output = false) {
* @param {String | Number} etudid l'identifiant de l'étudiant
*/
function actualizeEtud(etudid) {
actualizeEtudAssiduite(etudid, !isSingleEtud());
actualizeEtudAssiduite(etudid);
//Actualize row
const etudHolder = document.querySelector(".etud_holder");
const ancient_row = document.getElementById(`etud_row_${etudid}`);
@ -1412,10 +1428,10 @@ function setModuleImplId(assiduite, module = null) {
const moduleimpl = module == null ? getModuleImplId() : module;
if (moduleimpl === "autre") {
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object
assiduite.hasOwnProperty("external_data") &&
assiduite.external_data != null
) {
if ("module" in assiduite.external_data) {
if (assiduite.external_data.hasOwnProperty("module")) {
assiduite.external_data.module = "Autre";
} else {
assiduite["external_data"] = { module: "Autre" };
@ -1427,10 +1443,10 @@ function setModuleImplId(assiduite, module = null) {
} else {
assiduite["moduleimpl_id"] = moduleimpl;
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object
assiduite.hasOwnProperty("external_data") &&
assiduite.external_data != null
) {
if ("module" in assiduite.external_data) {
if (assiduite.external_data.hasOwnProperty("module")) {
delete assiduite.external_data.module;
}
}
@ -1482,9 +1498,9 @@ function getCurrentAssiduiteModuleImplId() {
let mod = currentAssiduites[0].moduleimpl_id;
if (
mod == null &&
"external_data" in currentAssiduites[0] &&
currentAssiduites[0].external_data instanceof Object &&
"module" in currentAssiduites[0].external_data
currentAssiduites[0].hasOwnProperty("external_data") &&
currentAssiduites[0].external_data != null &&
currentAssiduites[0].external_data.hasOwnProperty("module")
) {
mod = currentAssiduites[0].external_data.module;
}
@ -1567,20 +1583,18 @@ function fastJustify(assiduite) {
//créer justificatif
const justif = {
date_debut: new moment.tz(assiduite.date_debut, TIMEZONE)
.add(1, "s")
.format(),
date_fin: new moment.tz(assiduite.date_fin, TIMEZONE)
.subtract(1, "s")
.format(),
date_debut: new moment.tz(assiduite.date_debut, TIMEZONE).format(),
date_fin: new moment.tz(assiduite.date_fin, TIMEZONE).format(),
raison: raison,
etat: etat,
};
createJustificatif(justif);
// justifyAssiduite(assiduite.assiduite_id, true);
generateAllEtudRow();
try {
loadAll();
} catch {}
};
const content = document.createElement("fieldset");
@ -1643,8 +1657,17 @@ function createJustificatif(justif, success = () => {}) {
});
}
function getAllJustificatifsFromEtud(etudid, action) {
const url_api = getUrl() + `/api/justificatifs/${etudid}`;
function getAllJustificatifsFromEtud(
etudid,
action,
order = false,
courant = false
) {
const url_api =
getUrl() +
`/api/justificatifs/${etudid}${
order ? "/query?order°".replace("°", courant ? "&courant" : "") : ""
}`;
$.ajax({
async: true,
type: "GET",
@ -1696,9 +1719,9 @@ function getModuleImpl(assiduite) {
if (id == null || id == undefined) {
if (
"external_data" in assiduite &&
assiduite.external_data instanceof Object &&
"module" in assiduite.external_data
assiduite.hasOwnProperty("external_data") &&
assiduite.external_data != null &&
assiduite.external_data.hasOwnProperty("module")
) {
return assiduite.external_data.module;
} else {
@ -1724,11 +1747,13 @@ function getModuleImpl(assiduite) {
}
function getUser(obj) {
if ("external_data" in obj && obj.external_data != null) {
if ("enseignant" in obj.external_data) {
if (
obj.hasOwnProperty("external_data") &&
obj.external_data != null &&
obj.external_data.hasOwnProperty("enseignant")
) {
return obj.external_data.enseignant;
}
}
return obj.user_id;
}

View File

@ -7,25 +7,39 @@ $(function () {
display_itemsuivis(false);
});
function display_itemsuivis(active) {
var etudid = $('div#fichedebouche').data("etudid");
var readonly = $('div#fichedebouche').data('readonly'); // present ro interface
var etudid = $("div#fichedebouche").data("etudid");
var readonly = $("div#fichedebouche").data("readonly"); // present ro interface
if (!readonly) {
$('#adddebouchelink').off("click").click(function (e) {
$("#adddebouchelink")
.off("click")
.click(function (e) {
e.preventDefault();
$.post(SCO_URL + "/itemsuivi_create", { etudid: etudid, format: 'json' }).done(item_insert_new);
$.post(SCO_URL + "/itemsuivi_create", {
etudid: etudid,
fmt: "json",
}).done(item_insert_new);
return false;
});
}
// add existing items
$.get(SCO_URL + "/itemsuivi_list_etud", { etudid: etudid, format: 'json' }, function (L) {
$.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);
item_insert(
L[i]["itemsuivi_id"],
L[i]["item_date"],
L[i]["situation"],
L[i]["tags"],
readonly
);
}
});
}
);
$("div#fichedebouche").accordion({
heightStyle: "content",
@ -35,99 +49,115 @@ function display_itemsuivis(active) {
}
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) {
if (item_date === undefined)
item_date = Date2DMY(new Date());
if (situation === undefined)
situation = '';
if (tags === undefined)
tags = '';
if (item_date === undefined) item_date = Date2DMY(new Date());
if (situation === undefined) situation = "";
if (tags === undefined) tags = "";
var nodes = item_nodes(itemsuivi_id, item_date, situation, tags, readonly);
// insert just before last li:
if ($('ul.listdebouches li.adddebouche').length > 0) {
$('ul.listdebouches').children(':last').before(nodes);
if ($("ul.listdebouches li.adddebouche").length > 0) {
$("ul.listdebouches").children(":last").before(nodes);
} else {
// mode readonly, pas de li "ajouter"
$('ul.listdebouches').append(nodes);
$("ul.listdebouches").append(nodes);
}
};
}
function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
// 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;
// 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 +=
'<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>";
// tags:
h += '<div class="itemsuivi_tag_edit"><textarea class="itemsuivi_tag_editor">' + tags + '</textarea></div>';
h +=
'<div class="itemsuivi_tag_edit"><textarea class="itemsuivi_tag_editor">' +
tags +
"</textarea></div>";
var nodes = $($.parseHTML('<li class="itemsuivi">' + h + '</li>'));
var dp = nodes.find('.itemsuividatepicker');
var nodes = $($.parseHTML('<li class="itemsuivi">' + h + "</li>"));
var dp = nodes.find(".itemsuividatepicker");
dp.blur(function (e) {
var date = this.value;
// 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) {
// console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id);
$.post(SCO_URL + "/itemsuivi_set_date", { item_date: date, itemsuivi_id: itemsuivi_id });
},
showOn: 'button',
buttonImage: '/ScoDoc/static/icons/calendar_img.png',
buttonImageOnly: true,
dateFormat: 'dd/mm/yy',
duration: 'fast',
disabled: readonly
$.post(SCO_URL + "/itemsuivi_set_date", {
item_date: date,
itemsuivi_id: itemsuivi_id,
});
dp.datepicker('option', $.extend({ showMonthAfterYear: false },
$.datepicker.regional['fr']));
},
showOn: "button",
buttonImage: "/ScoDoc/static/icons/calendar_img.png",
buttonImageOnly: true,
dateFormat: "dd/mm/yy",
duration: "fast",
disabled: readonly,
});
dp.datepicker(
"option",
$.extend({ showMonthAfterYear: false }, $.datepicker.regional["fr"])
);
if (readonly) {
// show tags read-only
readOnlyTags(nodes.find('.itemsuivi_tag_editor'));
}
else {
readOnlyTags(nodes.find(".itemsuivi_tag_editor"));
} else {
// bind tag editor
nodes.find('.itemsuivi_tag_editor').tagEditor({
initialTags: '',
placeholder: 'Tags...',
nodes.find(".itemsuivi_tag_editor").tagEditor({
initialTags: "",
placeholder: "Tags...",
onChange: function (field, editor, tags) {
$.post('itemsuivi_tag_set',
{
$.post("itemsuivi_tag_set", {
itemsuivi_id: itemsuivi_id,
taglist: tags.join()
taglist: tags.join(),
});
},
autocomplete: {
delay: 200, // ms before suggest
position: { collision: 'flip' }, // automatic menu position up/down
source: "itemsuivi_tag_search"
position: { collision: "flip" }, // automatic menu position up/down
source: "itemsuivi_tag_search",
},
});
// bind inplace editor
nodes.find('div.itemsituation').jinplace();
nodes.find("div.itemsituation").jinplace();
}
return nodes;
};
}
function Date2DMY(date) {
var year = date.getFullYear();
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();
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) {

View File

@ -11,7 +11,6 @@ $().ready(function () {
get_notes_and_draw(formsemestre_id, etudid);
});
var WIDTH = 460; // taille du canvas SVG
var HEIGHT = WIDTH;
var CX = WIDTH / 2; // coordonnees centre du cercle
@ -33,18 +32,23 @@ function get_notes_and_draw(formsemestre_id, etudid) {
'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 = [];
bul.ue.forEach(
function (ue, i, ues) {
ue['module'].forEach(function (m, i) {
bul.ue.forEach(function (ue, i, ues) {
ue["module"].forEach(function (m, i) {
notes.push({
'code': m['code'],
'titre': m['titre'],
'note': m['note']['value'],
'moy': m['note']['moy']
code: m["code"],
titre: m["titre"],
note: m["note"]["value"],
moy: m["note"]["moy"],
});
});
});
@ -55,50 +59,62 @@ function get_notes_and_draw(formsemestre_id, etudid) {
function draw_radar(notes) {
/* Calcul coordonnées des éléments */
var nmod = notes.length;
var angle = 2 * Math.PI / nmod;
var angle = (2 * Math.PI) / nmod;
for (var i = 0; i < notes.length; i++) {
var d = notes[i];
var cx = Math.sin(i * angle);
var cy = - Math.cos(i * angle);
d["x_v"] = CX + RR * d.note / 20 * cx;
d["y_v"] = CY + RR * d.note / 20 * cy;
d["x_moy"] = CX + RR * d.moy / 20 * cx;
d["y_moy"] = CY + RR * d.moy / 20 * cy;
var cy = -Math.cos(i * angle);
d["x_v"] = CX + ((RR * d.note) / 20) * cx;
d["y_v"] = CY + ((RR * d.note) / 20) * cy;
d["x_moy"] = CX + ((RR * d.moy) / 20) * cx;
d["y_moy"] = CY + ((RR * d.moy) / 20) * cy;
d["x_20"] = CX + RR * cx;
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["tics"] = [];
// 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 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 != '-'; })
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")
g = d3
.select("#radar_bulletin")
.append("svg")
.attr("class", "radar")
.attr("width", WIDTH + 100)
.attr("height", HEIGHT);
/* Centre */
g.append("circle").attr("cy", CY)
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++) {
var ligne_tics = d3.svg.line()
.x(function (d) { return d["tics"][j]["x"]; })
.y(function (d) { return d["tics"][j]["y"]; });
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")
.attr("class", "radar_disk_tic")
.attr("id", "radar_disk_tic_" + R_TICS[j])
@ -108,26 +124,40 @@ function draw_radar(notes) {
/* Lignes radiales pour chaque module */
g.selectAll("radar_rad")
.data(notes)
.enter().append("line")
.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("x2", function (d) {
return d["x_20"];
})
.attr("y2", function (d) {
return d["y_20"];
})
.attr("class", "radarrad");
/* Lignes entre notes */
var ligne = d3.svg.line()
.x(function (d) { return d["x_v"]; })
.y(function (d) { return d["y_v"]; });
var ligne = d3.svg
.line()
.x(function (d) {
return d["x_v"];
})
.y(function (d) {
return d["y_v"];
});
g.append("svg:path")
.attr("class", "radarnoteslines")
.attr("d", ligne(notes_circ_valid));
var ligne_moy = d3.svg.line()
.x(function (d) { return d["x_moy"]; })
.y(function (d) { return d["y_moy"]; })
var ligne_moy = d3.svg
.line()
.x(function (d) {
return d["x_moy"];
})
.y(function (d) {
return d["y_moy"];
});
g.append("svg:path")
.attr("class", "radarmoylines")
@ -136,81 +166,96 @@ function draw_radar(notes) {
/* Points (notes) */
g.selectAll("circle1")
.data(notes_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; })
.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) {
if (x - CX < 0) {
x = x + 5;
if (x + rwidth + 12 > WIDTH) {
x = WIDTH - rwidth - 12;
}
}
else {
if ((x - CX) > 0) {
} else {
if (x - CX > 0) {
x = x - rwidth - 5;
if (x < 12) {
x = 12;
}
}
else {
} else {
x = CX - rwidth / 2;
}
}
var yrect = d["y_v"];
var ytext = d["y_v"];
if ((yrect - CY) > 0) {
if (yrect - CY > 0) {
yrect = yrect - 5 - 20;
ytext = ytext - 5 - 20 + 16;
}
else {
} else {
yrect = yrect + 5;
ytext = ytext + 5 + 16;
}
var r = g.append("rect")
.attr('class', 'radartip')
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")
.attr('class', 'radartip')
var txt = g
.append("text")
.text("Note: " + d.note + "/20, moyenne promo: " + d.moy + "/20")
.attr("class", "radartip")
.attr("x", x + 5)
.attr("y", ytext);
r.attr("width", rwidth).attr("height", 20);
})
.on("mouseout", function (d) {
d3.selectAll(".radartip").remove()
d3.selectAll(".radartip").remove();
});
/* Valeurs des notes */
g.selectAll("notes_labels")
.data(notes_valid)
.enter().append("text")
.text(function (d) { return d["note"]; })
.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;
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 */
g.selectAll("circle2")
.data(notes_valid)
.enter().append("circle")
.attr("cx", function (d) { return d["x_moy"]; })
.attr("cy", function (d) { return d["y_moy"]; })
.attr("r", function (x, i) { return 2; })
.enter()
.append("circle")
.attr("cx", function (d) {
return d["x_moy"];
})
.attr("cy", function (d) {
return d["y_moy"];
})
.attr("r", function (x, i) {
return 2;
})
.style("stroke-width", 0)
.style("stroke", "black")
.style("fill", "rgb(20,90,50)");
@ -218,64 +263,74 @@ function draw_radar(notes) {
/* Valeurs sur axe */
g.selectAll("textaxis")
.data(R_AXIS_TICS)
.enter().append("text")
.enter()
.append("text")
.text(String)
.attr("x", CX - 10)
.attr("y", function (x, i) { return CY - x * RR / 20 + 6; })
.attr("y", function (x, i) {
return CY - (x * RR) / 20 + 6;
})
.attr("class", "textaxis");
/* Noms des modules */
g.selectAll("text_modules")
.data(notes)
.enter().append("text")
.text(function (d) { return d['code']; })
.attr("x", function (d) { return d['x_label']; })
.attr("y", function (d) { return d['y_label']; })
.enter()
.append("text")
.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 titre = d["titre"].replace("&apos;", "'").substring(0, 64);
var rwidth = titre.length * 9; // rough estimate of string width in pixels
if ((x - CX) < 0) {
if (x - CX < 0) {
x = x + 5;
if (x + rwidth + 12 > WIDTH) {
x = WIDTH - rwidth - 12;
}
}
else {
if ((x - CX) > 0) {
} else {
if (x - CX > 0) {
x = x - rwidth - 5;
if (x < 12) {
x = 12;
}
}
else {
} else {
x = CX - rwidth / 2;
}
}
if ((yrect - CY) > 0) {
if (yrect - CY > 0) {
yrect = yrect - 5 - 20;
ytext = ytext - 5 - 20 + 16;
}
else {
} else {
yrect = yrect + 5;
ytext = ytext + 5 + 16;
}
var r = g.append("rect")
.attr('class', 'radartip')
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')
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()
d3.selectAll(".radartip").remove();
});
}

View File

@ -114,9 +114,18 @@ class RowAssi(tb.Row):
compte_justificatifs = scass.filter_by_date(
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]]:
retour: dict[str, tuple[str, float, float]] = {

View File

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

View File

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

View File

@ -26,10 +26,18 @@
<section class="nonvalide">
<!-- 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" %}
<!-- Tableaux des justificatifs à valider (attente / modifié ) -->
<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" %}
</section>
@ -44,7 +52,7 @@
<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
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>
<h3>Gestion des justificatifs</h3>
<p>
@ -53,21 +61,21 @@
contextuel :
</p>
<ul>
<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>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
<li>Détails : affiche les détails du justificatif sélectionné</li>
<li>Éditer : modifie le justificatif (dates, état, ajouter/supprimer fichier, etc.)</li>
<li>Supprimer : supprime le justificatif (action irréversible)</li>
</ul>
<h3>Gestion des Assiduités</h3>
<h3>Gestion de l'assiduité</h3>
<p>
Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
contextuel :
</p>
<ul>
<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>Supprimer : Permet de supprimer l'assiduité (Action Irréversible)</li>
<li>Détails : affiche les détails de l'élément sélectionnée</li>
<li>Editer : modifie l'élément (module, état)</li>
<li>Supprimer : supprime l'élément (action irréversible)</li>
</ul>
</div>
@ -181,9 +189,9 @@
function removeAllAssiduites() {
openPromptModal(
"Suppression des assiduités",
"Suppression de l'assiduité",
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) => {
@ -266,6 +274,9 @@
const assi_date_debut = "{{date_debut}}";
const assi_date_fin = "{{date_fin}}";
const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false;
window.addEventListener('load', () => {
filterAssiduites = {
"columns": [

View File

@ -3,7 +3,7 @@
<div class="pageContent">
{{minitimeline | safe }}
<h2>Assiduités de {{sco.etud.nomprenom}}</h2>
<h2>Assiduité de {{sco.etud.nomprenom}}</h2>
<div class="calendrier">
</div>
@ -13,22 +13,22 @@
</select>
</div>
<div class="legende">
<div class="help">
<h3>Calendrier</h3>
<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>
<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>
<p>Le jour sera affiché en : </p>
<ul>
<li>Rouge : S'il y a une assiduité "Absent"</li>
<li>Orange : S'il y a une assiduité "Retard" et pas d'assiduité "Absent"</li>
<li>Vert : S'il y a une assiduité "Present" et pas d'assiduité "Absent" ni "Retard"</li>
<li>Blanc : S'il n'y a pas d'assiduité</li>
<li>Rouge : s'il y a une absence enregistrée</li>
<li>Orange : s'il y a un retard et pas d'absence</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 rien d'enregistré</li>
</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>
@ -354,5 +354,7 @@
setterAnnee(defAnnee)
};
function isCalendrier() { return true }
</script>
{% 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>
{% include "assiduites/widgets/tableau_base.j2" %}
<h3>Assiduités :</h3>
<a class="icon filter" onclick="filter()"></a>
<h3>Assiduité :</h3>
<span class="iconline">
<a class="icon filter" onclick="filterAssi()"></a>
<a class="icon download" onclick="downloadAssi()"></a>
</span>
{% include "assiduites/widgets/tableau_assi.j2" %}
<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" %}
<ul id="contextMenu" class="context-menu">
<li id="detailOption">Detail</li>
@ -27,20 +33,20 @@
<li>Supprimer : Permet de supprimer le justificatif (Action Irréversible)</li>
</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>
Faites
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu
contextuel :
</p>
<ul>
<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>Supprimer : Permet de supprimer l'assiduité (Action Irréversible)</li>
<li>Détails : affiche les détails de l'assiduité sélectionnée</li>
<li>Éditer : modifier l'élément (module, état)</li>
<li>Supprimer : supprimer l'élément (action irréversible)</li>
</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>
@ -48,8 +54,43 @@
<script>
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 = () => {
loadAll();
if (assiduite_unique_id != -1) {
wayForFilter()
}
}
</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>
<div class="legende">
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
<div class="help">
<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>
<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>
<p>Modifier le moduleimpl alors que des assiduités sont déjà enregistrées pour la période changera leur
moduleimpl.</p>
<p>Il y a 4 boutons d'assiduités sur la colonne permettant de mettre l'assiduités à tous les étudiants</p>
<p>Le dernier des boutons retire l'assiduité.</p>
<p>Modifier le module alors que des informations d'assiduité sont déjà enregistrées pour la période changera leur
module.</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'information présente.</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>
</div>
<h3>{{sem | safe }}</h3>

View File

@ -32,6 +32,18 @@
</div>
</div>
<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}}
<div class="legende">
@ -62,16 +74,16 @@
<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>
<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>
<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>
<p>Modifier le moduleimpl alors que des assiduités sont déjà enregistrées pour la période changera leur
moduleimpl.</p>
<p>Il y a 4 boutons d'assiduités sur la colonne permettant de mettre l'assiduités à tous les étudiants</p>
<p>Le dernier des boutons retire l'assiduité.</p>
<p>Modifier le module alors que des informations sont déjà enregistrées pour la période changera leur
module.</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'information présente.</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>
</div>
@ -118,7 +130,20 @@
window.forceModule = "{{ forcer_module }}"
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();
{% endif %}
</script>

View File

@ -16,13 +16,13 @@
value="{{date_fin}}"></label>
<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>
{{tableau | safe}}
<div class=""help">
Les comptes sont exprimés en {{ assi_metric }}.
Les comptes sont exprimés en {{ assi_metric | lower}}s.
</div>
<script>

View File

@ -129,7 +129,7 @@
</div>
<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="split" class="btnPrompt" disabled>Séparer</button>
<button id="edit" class="btnPrompt" disabled>Modifier l'état</button>
@ -348,7 +348,7 @@
// Actualiser l'affichage
editAssiduite(this.selectedAssiduite.assiduite_id, newState);
editAssiduite(this.selectedAssiduite.assiduite_id, newState, [this.selectedAssiduite]);
this.callbacks.edit(this.selectedAssiduite)
this.refresh(assiduites[this.selectedAssiduite.etudid]);

View File

@ -96,6 +96,15 @@
display: flex;
}
.td[assiduite_id='insc'] * {
display: none;
}
.td[assiduite_id='insc']::after {
content: "non inscrit au module";
font-style: italic;
}
.sticky {
position: sticky;
left: 0;
@ -278,7 +287,9 @@
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 th = document.createElement("div");
th.classList.add("th", "error");
@ -343,6 +354,10 @@
editModuleImpl(sl);
})
if (moduleimpl_id != "") {
sl.value = moduleimpl_id;
}
let rows = table.querySelector(".tbody").querySelectorAll(".tr");
for (let i = 0; i < rows.length; i++) {
let td = document.createElement("div");
@ -533,7 +548,7 @@
}
if (get) {
getAssiduitesFromEtuds(false, false, d_debut.format(), d_fin.format())
getAssiduitesFromEtuds(false, d_debut.format(), d_fin.format())
return 0x0;
}
@ -557,6 +572,8 @@
const d_debut = moment(inputDeb).tz(TIMEZONE);
const d_fin = moment(inputFin).tz(TIMEZONE);
const moduleimpl_id = col.querySelector("#moduleimpl_select").value;
const periode = {
deb: d_debut,
fin: d_fin,
@ -569,9 +586,12 @@
});
setEtatLine(td, "")
const etu = td.parentElement.getAttribute('etudid');
const inscriptionModule = ["", "autre"].indexOf(moduleimpl_id) !== -1 ? true : checkInscriptionModule(moduleimpl_id, etu);
const conflits = getAssiduitesConflict(etu, periode);
if (conflits.length == 0) {
if (!inscriptionModule) {
td.setAttribute('assiduite_id', "insc");
}
else if (conflits.length == 0) {
td.setAttribute('assiduite_id', "-1");
} else if (conflits.length == 1 && isConflictSameAsPeriod(conflits[0], periode)) {
const assi = conflits[0];
@ -583,7 +603,6 @@
const inputs = [...td.querySelectorAll('input')];
inputs.forEach((i) => {
i.disabled = true;
})
}
})
@ -867,7 +886,7 @@
const { moduleimpl, deb, fin } = getAssiduitesCol(colid, false);
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' })
@ -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', () => {
document.getElementById("addColumn").addEventListener("click", () => {
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());
updateJustifyBtn();
}
try {
if (isCalendrier()) {
window.location = `ListeAssiduitesEtud?etudid=${etudid}&assiduite_id=${assiduité.assiduite_id}`
}
} catch { }
});
//ajouter affichage assiduites on over
setupAssiduiteBuble(block, assiduité);

View File

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

View File

@ -88,6 +88,10 @@
td.textContent = getModuleImpl(assiduite);
} else if (k.indexOf('est_just') != -1) {
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 {
td.textContent = assiduite[k].capitalize()
}
@ -147,7 +151,7 @@
<span class="obj-content">${etat}</span>
</div>
<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>
</div>
</div>
@ -184,8 +188,11 @@
path,
(data) => {
let module = data.moduleimpl_id;
if (module == null && "external_data" in data && "module" in data.external_data) {
if (
module == null && data.hasOwnProperty("external_data") &&
data.external_data != null &&
data.external_data.hasOwnProperty('module')
) {
module = data.external_data.module.toLowerCase();
}
@ -220,7 +227,7 @@
assiEdit.querySelector('#etat').value = etat.toLowerCase();
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));
openPromptModal("Modification de l'assiduité", assiEdit, () => {
const prompt = document.querySelector('.assi-edit');
@ -236,7 +243,7 @@
edit = setModuleImplId(edit, module);
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>

View File

@ -18,6 +18,9 @@
document.addEventListener("click", () => {
contextMenu.style.display = "none";
if (contextMenu.childElementCount > 3) {
contextMenu.removeChild(contextMenu.lastElementChild)
}
});
editOption.addEventListener("click", () => {
@ -57,8 +60,6 @@
deleteJustificatif(obj_id);
}
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;
})
@ -150,7 +167,7 @@
paginationContainerAssiduites.querySelector('.pagination_moins').addEventListener('click', () => {
if (currentPageAssiduites > 1) {
currentPageAssiduites--;
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + ""
assiduiteCallBack(array);
}
@ -159,7 +176,7 @@
paginationContainerAssiduites.querySelector('.pagination_plus').addEventListener('click', () => {
if (currentPageAssiduites < totalPages) {
currentPageAssiduites++;
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites
paginationContainerAssiduites.querySelector('#paginationAssi').value = currentPageAssiduites + ""
assiduiteCallBack(array);
}
})
@ -199,8 +216,12 @@
if (assi) {
paginationContainerAssiduites.querySelector('#paginationAssi').appendChild(paginationButton)
if (i == currentPageAssiduites)
paginationContainerAssiduites.querySelector('#paginationAssi').value = i + "";
} else {
paginationContainerJustificatifs.querySelector('#paginationJusti').appendChild(paginationButton)
if (i == currentPageJustificatifs)
paginationContainerJustificatifs.querySelector('#paginationJusti').value = i + "";
}
}
updateActivePaginationButton(assi);
@ -230,8 +251,8 @@
}
function loadAll() {
try { getAllAssiduitesFromEtud(etudid, assiduiteCallBack) } catch (_) { }
try { getAllJustificatifsFromEtud(etudid, justificatifCallBack) } catch (_) { }
try { getAssi(assiduiteCallBack) } catch { }
try { getJusti(justificatifCallBack) } catch { }
}
function order(keyword, callback = () => { }, el, assi = true) {
@ -249,6 +270,13 @@
keyValueA = getModuleImpl(a);
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;
@ -266,351 +294,14 @@
if (assi) {
orderAssiduites = !orderAssiduites;
getAllAssiduitesFromEtud(etudid, (a) => { call(a, orderAssiduites) })
getAssi((a) => { call(a, orderAssiduites) });
} else {
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) {
switch (colName) {
@ -632,6 +323,8 @@
return "Fichier";
case "etudid":
return "Etudiant";
case "formsemestre":
return "Semestre";
}
}
@ -641,6 +334,103 @@
contextMenu.style.top = `${e.clientY - contextMenu.offsetHeight}px`;
contextMenu.style.left = `${e.clientX}px`;
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>

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