forked from ScoDoc/ScoDoc
Merge remote-tracking branch 'scodoc/master' into pe-BUT-v2
# Conflicts: # app/pe/pe_jurype.py
This commit is contained in:
commit
83c6ec44c8
@ -3,9 +3,11 @@
|
||||
from flask_json import as_json
|
||||
from flask import Blueprint
|
||||
from flask import request, g
|
||||
from flask_login import current_user
|
||||
from app import db
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoException
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
api_bp = Blueprint("api", __name__)
|
||||
api_web_bp = Blueprint("apiweb", __name__)
|
||||
@ -48,13 +50,21 @@ def requested_format(default_format="json", allowed_formats=None):
|
||||
|
||||
|
||||
@as_json
|
||||
def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
|
||||
def get_model_api_object(
|
||||
model_cls: db.Model,
|
||||
model_id: int,
|
||||
join_cls: db.Model = None,
|
||||
restrict: bool | None = None,
|
||||
):
|
||||
"""
|
||||
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
|
||||
|
||||
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemestre) -> join_cls
|
||||
|
||||
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
|
||||
|
||||
L'agument restrict est passé to_dict, est signale que l'on veut une version restreinte
|
||||
(sans données personnelles, ou sans informations sur le justificatif d'absence)
|
||||
"""
|
||||
query = model_cls.query.filter_by(id=model_id)
|
||||
if g.scodoc_dept and join_cls is not None:
|
||||
@ -66,8 +76,9 @@ def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model
|
||||
404,
|
||||
message=f"{model_cls.__name__} inexistant(e)",
|
||||
)
|
||||
|
||||
if restrict is None:
|
||||
return unique.to_dict(format_api=True)
|
||||
return unique.to_dict(format_api=True, restrict=restrict)
|
||||
|
||||
|
||||
from app.api import tokens
|
||||
|
@ -104,7 +104,8 @@ def etudiants_courants(long=False):
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
if long:
|
||||
data = [etud.to_dict_api() for etud in etuds]
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
data = [etud.to_dict_api(restrict=restrict) for etud in etuds]
|
||||
else:
|
||||
data = [etud.to_dict_short() for etud in etuds]
|
||||
return data
|
||||
@ -138,8 +139,8 @@ def etudiant(etudid: int = None, nip: str = None, ine: str = None):
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
|
||||
return etud.to_dict_api()
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
return etud.to_dict_api(restrict=restrict)
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/photo")
|
||||
@ -251,7 +252,8 @@ def etudiants(etudid: int = None, nip: str = None, ine: str = None):
|
||||
query = query.join(Departement).filter(
|
||||
or_(Departement.acronym == acronym for acronym in allowed_depts)
|
||||
)
|
||||
return [etud.to_dict_api() for etud in query]
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
return [etud.to_dict_api(restrict=restrict) for etud in query]
|
||||
|
||||
|
||||
@bp.route("/etudiants/name/<string:start>")
|
||||
@ -278,7 +280,11 @@ def etudiants_by_name(start: str = "", min_len=3, limit=32):
|
||||
)
|
||||
etuds = query.order_by(Identite.nom, Identite.prenom).limit(limit)
|
||||
# Note: on raffine le tri pour les caractères spéciaux et nom usuel ici:
|
||||
return [etud.to_dict_api() for etud in sorted(etuds, key=attrgetter("sort_key"))]
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
return [
|
||||
etud.to_dict_api(restrict=restrict)
|
||||
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
||||
]
|
||||
|
||||
|
||||
@bp.route("/etudiant/etudid/<int:etudid>/formsemestres")
|
||||
@ -543,7 +549,8 @@ def etudiant_create(force=False):
|
||||
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
|
||||
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
|
||||
db.session.refresh(etud)
|
||||
r = etud.to_dict_api()
|
||||
|
||||
r = etud.to_dict_api(restrict=False) # pas de restriction, on vient de le créer
|
||||
return r
|
||||
|
||||
|
||||
@ -590,5 +597,6 @@ def etudiant_edit(
|
||||
# Note: je ne comprends pas pourquoi un refresh est nécessaire ici
|
||||
# sans ce refresh, etud.__dict__ est incomplet (pas de 'nom').
|
||||
db.session.refresh(etud)
|
||||
r = etud.to_dict_api()
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
r = etud.to_dict_api(restrict=restrict)
|
||||
return r
|
||||
|
@ -67,7 +67,7 @@ def get_evaluation(evaluation_id: int):
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def evaluations(moduleimpl_id: int):
|
||||
def moduleimpl_evaluations(moduleimpl_id: int):
|
||||
"""
|
||||
Retourne la liste des évaluations d'un moduleimpl
|
||||
|
||||
@ -75,14 +75,8 @@ def evaluations(moduleimpl_id: int):
|
||||
|
||||
Exemple de résultat : voir /evaluation
|
||||
"""
|
||||
query = Evaluation.query.filter_by(moduleimpl_id=moduleimpl_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
query.join(ModuleImpl)
|
||||
.join(FormSemestre)
|
||||
.filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
return [e.to_dict_api() for e in query]
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return [evaluation.to_dict_api() for evaluation in modimpl.evaluations]
|
||||
|
||||
|
||||
@bp.route("/evaluation/<int:evaluation_id>/notes")
|
||||
|
@ -11,7 +11,7 @@ from operator import attrgetter, itemgetter
|
||||
|
||||
from flask import g, make_response, request
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
import app
|
||||
from app import db
|
||||
@ -360,7 +360,8 @@ def formsemestre_etudiants(
|
||||
inscriptions = formsemestre.inscriptions
|
||||
|
||||
if long:
|
||||
etuds = [ins.etud.to_dict_api() for ins in inscriptions]
|
||||
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
||||
etuds = [ins.etud.to_dict_api(restrict=restrict) for ins in inscriptions]
|
||||
else:
|
||||
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
|
||||
# Ajout des groupes de chaque étudiants
|
||||
|
@ -66,7 +66,7 @@ def _news_delete_jury_etud(etud: Identite):
|
||||
"génère news sur effacement décision"
|
||||
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
|
||||
url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||
"scolar.fiche_etud", scodoc_dept=etud.departement.acronym, etudid=etud.id
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_JURY,
|
||||
|
@ -15,7 +15,7 @@ from werkzeug.exceptions import NotFound
|
||||
|
||||
import app.scodoc.sco_assiduites as scass
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import db
|
||||
from app import db, set_sco_dept
|
||||
from app.api import api_bp as bp
|
||||
from app.api import api_web_bp
|
||||
from app.api import get_model_api_object, tools
|
||||
@ -53,14 +53,19 @@ def justificatif(justif_id: int = None):
|
||||
"date_fin": "2022-10-31T10:00+01:00",
|
||||
"etat": "valide",
|
||||
"fichier": "archive_id",
|
||||
"raison": "une raison",
|
||||
"raison": "une raison", // VIDE si pas le droit
|
||||
"entry_date": "2022-10-31T08:00+01:00",
|
||||
"user_id": 1 or null,
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
return get_model_api_object(Justificatif, justif_id, Identite)
|
||||
return get_model_api_object(
|
||||
Justificatif,
|
||||
justif_id,
|
||||
Identite,
|
||||
restrict=not current_user.has_permission(Permission.AbsJustifView),
|
||||
)
|
||||
|
||||
|
||||
# etudid
|
||||
@ -133,8 +138,9 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
|
||||
|
||||
# Mise en forme des données puis retour en JSON
|
||||
data_set: list[dict] = []
|
||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
||||
for just in justificatifs_query.all():
|
||||
data = just.to_dict(format_api=True)
|
||||
data = just.to_dict(format_api=True, restrict=restrict)
|
||||
data_set.append(data)
|
||||
|
||||
return data_set
|
||||
@ -151,7 +157,10 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
|
||||
@as_json
|
||||
@permission_required(Permission.ScoView)
|
||||
def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
||||
"""XXX TODO missing doc"""
|
||||
"""
|
||||
Renvoie tous les justificatifs d'un département
|
||||
(en ajoutant un champ "formsemestre" si possible)
|
||||
"""
|
||||
|
||||
# Récupération du département et des étudiants du département
|
||||
dept: Departement = Departement.query.get(dept_id)
|
||||
@ -169,14 +178,15 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
|
||||
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
||||
|
||||
# Mise en forme des données et retour JSON
|
||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
||||
data_set: list[dict] = []
|
||||
for just in justificatifs_query:
|
||||
data_set.append(_set_sems(just))
|
||||
data_set.append(_set_sems(just, restrict=restrict))
|
||||
|
||||
return data_set
|
||||
|
||||
|
||||
def _set_sems(justi: Justificatif) -> dict:
|
||||
def _set_sems(justi: Justificatif, restrict: bool) -> dict:
|
||||
"""
|
||||
_set_sems Ajoute le formsemestre associé au justificatif s'il existe
|
||||
|
||||
@ -189,7 +199,7 @@ def _set_sems(justi: Justificatif) -> dict:
|
||||
dict: La représentation de l'assiduité en dictionnaire
|
||||
"""
|
||||
# Conversion du justificatif en dictionnaire
|
||||
data = justi.to_dict(format_api=True)
|
||||
data = justi.to_dict(format_api=True, restrict=restrict)
|
||||
|
||||
# Récupération du formsemestre de l'assiduité
|
||||
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
|
||||
@ -243,9 +253,10 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
|
||||
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
|
||||
|
||||
# Retour des justificatifs en JSON
|
||||
restrict = not current_user.has_permission(Permission.AbsJustifView)
|
||||
data_set: list[dict] = []
|
||||
for justi in justificatifs_query.all():
|
||||
data = justi.to_dict(format_api=True)
|
||||
data = justi.to_dict(format_api=True, restrict=restrict)
|
||||
data_set.append(data)
|
||||
|
||||
return data_set
|
||||
@ -294,6 +305,7 @@ def justif_create(etudid: int = None, nip=None, ine=None):
|
||||
404,
|
||||
message="étudiant inconnu",
|
||||
)
|
||||
set_sco_dept(etud.departement.acronym)
|
||||
|
||||
# Récupération des justificatifs à créer
|
||||
create_list: list[object] = request.get_json(force=True)
|
||||
|
@ -8,16 +8,14 @@
|
||||
ScoDoc 9 API : accès aux moduleimpl
|
||||
"""
|
||||
|
||||
from flask import g
|
||||
from flask_json import as_json
|
||||
from flask_login import login_required
|
||||
|
||||
import app
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
ModuleImpl,
|
||||
)
|
||||
from app.models import ModuleImpl
|
||||
from app.scodoc import sco_liste_notes
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
@ -62,10 +60,7 @@ def moduleimpl(moduleimpl_id: int):
|
||||
}
|
||||
}
|
||||
"""
|
||||
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()
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return modimpl.to_dict(convert_objects=True)
|
||||
|
||||
|
||||
@ -87,8 +82,36 @@ def moduleimpl_inscriptions(moduleimpl_id: int):
|
||||
...
|
||||
]
|
||||
"""
|
||||
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()
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
return [i.to_dict() for i in modimpl.inscriptions]
|
||||
|
||||
|
||||
@bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
|
||||
@api_web_bp.route("/moduleimpl/<int:moduleimpl_id>/notes")
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
def moduleimpl_notes(moduleimpl_id: int):
|
||||
"""Liste des notes dans ce moduleimpl
|
||||
Exemple de résultat :
|
||||
[
|
||||
{
|
||||
"etudid": 17776, // code de l'étudiant
|
||||
"nom": "DUPONT",
|
||||
"prenom": "Luz",
|
||||
"38411": 16.0, // Note dans l'évaluation d'id 38411
|
||||
"38410": 15.0,
|
||||
"moymod": 15.5, // Moyenne INDICATIVE module
|
||||
"moy_ue_2875": 15.5, // Moyenne vers l'UE 2875
|
||||
"moy_ue_2876": 15.5, // Moyenne vers l'UE 2876
|
||||
"moy_ue_2877": 15.5 // Moyenne vers l'UE 2877
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
app.set_sco_dept(modimpl.formsemestre.departement.acronym)
|
||||
table, _ = sco_liste_notes.do_evaluation_listenotes(
|
||||
moduleimpl_id=modimpl.id, fmt="json"
|
||||
)
|
||||
return table
|
||||
|
@ -7,7 +7,6 @@
|
||||
"""
|
||||
ScoDoc 9 API : accès aux utilisateurs
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
@ -15,13 +14,14 @@ from flask_login import current_user, login_required
|
||||
|
||||
from app import db, log
|
||||
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.auth.models import User, Role, UserRole
|
||||
from app.auth.models import is_valid_password
|
||||
from app.decorators import scodoc, permission_required
|
||||
from app.models import Departement
|
||||
from app.models import Departement, ScoDocSiteConfig
|
||||
from app.scodoc import sco_edt_cal
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
@ -441,3 +441,63 @@ def role_delete(role_name: str):
|
||||
db.session.delete(role)
|
||||
db.session.commit()
|
||||
return {"OK": True}
|
||||
|
||||
|
||||
# @bp.route("/user/<int:uid>/edt")
|
||||
# @api_web_bp.route("/user/<int:uid>/edt")
|
||||
# @login_required
|
||||
# @scodoc
|
||||
# @permission_required(Permission.ScoView)
|
||||
# @as_json
|
||||
# def user_edt(uid: int):
|
||||
# """L'emploi du temps de l'utilisateur.
|
||||
# Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
|
||||
|
||||
# show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
||||
|
||||
# Il faut la permission ScoView + (UsersView ou bien être connecté comme l'utilisateur demandé)
|
||||
# """
|
||||
# if g.scodoc_dept is None: # route API non départementale
|
||||
# if not current_user.has_permission(Permission.UsersView):
|
||||
# return scu.json_error(403, "accès non autorisé")
|
||||
# user: User = db.session.get(User, uid)
|
||||
# if user is None:
|
||||
# return json_error(404, "user not found")
|
||||
# # Check permission
|
||||
# if current_user.id != user.id:
|
||||
# if g.scodoc_dept:
|
||||
# allowed_depts = current_user.get_depts_with_permission(Permission.UsersView)
|
||||
# if (None not in allowed_depts) and (user.dept not in allowed_depts):
|
||||
# return json_error(404, "user not found")
|
||||
|
||||
# show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
|
||||
|
||||
# # Cherche ics
|
||||
# if not user.edt_id:
|
||||
# return json_error(404, "user not configured")
|
||||
# ics_filename = sco_edt_cal.get_ics_user_edt_filename(user.edt_id)
|
||||
# if not ics_filename:
|
||||
# return json_error(404, "no calendar for this user")
|
||||
|
||||
# _, calendar = sco_edt_cal.load_calendar(ics_filename)
|
||||
|
||||
# # TODO:
|
||||
# # - Construire mapping edt2modimpl: edt_id -> modimpl
|
||||
# # pour cela, considérer tous les formsemestres de la période de l'edt
|
||||
# # (soit on considère l'année scolaire du 1er event, ou celle courante,
|
||||
# # soit on cherche min, max des dates des events)
|
||||
# # - Modifier décodage des groupes dans convert_ics pour avoi run mapping
|
||||
# # de groupe par semestre (retrouvé grâce au modimpl associé à l'event)
|
||||
|
||||
# raise NotImplementedError() # TODO XXX WIP
|
||||
|
||||
# events_scodoc, _ = sco_edt_cal.convert_ics(
|
||||
# calendar,
|
||||
# edt2group=edt2group,
|
||||
# default_group=default_group,
|
||||
# edt2modimpl=edt2modimpl,
|
||||
# )
|
||||
# edt_dict = sco_edt_cal.translate_calendar(
|
||||
# events_scodoc, group_ids, show_modules_titles=show_modules_titles
|
||||
# )
|
||||
# return edt_dict
|
||||
|
@ -499,10 +499,8 @@ class BulletinBUT:
|
||||
d["etud"]["etat_civil"] = etud.etat_civil
|
||||
d.update(self.res.sem)
|
||||
etud_etat = self.res.get_etud_etat(etud.id)
|
||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
|
||||
etud_etat,
|
||||
self.prefs,
|
||||
decision_sem=d["semestre"].get("decision"),
|
||||
d["filigranne"] = sco_bulletins_pdf.get_filigranne_apc(
|
||||
etud_etat, self.prefs, etud.id, res=self.res
|
||||
)
|
||||
if etud_etat == scu.DEMISSION:
|
||||
d["demission"] = "(Démission)"
|
||||
|
@ -35,6 +35,7 @@ from app.decorators import (
|
||||
permission_required,
|
||||
)
|
||||
from app.models import FormSemestre, FormSemestreInscription, Identite
|
||||
from app.scodoc import sco_bulletins_pdf
|
||||
from app.scodoc.codes_cursus import UE_STANDARD
|
||||
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
@ -104,8 +105,10 @@ def _build_bulletin_but_infos(
|
||||
bulletins_sem = BulletinBUT(formsemestre)
|
||||
if fmt == "pdf":
|
||||
bul: dict = bulletins_sem.bulletin_etud_complet(etud)
|
||||
filigranne = bul["filigranne"]
|
||||
else: # la même chose avec un peu moins d'infos
|
||||
bul: dict = bulletins_sem.bulletin_etud(etud, force_publishing=True)
|
||||
filigranne = ""
|
||||
decision_ues = (
|
||||
{x["acronyme"]: x for x in bul["semestre"]["decision_ue"]}
|
||||
if "semestre" in bul and "decision_ue" in bul["semestre"]
|
||||
@ -131,6 +134,7 @@ def _build_bulletin_but_infos(
|
||||
"decision_ues": decision_ues,
|
||||
"ects_total": ects_total,
|
||||
"etud": etud,
|
||||
"filigranne": filigranne,
|
||||
"formsemestre": formsemestre,
|
||||
"logo": logo,
|
||||
"prefs": bulletins_sem.prefs,
|
||||
|
@ -48,6 +48,7 @@ def make_bulletin_but_court_pdf(
|
||||
ects_total: float = 0.0,
|
||||
etud: Identite = None,
|
||||
formsemestre: FormSemestre = None,
|
||||
filigranne=""
|
||||
logo: Logo = None,
|
||||
prefs: SemPreferences = None,
|
||||
title: str = "",
|
||||
@ -86,6 +87,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
decision_ues: dict = None,
|
||||
ects_total: float = 0.0,
|
||||
etud: Identite = None,
|
||||
filigranne="",
|
||||
formsemestre: FormSemestre = None,
|
||||
logo: Logo = None,
|
||||
prefs: SemPreferences = None,
|
||||
@ -95,7 +97,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
|
||||
] = None,
|
||||
ues_acronyms: list[str] = None,
|
||||
):
|
||||
super().__init__(bul, authuser=current_user)
|
||||
super().__init__(bul, authuser=current_user, filigranne=filigranne)
|
||||
self.bul = bul
|
||||
self.cursus = cursus
|
||||
self.decision_ues = decision_ues
|
||||
|
@ -380,14 +380,24 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
sco_codes.ADJ,
|
||||
] + self.codes
|
||||
explanation += f" et {self.nb_rcues_under_8} < 8"
|
||||
else: # autres cas: non admis, non passage, non dem, pas la moitié des rcue:
|
||||
if formsemestre.semestre_id % 2 and self.formsemestre_pair is None:
|
||||
# Si jury sur un seul semestre impair, ne propose pas redoublement
|
||||
# et efface décision éventuellement existante
|
||||
codes = [None]
|
||||
else:
|
||||
self.codes = [
|
||||
codes = []
|
||||
self.codes = (
|
||||
codes
|
||||
+ [
|
||||
sco_codes.RED,
|
||||
sco_codes.NAR,
|
||||
sco_codes.PAS1NCI,
|
||||
sco_codes.ADJ,
|
||||
sco_codes.PASD, # voir #488 (discutable, conventions locales)
|
||||
] + self.codes
|
||||
]
|
||||
+ self.codes
|
||||
)
|
||||
explanation += f""" et {self.nb_rcues_under_8
|
||||
} niveau{'x' if self.nb_rcues_under_8 > 1 else ''} < 8"""
|
||||
|
||||
@ -514,7 +524,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
"""Les deux formsemestres auquel est inscrit l'étudiant (ni DEM ni DEF)
|
||||
du niveau auquel appartient formsemestre.
|
||||
|
||||
-> S_impair, S_pair
|
||||
-> S_impair, S_pair (de la même année scolaire)
|
||||
|
||||
Si l'origine est impair, S_impair est l'origine et S_pair est None
|
||||
Si l'origine est paire, S_pair est l'origine, et S_impair l'antérieur
|
||||
@ -524,9 +534,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
return None, None
|
||||
|
||||
if formsemestre.semestre_id % 2:
|
||||
idx_autre = formsemestre.semestre_id + 1
|
||||
idx_autre = formsemestre.semestre_id + 1 # impair, autre = suivant
|
||||
else:
|
||||
idx_autre = formsemestre.semestre_id - 1
|
||||
idx_autre = formsemestre.semestre_id - 1 # pair: autre = précédent
|
||||
|
||||
# Cherche l'autre semestre de la même année scolaire:
|
||||
autre_formsemestre = None
|
||||
@ -610,6 +620,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
def next_semestre_ids(self, code: str) -> set[int]:
|
||||
"""Les indices des semestres dans lequels l'étudiant est autorisé
|
||||
à poursuivre après le semestre courant.
|
||||
code: code jury sur année BUT
|
||||
"""
|
||||
# La poursuite d'études dans un semestre pair d'une même année
|
||||
# est de droit pour tout étudiant.
|
||||
@ -653,6 +664,8 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
|
||||
Si les code_rcue et le code_annee ne sont pas fournis,
|
||||
et qu'il n'y en a pas déjà, enregistre ceux par défaut.
|
||||
|
||||
Si le code_annee est None, efface le code déjà enregistré.
|
||||
"""
|
||||
log("jury_but.DecisionsProposeesAnnee.record_form")
|
||||
code_annee = self.codes[0] # si pas dans le form, valeur par defaut
|
||||
@ -697,6 +710,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
def record(self, code: str, mark_recorded: bool = True) -> bool:
|
||||
"""Enregistre le code de l'année, et au besoin l'autorisation d'inscription.
|
||||
Si l'étudiant est DEM ou DEF, ne fait rien.
|
||||
Si le code est None, efface le code déjà enregistré.
|
||||
Si mark_recorded est vrai, positionne self.recorded
|
||||
"""
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
@ -746,7 +760,9 @@ class DecisionsProposeesAnnee(DecisionsProposees):
|
||||
return True
|
||||
|
||||
def record_autorisation_inscription(self, code: str):
|
||||
"""Autorisation d'inscription dans semestre suivant"""
|
||||
"""Autorisation d'inscription dans semestre suivant.
|
||||
code: code jury sur année BUT
|
||||
"""
|
||||
if self.autorisations_recorded:
|
||||
return
|
||||
if self.inscription_etat != scu.INSCRIT:
|
||||
|
@ -154,7 +154,7 @@ def pvjury_table_but(
|
||||
"_nom_target_attrs": f'class="etudinfo" id="{etud.id}"',
|
||||
"_nom_td_attrs": f'id="{etud.id}" class="etudinfo"',
|
||||
"_nom_target": url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
),
|
||||
|
@ -447,7 +447,7 @@ def jury_but_semestriel(
|
||||
<div class="nom_etud">{etud.nomprenom}</div>
|
||||
</div>
|
||||
<div class="bull_photo"><a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -234,7 +234,7 @@ class ResultatsSemestreClassic(NotesTableCompat):
|
||||
raise ScoValueError(
|
||||
f"""<div class="scovalueerror"><p>Coefficient de l'UE capitalisée {ue.acronyme}
|
||||
impossible à déterminer pour l'étudiant <a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" class="discretelink">{etud.nom_disp()}</a></p>
|
||||
<p>Il faut <a href="{
|
||||
url_for("notes.formsemestre_edit_uecoefs", scodoc_dept=g.scodoc_dept,
|
||||
|
@ -32,6 +32,7 @@ Formulaire ajout d'un justificatif sur un étudiant
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import MultipleFileField
|
||||
from wtforms import (
|
||||
BooleanField,
|
||||
SelectField,
|
||||
StringField,
|
||||
SubmitField,
|
||||
@ -136,6 +137,7 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
|
||||
"Module",
|
||||
choices={}, # will be populated dynamically
|
||||
)
|
||||
est_just = BooleanField("Justifiée")
|
||||
|
||||
|
||||
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
|
||||
|
@ -34,52 +34,11 @@ import re
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import DecimalField, SubmitField, ValidationError
|
||||
from wtforms.fields.simple import StringField
|
||||
from wtforms.validators import Optional
|
||||
from wtforms.validators import Optional, Length
|
||||
|
||||
from wtforms.widgets import TimeInput
|
||||
|
||||
|
||||
class TimeField(StringField):
|
||||
"""HTML5 time input.
|
||||
tiré de : https://gist.github.com/tachyondecay/6016d32f65a996d0d94f
|
||||
"""
|
||||
|
||||
widget = TimeInput()
|
||||
|
||||
def __init__(self, label=None, validators=None, fmt="%H:%M:%S", **kwargs):
|
||||
super(TimeField, self).__init__(label, validators, **kwargs)
|
||||
self.fmt = fmt
|
||||
self.data = None
|
||||
|
||||
def _value(self):
|
||||
if self.raw_data:
|
||||
return " ".join(self.raw_data)
|
||||
if self.data and isinstance(self.data, str):
|
||||
self.data = datetime.time(*map(int, self.data.split(":")))
|
||||
return self.data and self.data.strftime(self.fmt) or ""
|
||||
|
||||
def process_formdata(self, valuelist):
|
||||
if valuelist:
|
||||
time_str = " ".join(valuelist)
|
||||
try:
|
||||
components = time_str.split(":")
|
||||
hour = 0
|
||||
minutes = 0
|
||||
seconds = 0
|
||||
if len(components) in range(2, 4):
|
||||
hour = int(components[0])
|
||||
minutes = int(components[1])
|
||||
|
||||
if len(components) == 3:
|
||||
seconds = int(components[2])
|
||||
else:
|
||||
raise ValueError
|
||||
self.data = datetime.time(hour, minutes, seconds)
|
||||
except ValueError as exc:
|
||||
self.data = None
|
||||
raise ValueError(self.gettext("Not a valid time string")) from exc
|
||||
|
||||
|
||||
def check_tick_time(form, field):
|
||||
"""Le tick_time doit être entre 0 et 60 minutes"""
|
||||
if field.data < 1 or field.data > 59:
|
||||
@ -118,14 +77,36 @@ def check_ics_regexp(form, field):
|
||||
|
||||
class ConfigAssiduitesForm(FlaskForm):
|
||||
"Formulaire paramétrage Module Assiduité"
|
||||
|
||||
assi_morning_time = TimeField(
|
||||
"Début de la journée"
|
||||
) # TODO utiliser TextField + timepicker voir AjoutAssiOrJustForm
|
||||
assi_lunch_time = TimeField(
|
||||
"Heure de midi (date pivot entre matin et après-midi)"
|
||||
) # TODO
|
||||
assi_afternoon_time = TimeField("Fin de la journée") # TODO
|
||||
assi_morning_time = StringField(
|
||||
"Début de la journée",
|
||||
default="",
|
||||
validators=[Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_morning_time",
|
||||
},
|
||||
)
|
||||
assi_lunch_time = StringField(
|
||||
"Heure de midi (date pivot entre matin et après-midi)",
|
||||
default="",
|
||||
validators=[Length(max=5)],
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_lunch_time",
|
||||
},
|
||||
)
|
||||
assi_afternoon_time = StringField(
|
||||
"Fin de la journée",
|
||||
validators=[Length(max=5)],
|
||||
default="",
|
||||
render_kw={
|
||||
"class": "timepicker",
|
||||
"size": 5,
|
||||
"id": "assi_afternoon_time",
|
||||
},
|
||||
)
|
||||
|
||||
assi_tick_time = DecimalField(
|
||||
"Granularité de la timeline (temps en minutes)",
|
||||
|
49
app/forms/main/config_rgpd.py
Normal file
49
app/forms/main/config_rgpd.py
Normal file
@ -0,0 +1,49 @@
|
||||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaire configuration RGPD
|
||||
"""
|
||||
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField
|
||||
from wtforms.fields.simple import TextAreaField
|
||||
|
||||
|
||||
class ConfigRGPDForm(FlaskForm):
|
||||
"Formulaire paramétrage RGPD"
|
||||
rgpd_coordonnees_dpo = TextAreaField(
|
||||
label="Optionnel: coordonnées du DPO",
|
||||
description="""Le délégué à la protection des données (DPO) est chargé de mettre en œuvre
|
||||
la conformité au règlement européen sur la protection des données (RGPD) au sein de l’organisme.
|
||||
Indiquer ici les coordonnées (format libre) qui seront affichées aux utilisateurs de ScoDoc.
|
||||
""",
|
||||
render_kw={"rows": 5, "cols": 72},
|
||||
)
|
||||
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
@ -2,6 +2,7 @@
|
||||
"""Gestion de l'assiduité (assiduités + justificatifs)
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy.exc import DataError
|
||||
@ -88,8 +89,10 @@ class Assiduite(ScoDocModel):
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
def to_dict(self, format_api=True) -> dict:
|
||||
"""Retourne la représentation json de l'assiduité"""
|
||||
def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
|
||||
"""Retourne la représentation json de l'assiduité
|
||||
restrict n'est pas utilisé ici.
|
||||
"""
|
||||
etat = self.etat
|
||||
user: User | None = None
|
||||
if format_api:
|
||||
@ -252,43 +255,19 @@ class Assiduite(ScoDocModel):
|
||||
|
||||
def set_moduleimpl(self, moduleimpl_id: int | str):
|
||||
"""Mise à jour du moduleimpl_id
|
||||
Les valeurs du champs "moduleimpl_id" possibles sont :
|
||||
Les valeurs du champ "moduleimpl_id" possibles sont :
|
||||
- <int> (un id classique)
|
||||
- <str> ("autre" ou "<id>")
|
||||
- None (pas de moduleimpl_id)
|
||||
- "" (pas de moduleimpl_id)
|
||||
Si la valeur est "autre" il faut:
|
||||
- mettre à None assiduité.moduleimpl_id
|
||||
- mettre à jour assiduite.external_data["module"] = "autre"
|
||||
En fonction de la configuration du semestre la valeur `None` peut-être considérée comme invalide.
|
||||
En fonction de la configuration du semestre (option force_module) la valeur "" peut-être
|
||||
considérée comme invalide.
|
||||
- Il faudra donc vérifier que ce n'est pas le cas avant de mettre à jour l'assiduité
|
||||
"""
|
||||
moduleimpl: ModuleImpl = None
|
||||
try:
|
||||
# ne lève une erreur que si moduleimpl_id est une chaine de caractère non parsable (parseInt)
|
||||
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
# moduleImpl est soit :
|
||||
# - None si moduleimpl_id==None
|
||||
# - None si moduleimpl_id==<int> non reconnu
|
||||
# - ModuleImpl si <int|str> valide
|
||||
|
||||
# Vérification ModuleImpl not None (raise ScoValueError)
|
||||
if moduleimpl is None and self._check_force_module(moduleimpl):
|
||||
# Ici uniquement si on est autorisé à ne pas avoir de module
|
||||
self.moduleimpl_id = None
|
||||
return
|
||||
|
||||
# Vérification Inscription ModuleImpl (raise ScoValueError)
|
||||
if moduleimpl.est_inscrit(self.etudiant):
|
||||
self.moduleimpl_id = moduleimpl.id
|
||||
else:
|
||||
raise ScoValueError("L'étudiant n'est pas inscrit au module")
|
||||
|
||||
except DataError:
|
||||
# On arrive ici si moduleimpl_id == "autre" ou moduleimpl_id == <str> non parsé
|
||||
|
||||
if moduleimpl_id != "autre":
|
||||
raise ScoValueError("Module non reconnu")
|
||||
|
||||
if moduleimpl_id == "autre":
|
||||
# Configuration de external_data pour Module Autre
|
||||
# Si self.external_data None alors on créé un dictionnaire {"module": "autre"}
|
||||
# Sinon on met à jour external_data["module"] à "autre"
|
||||
@ -302,6 +281,29 @@ class Assiduite(ScoDocModel):
|
||||
self.moduleimpl_id = None
|
||||
|
||||
# Ici pas de vérification du force module car on l'a mis dans "external_data"
|
||||
return
|
||||
|
||||
if moduleimpl_id != "":
|
||||
try:
|
||||
moduleimpl_id = int(moduleimpl_id)
|
||||
except ValueError as exc:
|
||||
raise ScoValueError("Module non reconnu") from exc
|
||||
moduleimpl: ModuleImpl = ModuleImpl.query.get(moduleimpl_id)
|
||||
|
||||
# ici moduleimpl est None si non spécifié
|
||||
|
||||
# Vérification ModuleImpl not None (raise ScoValueError)
|
||||
if moduleimpl is None:
|
||||
self._check_force_module()
|
||||
# Ici uniquement si on est autorisé à ne pas avoir de module
|
||||
self.moduleimpl_id = None
|
||||
return
|
||||
|
||||
# Vérification Inscription ModuleImpl (raise ScoValueError)
|
||||
if moduleimpl.est_inscrit(self.etudiant):
|
||||
self.moduleimpl_id = moduleimpl.id
|
||||
else:
|
||||
raise ScoValueError("L'étudiant n'est pas inscrit au module")
|
||||
|
||||
def supprime(self):
|
||||
"Supprime l'assiduité. Log et commit."
|
||||
@ -331,7 +333,7 @@ class Assiduite(ScoDocModel):
|
||||
return get_formsemestre_from_data(self.to_dict())
|
||||
|
||||
def get_module(self, traduire: bool = False) -> int | str:
|
||||
"TODO"
|
||||
"TODO documenter"
|
||||
if self.moduleimpl_id is not None:
|
||||
if traduire:
|
||||
modimpl: ModuleImpl = ModuleImpl.query.get(self.moduleimpl_id)
|
||||
@ -360,8 +362,12 @@ class Assiduite(ScoDocModel):
|
||||
|
||||
return f"saisie le {date} {utilisateur}"
|
||||
|
||||
def _check_force_module(self, moduleimpl: ModuleImpl) -> bool:
|
||||
# Vérification si module forcé
|
||||
def _check_force_module(self):
|
||||
"""Vérification si module forcé:
|
||||
Si le module est requis, raise ScoValueError
|
||||
sinon ne fait rien.
|
||||
"""
|
||||
# cherche le formsemestre affecté pour utiliser ses préférences
|
||||
formsemestre: FormSemestre = get_formsemestre_from_data(
|
||||
{
|
||||
"etudid": self.etudid,
|
||||
@ -369,18 +375,15 @@ class Assiduite(ScoDocModel):
|
||||
"date_fin": self.date_fin,
|
||||
}
|
||||
)
|
||||
force: bool
|
||||
|
||||
if formsemestre:
|
||||
force = is_assiduites_module_forced(formsemestre_id=formsemestre.id)
|
||||
else:
|
||||
force = is_assiduites_module_forced(dept_id=self.etudiant.dept_id)
|
||||
|
||||
formsemestre_id = formsemestre.id if formsemestre else None
|
||||
# si pas de formsemestre, utilisera les prefs globales du département
|
||||
dept_id = self.etudiant.dept_id
|
||||
force = is_assiduites_module_forced(
|
||||
formsemestre_id=formsemestre_id, dept_id=dept_id
|
||||
)
|
||||
if force:
|
||||
raise ScoValueError("Module non renseigné")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class Justificatif(ScoDocModel):
|
||||
"""
|
||||
@ -434,6 +437,14 @@ class Justificatif(ScoDocModel):
|
||||
etudiant = db.relationship(
|
||||
"Identite", back_populates="justificatifs", lazy="joined"
|
||||
)
|
||||
# En revanche, user est rarement accédé:
|
||||
user = db.relationship(
|
||||
"User",
|
||||
backref=db.backref(
|
||||
"justificatifs", lazy="select", order_by="Justificatif.entry_date"
|
||||
),
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
external_data = db.Column(db.JSON, nullable=True)
|
||||
|
||||
@ -445,20 +456,16 @@ class Justificatif(ScoDocModel):
|
||||
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
|
||||
return query.first_or_404()
|
||||
|
||||
def to_dict(self, format_api: bool = False) -> dict:
|
||||
"""transformation de l'objet en dictionnaire sérialisable"""
|
||||
def to_dict(self, format_api: bool = False, restrict: bool = False) -> dict:
|
||||
"""L'objet en dictionnaire sérialisable.
|
||||
Si restrict, ne donne par la raison et les fichiers et external_data
|
||||
"""
|
||||
|
||||
etat = self.etat
|
||||
username = self.user_id
|
||||
user: User = self.user if self.user_id is not None else None
|
||||
|
||||
if format_api:
|
||||
etat = EtatJustificatif.inverse().get(self.etat).name
|
||||
if self.user_id is not None:
|
||||
user: User = db.session.get(User, self.user_id)
|
||||
if user is None:
|
||||
username = "Non renseigné"
|
||||
else:
|
||||
username = user.get_prenomnom()
|
||||
|
||||
data = {
|
||||
"justif_id": self.justif_id,
|
||||
@ -467,11 +474,13 @@ class Justificatif(ScoDocModel):
|
||||
"date_debut": self.date_debut,
|
||||
"date_fin": self.date_fin,
|
||||
"etat": etat,
|
||||
"raison": self.raison,
|
||||
"fichier": self.fichier,
|
||||
"raison": None if restrict else self.raison,
|
||||
"fichier": None if restrict else self.fichier,
|
||||
"entry_date": self.entry_date,
|
||||
"user_id": username,
|
||||
"external_data": self.external_data,
|
||||
"user_id": None if user is None else user.id, # l'uid
|
||||
"user_name": None if user is None else user.user_name, # le login
|
||||
"user_nom_complet": None if user is None else user.get_nomcomplet(),
|
||||
"external_data": None if restrict else self.external_data,
|
||||
}
|
||||
return data
|
||||
|
||||
@ -618,6 +627,12 @@ def compute_assiduites_justified(
|
||||
Returns:
|
||||
list[int]: la liste des assiduités qui ont été justifiées.
|
||||
"""
|
||||
# TODO à optimiser (car très long avec 40000 assiduités)
|
||||
# On devrait :
|
||||
# - récupérer uniquement les assiduités qui sont sur la période des justificatifs donnés
|
||||
# - Pour chaque assiduité trouvée, il faut récupérer les justificatifs qui la justifie
|
||||
# - Si au moins un justificatif valide couvre la période de l'assiduité alors on la justifie
|
||||
|
||||
# Si on ne donne pas de justificatifs on prendra par défaut tous les justificatifs de l'étudiant
|
||||
if justificatifs is None:
|
||||
justificatifs: list[Justificatif] = Justificatif.query.filter_by(
|
||||
|
@ -119,6 +119,9 @@ class Identite(models.ScoDocModel):
|
||||
"Justificatif", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
||||
)
|
||||
|
||||
# Champs "protégés" par ViewEtudData (RGPD)
|
||||
protected_attrs = {"boursier"}
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<Etud {self.id}/{self.departement.acronym} {self.nom!r} {self.prenom!r}>"
|
||||
@ -176,7 +179,7 @@ class Identite(models.ScoDocModel):
|
||||
def url_fiche(self) -> str:
|
||||
"url de la fiche étudiant"
|
||||
return url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id
|
||||
"scolar.fiche_etud", scodoc_dept=self.departement.acronym, etudid=self.id
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ -418,7 +421,7 @@ class Identite(models.ScoDocModel):
|
||||
return args_dict
|
||||
|
||||
def to_dict_short(self) -> dict:
|
||||
"""Les champs essentiels"""
|
||||
"""Les champs essentiels (aucune donnée perso protégée)"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"civilite": self.civilite,
|
||||
@ -433,9 +436,10 @@ class Identite(models.ScoDocModel):
|
||||
"prenom_etat_civil": self.prenom_etat_civil,
|
||||
}
|
||||
|
||||
def to_dict_scodoc7(self) -> dict:
|
||||
def to_dict_scodoc7(self, restrict=False) -> dict:
|
||||
"""Représentation dictionnaire,
|
||||
compatible ScoDoc7 mais sans infos admission
|
||||
compatible ScoDoc7 mais sans infos admission.
|
||||
Si restrict, cache les infos "personnelles" si pas permission ViewEtudData
|
||||
"""
|
||||
e_dict = self.__dict__.copy() # dict(self.__dict__)
|
||||
e_dict.pop("_sa_instance_state", None)
|
||||
@ -446,7 +450,7 @@ class Identite(models.ScoDocModel):
|
||||
e_dict["nomprenom"] = self.nomprenom
|
||||
adresse = self.adresses.first()
|
||||
if adresse:
|
||||
e_dict.update(adresse.to_dict())
|
||||
e_dict.update(adresse.to_dict(restrict=restrict))
|
||||
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
|
||||
|
||||
def to_dict_bul(self, include_urls=True):
|
||||
@ -481,7 +485,7 @@ class Identite(models.ScoDocModel):
|
||||
if include_urls and has_request_context():
|
||||
# test request context so we can use this func in tests under the flask shell
|
||||
d["fiche_url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=self.id
|
||||
)
|
||||
d["photo_url"] = sco_photos.get_etud_photo_url(self.id)
|
||||
adresse = self.adresses.first()
|
||||
@ -490,16 +494,22 @@ class Identite(models.ScoDocModel):
|
||||
d["id"] = self.id # a été écrasé par l'id de adresse
|
||||
return d
|
||||
|
||||
def to_dict_api(self) -> dict:
|
||||
"""Représentation dictionnaire pour export API, avec adresses et admission."""
|
||||
def to_dict_api(self, restrict=False) -> dict:
|
||||
"""Représentation dictionnaire pour export API, avec adresses et admission.
|
||||
Si restrict, supprime les infos "personnelles" (boursier)
|
||||
"""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
admission = self.admission
|
||||
e["admission"] = admission.to_dict() if admission is not None else None
|
||||
e["adresses"] = [adr.to_dict() for adr in self.adresses]
|
||||
e["adresses"] = [adr.to_dict(restrict=restrict) for adr in self.adresses]
|
||||
e["dept_acronym"] = self.departement.acronym
|
||||
e.pop("departement", None)
|
||||
e["sort_key"] = self.sort_key
|
||||
if restrict:
|
||||
# Met à None les attributs protégés:
|
||||
for attr in self.protected_attrs:
|
||||
e[attr] = None
|
||||
return e
|
||||
|
||||
def inscriptions(self) -> list["FormSemestreInscription"]:
|
||||
@ -825,12 +835,25 @@ class Adresse(models.ScoDocModel):
|
||||
)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
def to_dict(self, convert_nulls_to_str=False):
|
||||
"""Représentation dictionnaire,"""
|
||||
# Champs "protégés" par ViewEtudData (RGPD)
|
||||
protected_attrs = {
|
||||
"emailperso",
|
||||
"domicile",
|
||||
"codepostaldomicile",
|
||||
"villedomicile",
|
||||
"telephone",
|
||||
"telephonemobile",
|
||||
"fax",
|
||||
}
|
||||
|
||||
def to_dict(self, convert_nulls_to_str=False, restrict=False):
|
||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
||||
e = dict(self.__dict__)
|
||||
e.pop("_sa_instance_state", None)
|
||||
if convert_nulls_to_str:
|
||||
return {k: e[k] or "" for k in e}
|
||||
e = {k: v or "" for k, v in e.items()}
|
||||
if restrict:
|
||||
e = {k: v for (k, v) in e.items() if k not in self.protected_attrs}
|
||||
return e
|
||||
|
||||
|
||||
@ -885,12 +908,16 @@ class Admission(models.ScoDocModel):
|
||||
# classement (1..Ngr) par le jury dans le groupe APB
|
||||
apb_classement_gr = db.Column(db.Integer)
|
||||
|
||||
# Tous les champs sont "protégés" par ViewEtudData (RGPD)
|
||||
# sauf:
|
||||
not_protected_attrs = {"bac", "specialite", "anne_bac"}
|
||||
|
||||
def get_bac(self) -> Baccalaureat:
|
||||
"Le bac. utiliser bac.abbrev() pour avoir une chaine de caractères."
|
||||
return Baccalaureat(self.bac, specialite=self.specialite)
|
||||
|
||||
def to_dict(self, no_nulls=False):
|
||||
"""Représentation dictionnaire,"""
|
||||
def to_dict(self, no_nulls=False, restrict=False):
|
||||
"""Représentation dictionnaire. Si restrict, filtre les champs protégés (RGPD)."""
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
if no_nulls:
|
||||
@ -905,6 +932,8 @@ class Admission(models.ScoDocModel):
|
||||
d[key] = 0
|
||||
elif isinstance(col_type, sqlalchemy.Boolean):
|
||||
d[key] = False
|
||||
if restrict:
|
||||
d = {k: v for (k, v) in d.items() if k in self.not_protected_attrs}
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
|
@ -184,7 +184,7 @@ class Evaluation(db.Model):
|
||||
# ScoDoc7 output_formators
|
||||
e_dict["evaluation_id"] = self.id
|
||||
e_dict["date_debut"] = self.date_debut.isoformat() if self.date_debut else None
|
||||
e_dict["date_fin"] = self.date_debut.isoformat() if self.date_fin else None
|
||||
e_dict["date_fin"] = self.date_fin.isoformat() if self.date_fin else None
|
||||
e_dict["numero"] = self.numero or 0
|
||||
e_dict["poids"] = self.get_ue_poids_dict() # { ue_id : poids }
|
||||
|
||||
@ -428,8 +428,8 @@ class Evaluation(db.Model):
|
||||
|
||||
def get_ue_poids_str(self) -> str:
|
||||
"""string describing poids, for excel cells and pdfs
|
||||
Note: si les poids ne sont pas initialisés (poids par défaut),
|
||||
ils ne sont pas affichés.
|
||||
Note: les poids nuls ou non initialisés (poids par défaut),
|
||||
ne sont pas affichés.
|
||||
"""
|
||||
# restreint aux UE du semestre dans lequel est cette évaluation
|
||||
# au cas où le module ait changé de semestre et qu'il reste des poids
|
||||
@ -440,7 +440,7 @@ class Evaluation(db.Model):
|
||||
for p in sorted(
|
||||
self.ue_poids, key=lambda p: (p.ue.numero or 0, p.ue.acronyme)
|
||||
)
|
||||
if evaluation_semestre_idx == p.ue.semestre_idx
|
||||
if evaluation_semestre_idx == p.ue.semestre_idx and (p.poids or 0) > 0
|
||||
]
|
||||
)
|
||||
|
||||
@ -584,20 +584,10 @@ def check_convert_evaluation_args(moduleimpl: "ModuleImpl", data: dict):
|
||||
if date_debut and date_fin:
|
||||
duration = data["date_fin"] - data["date_debut"]
|
||||
if duration.total_seconds() < 0 or duration > MAX_EVALUATION_DURATION:
|
||||
raise ScoValueError("Heures de l'évaluation incohérentes !")
|
||||
# # --- heures
|
||||
# heure_debut = data.get("heure_debut", None)
|
||||
# if heure_debut and not isinstance(heure_debut, datetime.time):
|
||||
# if date_format == "dmy":
|
||||
# data["heure_debut"] = heure_to_time(heure_debut)
|
||||
# else: # ISO
|
||||
# data["heure_debut"] = datetime.time.fromisoformat(heure_debut)
|
||||
# heure_fin = data.get("heure_fin", None)
|
||||
# if heure_fin and not isinstance(heure_fin, datetime.time):
|
||||
# if date_format == "dmy":
|
||||
# data["heure_fin"] = heure_to_time(heure_fin)
|
||||
# else: # ISO
|
||||
# data["heure_fin"] = datetime.time.fromisoformat(heure_fin)
|
||||
raise ScoValueError(
|
||||
"Heures de l'évaluation incohérentes !",
|
||||
dest_url="javascript:history.back();",
|
||||
)
|
||||
|
||||
|
||||
def heure_to_time(heure: str) -> datetime.time:
|
||||
|
@ -187,7 +187,7 @@ class FormSemestre(db.Model):
|
||||
def get_formsemestre(
|
||||
cls, formsemestre_id: int | str, dept_id: int = None
|
||||
) -> "FormSemestre":
|
||||
""" "FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
|
||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant"""
|
||||
if not isinstance(formsemestre_id, int):
|
||||
try:
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
|
@ -2,6 +2,7 @@
|
||||
"""ScoDoc models: moduleimpls
|
||||
"""
|
||||
import pandas as pd
|
||||
from flask import abort, g
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import db
|
||||
@ -82,6 +83,23 @@ class ModuleImpl(db.Model):
|
||||
df_cache.EvaluationsPoidsCache.set(self.id, evaluations_poids)
|
||||
return evaluations_poids
|
||||
|
||||
@classmethod
|
||||
def get_modimpl(cls, moduleimpl_id: int | str, dept_id: int = None) -> "ModuleImpl":
|
||||
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant."""
|
||||
from app.models.formsemestre import FormSemestre
|
||||
|
||||
if not isinstance(moduleimpl_id, int):
|
||||
try:
|
||||
moduleimpl_id = int(moduleimpl_id)
|
||||
except (TypeError, ValueError):
|
||||
abort(404, "moduleimpl_id invalide")
|
||||
if g.scodoc_dept:
|
||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
||||
query = cls.query.filter_by(id=moduleimpl_id)
|
||||
if dept_id is not None:
|
||||
query = query.join(FormSemestre).filter_by(dept_id=dept_id)
|
||||
return query.first_or_404()
|
||||
|
||||
def invalidate_evaluations_poids(self):
|
||||
"""Invalide poids cachés"""
|
||||
df_cache.EvaluationsPoidsCache.delete(self.id)
|
||||
|
1274
app/pe/pe_jurype.py
Normal file
1274
app/pe/pe_jurype.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -685,6 +685,11 @@ class TF(object):
|
||||
'<input type="text" name="%s" size="10" value="%s" class="datepicker">'
|
||||
% (field, values[field])
|
||||
)
|
||||
elif input_type == "time": # JavaScript widget for date input
|
||||
lem.append(
|
||||
f"""<input type="text" name="{field}" maxlength="5" size="5" value="{
|
||||
values[field]}" class="timepicker">"""
|
||||
)
|
||||
elif input_type == "text_suggest":
|
||||
lem.append(
|
||||
'<input type="text" name="%s" id="%s" size="%d" %s'
|
||||
|
@ -145,7 +145,9 @@ def sco_header(
|
||||
etudid=None,
|
||||
formsemestre_id=None,
|
||||
):
|
||||
"Main HTML page header for ScoDoc"
|
||||
"""Main HTML page header for ScoDoc
|
||||
Utilisé dans les anciennes pages. Les nouvelles pages utilisent le template Jinja.
|
||||
"""
|
||||
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
|
||||
|
||||
if etudid is not None:
|
||||
@ -189,7 +191,12 @@ def sco_header(
|
||||
# jQuery UI
|
||||
# can modify loaded theme here
|
||||
H.append(
|
||||
f'<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />\n'
|
||||
f"""
|
||||
<link type="text/css" rel="stylesheet"
|
||||
href="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/css/smoothness/jquery-ui-1.10.4.custom.min.css" />
|
||||
<link type="text/css" rel="stylesheet"
|
||||
href="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.css" />
|
||||
"""
|
||||
)
|
||||
if init_google_maps:
|
||||
# It may be necessary to add an API key:
|
||||
@ -219,19 +226,26 @@ def sco_header(
|
||||
|
||||
# jQuery
|
||||
H.append(
|
||||
f"""<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>"""
|
||||
f"""
|
||||
<script src="{scu.STATIC_DIR}/jQuery/jquery.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
|
||||
"""
|
||||
)
|
||||
# qTip
|
||||
if init_qtip:
|
||||
H.append(
|
||||
f"""<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
|
||||
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />"""
|
||||
<link type="text/css" rel="stylesheet"
|
||||
href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />
|
||||
"""
|
||||
)
|
||||
|
||||
H.append(
|
||||
f"""<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>"""
|
||||
f"""<script
|
||||
src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
|
||||
<script src="{scu.STATIC_DIR}/js/scodoc.js"></script>
|
||||
"""
|
||||
)
|
||||
if init_google_maps:
|
||||
H.append(
|
||||
|
@ -32,12 +32,68 @@ from flask import render_template, url_for
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.models import Evaluation, GroupDescr, ModuleImpl, Partition
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from sco_version import SCOVERSION
|
||||
|
||||
|
||||
def retreive_formsemestre_from_request() -> int:
|
||||
"""Cherche si on a de quoi déduire le semestre affiché à partir des
|
||||
arguments de la requête:
|
||||
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
|
||||
Returns None si pas défini.
|
||||
"""
|
||||
if request.method == "GET":
|
||||
args = request.args
|
||||
elif request.method == "POST":
|
||||
args = request.form
|
||||
else:
|
||||
return None
|
||||
formsemestre_id = None
|
||||
# Search formsemestre
|
||||
group_ids = args.get("group_ids", [])
|
||||
if "formsemestre_id" in args:
|
||||
formsemestre_id = args["formsemestre_id"]
|
||||
elif "moduleimpl_id" in args and args["moduleimpl_id"]:
|
||||
modimpl = db.session.get(ModuleImpl, args["moduleimpl_id"])
|
||||
if not modimpl:
|
||||
return None # suppressed ?
|
||||
formsemestre_id = modimpl.formsemestre_id
|
||||
elif "evaluation_id" in args:
|
||||
evaluation = db.session.get(Evaluation, args["evaluation_id"])
|
||||
if not evaluation:
|
||||
return None # evaluation suppressed ?
|
||||
formsemestre_id = evaluation.moduleimpl.formsemestre_id
|
||||
elif "group_id" in args:
|
||||
group = db.session.get(GroupDescr, args["group_id"])
|
||||
if not group:
|
||||
return None
|
||||
formsemestre_id = group.partition.formsemestre_id
|
||||
elif group_ids:
|
||||
if isinstance(group_ids, str):
|
||||
group_ids = group_ids.split(",")
|
||||
group_id = group_ids[0]
|
||||
group = db.session.get(GroupDescr, group_id)
|
||||
if not group:
|
||||
return None
|
||||
formsemestre_id = group.partition.formsemestre_id
|
||||
elif "partition_id" in args:
|
||||
partition = db.session.get(Partition, args["partition_id"])
|
||||
if not partition:
|
||||
return None
|
||||
formsemestre_id = partition.formsemestre_id
|
||||
|
||||
if formsemestre_id is None:
|
||||
return None # no current formsemestre
|
||||
try:
|
||||
return int(formsemestre_id)
|
||||
except ValueError:
|
||||
return None # no current formsemestre
|
||||
|
||||
|
||||
def sidebar_common():
|
||||
"partie commune à toutes les sidebar"
|
||||
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
|
||||
@ -107,7 +163,7 @@ def sidebar(etudid: int = None):
|
||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
||||
params.update(etud)
|
||||
params["fiche_url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
# compte les absences du semestre en cours
|
||||
H.append(
|
||||
@ -129,13 +185,17 @@ def sidebar(etudid: int = None):
|
||||
)
|
||||
H.append("<ul>")
|
||||
if current_user.has_permission(Permission.AbsChange):
|
||||
# essaie de conserver le semestre actuellement en vue
|
||||
cur_formsemestre_id = retreive_formsemestre_from_request()
|
||||
H.append(
|
||||
f"""
|
||||
<li><a href="{ url_for('assiduites.ajout_assiduite_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">Ajouter</a></li>
|
||||
<li><a href="{ url_for('assiduites.ajout_justificatif_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
||||
formsemestre_id=cur_formsemestre_id,
|
||||
)
|
||||
}">Justifier</a></li>
|
||||
"""
|
||||
)
|
||||
|
@ -129,7 +129,7 @@ def table_billets(
|
||||
] = f'id="{billet.etudiant.id}" class="etudinfo"'
|
||||
if with_links:
|
||||
billet_dict["_nomprenom_target"] = url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=billet_dict["etudid"],
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ Il suffit d'appeler abs_notify() après chaque ajout d'absence.
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from flask import current_app, g, url_for
|
||||
from flask import g, url_for
|
||||
from flask_mail import Message
|
||||
|
||||
from app import db
|
||||
@ -42,6 +42,7 @@ from app import email
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.models.absences import AbsenceNotification
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.events import Scolog
|
||||
from app.models.formsemestre import FormSemestre
|
||||
import app.scodoc.notesdb as ndb
|
||||
@ -108,7 +109,6 @@ def do_abs_notify(
|
||||
return # abort
|
||||
|
||||
# Vérification fréquence (pour ne pas envoyer de mails trop souvent)
|
||||
# TODO Mettre la fréquence dans les préférences assiduités
|
||||
abs_notify_max_freq = sco_preferences.get_preference("abs_notify_max_freq")
|
||||
destinations_filtered = []
|
||||
for email_addr in destinations:
|
||||
@ -175,9 +175,15 @@ def abs_notify_get_destinations(
|
||||
if prefs["abs_notify_email"]:
|
||||
destinations.append(prefs["abs_notify_email"])
|
||||
if prefs["abs_notify_etud"]:
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
if etud["email_default"]:
|
||||
destinations.append(etud["email_default"])
|
||||
etud = Identite.get_etud(etudid)
|
||||
adresse = etud.adresses.first()
|
||||
if adresse:
|
||||
# Mail à utiliser pour les envois vers l'étudiant:
|
||||
# choix qui pourrait être controlé par une preference
|
||||
# ici priorité au mail institutionnel:
|
||||
email_default = adresse.email or adresse.emailperso
|
||||
if email_default:
|
||||
destinations.append(email_default)
|
||||
|
||||
# Notification (à chaque fois) des resp. de modules ayant des évaluations
|
||||
# à cette date
|
||||
@ -271,7 +277,7 @@ def abs_notification_message(
|
||||
values["nbabsjust"] = nbabsjust
|
||||
values["nbabsnonjust"] = nbabs - nbabsjust
|
||||
values["url_ficheetud"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid, _external=True
|
||||
)
|
||||
|
||||
template = prefs["abs_notification_mail_tmpl"]
|
||||
|
@ -62,7 +62,7 @@ def can_edit_etud_archive(authuser):
|
||||
|
||||
|
||||
def etud_list_archives_html(etud: Identite):
|
||||
"""HTML snippet listing archives"""
|
||||
"""HTML snippet listing archives."""
|
||||
can_edit = can_edit_etud_archive(current_user)
|
||||
etud_archive_id = etud.id
|
||||
L = []
|
||||
@ -177,7 +177,7 @@ def etud_upload_file_form(etudid):
|
||||
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
data = tf[2]["datafile"].read()
|
||||
@ -188,7 +188,7 @@ def etud_upload_file_form(etudid):
|
||||
etud_archive_id, data, filename, description=descr
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
|
||||
|
||||
@ -228,7 +228,7 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
|
||||
),
|
||||
dest_url="",
|
||||
cancel_url=url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etudid,
|
||||
head_message="annulation",
|
||||
@ -239,7 +239,7 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
|
||||
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)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
|
||||
|
||||
|
@ -17,7 +17,15 @@ from app import log
|
||||
|
||||
class Trace:
|
||||
"""gestionnaire de la trace des fichiers justificatifs
|
||||
XXX TODO à documenter: rôle et format des fichier strace
|
||||
|
||||
Role des fichiers traces :
|
||||
- Sauvegarder la date de dépot du fichier
|
||||
- Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif)
|
||||
- Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView)
|
||||
|
||||
_trace.csv :
|
||||
nom_fichier_srv,datetime_depot,datetime_suppr,user_id
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, path: str) -> None:
|
||||
@ -39,7 +47,7 @@ class Trace:
|
||||
continue
|
||||
entry_date: datetime = is_iso_formated(csv[1], True)
|
||||
delete_date: datetime = is_iso_formated(csv[2], True)
|
||||
user_id = csv[3]
|
||||
user_id = csv[3].strip()
|
||||
self.content[fname] = [entry_date, delete_date, user_id]
|
||||
|
||||
if os.path.isfile(self.path):
|
||||
@ -84,7 +92,14 @@ class Trace:
|
||||
self, fnames: list[str] = None
|
||||
) -> dict[str, list[datetime, datetime, str]]:
|
||||
"""Récupère la trace pour les noms de fichiers.
|
||||
si aucun nom n'est donné, récupère tous les fichiers"""
|
||||
si aucun nom n'est donné, récupère tous les fichiers
|
||||
|
||||
retour :
|
||||
{
|
||||
"nom_fichier_srv": [datetime_depot, datetime_suppr/None, user_id],
|
||||
...
|
||||
}
|
||||
"""
|
||||
|
||||
if fnames is None:
|
||||
return self.content
|
||||
@ -215,8 +230,7 @@ class JustificatifArchiver(BaseArchiver):
|
||||
filenames = self.list_archive(archive_id, dept_id=etud.dept_id)
|
||||
trace: Trace = Trace(archive_id)
|
||||
traced = trace.get_trace(filenames)
|
||||
|
||||
return [(key, value[2]) for key, value in traced.items()]
|
||||
return [(key, value[2]) for key, value in traced.items() if value is not None]
|
||||
|
||||
def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str):
|
||||
"""
|
||||
|
@ -6,7 +6,7 @@ from pytz import UTC
|
||||
|
||||
from flask_sqlalchemy.query import Query
|
||||
|
||||
from app import log, db
|
||||
from app import log, db, set_sco_dept
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.models.assiduites import Assiduite, Justificatif, compute_assiduites_justified
|
||||
from app.models.etudiants import Identite
|
||||
@ -170,7 +170,7 @@ class CountCalculator:
|
||||
"""Récupère une clé de dictionnaire en fonction de l'état de l'assiduité
|
||||
et si elle est justifié
|
||||
"""
|
||||
keys: dict[EtatAssiduite, str] = {
|
||||
keys: dict[scu.EtatAssiduite, str] = {
|
||||
scu.EtatAssiduite.ABSENT: "absent",
|
||||
scu.EtatAssiduite.RETARD: "retard",
|
||||
scu.EtatAssiduite.PRESENT: "present",
|
||||
@ -349,6 +349,11 @@ def get_assiduites_stats(
|
||||
assiduites: Query, metric: str = "all", filtered: dict[str, object] = None
|
||||
) -> dict[str, int | float]:
|
||||
"""Compte les assiduités en fonction des filtres"""
|
||||
# XXX TODO-assiduite : documenter !!!
|
||||
# Que sont les filtres ? Quelles valeurs ?
|
||||
# documenter permet de faire moins de bug: qualité du code non satisfaisante.
|
||||
#
|
||||
# + on se perd entre les clés en majuscules et en minuscules. Pourquoi
|
||||
|
||||
if filtered is not None:
|
||||
deb, fin = None, None
|
||||
@ -390,17 +395,18 @@ def get_assiduites_stats(
|
||||
|
||||
# Récupération des états
|
||||
etats: list[str] = (
|
||||
filtered["etat"].split(",")
|
||||
if "etat" in filtered
|
||||
else ["absent", "present", "retard"]
|
||||
filtered["etat"].split(",") if "etat" in filtered else scu.EtatAssiduite.all()
|
||||
)
|
||||
|
||||
# être sur que les états sont corrects
|
||||
etats = [etat for etat in etats if etat in ["absent", "present", "retard"]]
|
||||
etats = [etat for etat in etats if etat.upper() in scu.EtatAssiduite.all()]
|
||||
|
||||
# Préparation du dictionnaire de retour avec les valeurs du calcul
|
||||
count: dict = calculator.to_dict(only_total=False)
|
||||
for etat in etats:
|
||||
# TODO-assiduite: on se perd entre les lower et upper.
|
||||
# Pourquoi EtatAssiduite est en majuscules si tout le reste est en minuscules ?
|
||||
etat = etat.lower()
|
||||
if etat != "present":
|
||||
output[etat] = count[etat]
|
||||
output[etat]["justifie"] = count[etat + "_just"]
|
||||
@ -452,8 +458,6 @@ def filter_by_date(
|
||||
if date_fin is None:
|
||||
date_fin = datetime.max
|
||||
|
||||
date_deb = scu.localize_datetime(date_deb) # TODO A modifier (timezone ?)
|
||||
date_fin = scu.localize_datetime(date_fin)
|
||||
if not strict:
|
||||
return collection.filter(
|
||||
collection_cls.date_debut <= date_fin, collection_cls.date_fin >= date_deb
|
||||
@ -560,15 +564,19 @@ def get_all_justified(
|
||||
return after
|
||||
|
||||
|
||||
def create_absence(
|
||||
def create_absence_billet(
|
||||
date_debut: datetime,
|
||||
date_fin: datetime,
|
||||
etudid: int,
|
||||
description: str = None,
|
||||
est_just: bool = False,
|
||||
) -> int:
|
||||
"""TODO: doc, dire quand l'utiliser"""
|
||||
# TODO
|
||||
"""
|
||||
Permet de rapidement créer une absence.
|
||||
**UTILISÉ UNIQUEMENT POUR LES BILLETS**
|
||||
Ne pas utiliser autre par.
|
||||
TALK: Vérifier si nécessaire
|
||||
"""
|
||||
etud: Identite = Identite.query.filter_by(etudid=etudid).first_or_404()
|
||||
assiduite_unique: Assiduite = Assiduite.create_assiduite(
|
||||
etud=etud,
|
||||
@ -650,8 +658,7 @@ def get_assiduites_count_in_interval(
|
||||
"""
|
||||
date_debut_iso = date_debut_iso or date_debut.isoformat()
|
||||
date_fin_iso = date_fin_iso or date_fin.isoformat()
|
||||
# TODO Question: pourquoi ne pas cacher toutes les métriques, si l'API les veut toutes ?
|
||||
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}{metrique}_assiduites"
|
||||
key = f"{etudid}_{date_debut_iso}_{date_fin_iso}_assiduites"
|
||||
|
||||
r = sco_cache.AbsSemEtudCache.get(key)
|
||||
if not r or moduleimpl_id is not None:
|
||||
@ -668,26 +675,27 @@ def get_assiduites_count_in_interval(
|
||||
calculator: CountCalculator = CountCalculator()
|
||||
calculator.compute_assiduites(assiduites)
|
||||
calcul: dict = calculator.to_dict(only_total=False)
|
||||
nb_abs: dict = calcul["absent"][metrique]
|
||||
nb_abs_just: dict = calcul["absent_just"][metrique]
|
||||
|
||||
r = (nb_abs, nb_abs_just)
|
||||
r = calcul
|
||||
if moduleimpl_id is None:
|
||||
ans = sco_cache.AbsSemEtudCache.set(key, r)
|
||||
if not ans:
|
||||
log("warning: get_assiduites_count failed to cache")
|
||||
return r
|
||||
|
||||
nb_abs: dict = r["absent"][metrique]
|
||||
nb_abs_just: dict = r["absent_just"][metrique]
|
||||
return (nb_abs, nb_abs_just)
|
||||
|
||||
|
||||
def invalidate_assiduites_count(etudid: int, sem: dict):
|
||||
"""Invalidate (clear) cached counts"""
|
||||
date_debut = sem["date_debut_iso"]
|
||||
date_fin = sem["date_fin_iso"]
|
||||
for met in scu.AssiduitesMetrics.TAG:
|
||||
key = str(etudid) + "_" + date_debut + "_" + date_fin + f"{met}_assiduites"
|
||||
key = str(etudid) + "_" + date_debut + "_" + date_fin + "_assiduites"
|
||||
sco_cache.AbsSemEtudCache.delete(key)
|
||||
|
||||
|
||||
# Non utilisé
|
||||
def invalidate_assiduites_count_sem(sem: dict):
|
||||
"""Invalidate (clear) cached abs counts for all the students of this semestre"""
|
||||
inscriptions = (
|
||||
@ -756,3 +764,14 @@ def simple_invalidate_cache(obj: dict, etudid: str | int = None):
|
||||
etudid = etudid if etudid is not None else obj["etudid"]
|
||||
invalidate_assiduites_etud_date(etudid, date_debut)
|
||||
invalidate_assiduites_etud_date(etudid, date_fin)
|
||||
|
||||
# mettre à jour le scodoc_dept en fonction de l'étudiant
|
||||
etud = Identite.query.filter_by(etudid=etudid).first_or_404()
|
||||
set_sco_dept(etud.departement.acronym)
|
||||
|
||||
# Invalide les caches des tableaux de l'étudiant
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(
|
||||
pattern=f"tableau-etud-{etudid}*"
|
||||
)
|
||||
# Invalide les tableaux "bilan dept"
|
||||
sco_cache.RequeteTableauAssiduiteCache.delete_pattern(pattern=f"tableau-dept*")
|
||||
|
@ -60,6 +60,7 @@ import traceback
|
||||
from flask import g, request
|
||||
|
||||
from app import log, ScoValueError
|
||||
from app.comp.res_but import ResultatsSemestreBUT
|
||||
from app.models import FormSemestre, Identite
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import codes_cursus
|
||||
@ -318,14 +319,34 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
|
||||
return pdfdoc, filename
|
||||
|
||||
|
||||
def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str:
|
||||
"""Texte à placer en "filigranne" sur le bulletin pdf"""
|
||||
def get_filigranne(
|
||||
etud_etat: str, prefs, decision_sem: str | None | bool = None
|
||||
) -> str:
|
||||
"""Texte à placer en "filigranne" sur le bulletin pdf.
|
||||
etud_etat : etat de l'inscription (I ou D)
|
||||
decision_sem = code jury ou vide
|
||||
"""
|
||||
if etud_etat == scu.DEMISSION:
|
||||
return "Démission"
|
||||
elif etud_etat == codes_cursus.DEF:
|
||||
if etud_etat == codes_cursus.DEF:
|
||||
return "Défaillant"
|
||||
elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[
|
||||
if (prefs["bul_show_temporary"] and not decision_sem) or prefs[
|
||||
"bul_show_temporary_forced"
|
||||
]:
|
||||
return prefs["bul_temporary_txt"]
|
||||
return ""
|
||||
|
||||
|
||||
def get_filigranne_apc(
|
||||
etud_etat: str, prefs, etudid: int, res: ResultatsSemestreBUT
|
||||
) -> str:
|
||||
"""Texte à placer en "filigranne" sur le bulletin pdf.
|
||||
Version optimisée pour BUT
|
||||
"""
|
||||
if prefs["bul_show_temporary_forced"]:
|
||||
return get_filigranne(etud_etat, prefs)
|
||||
if prefs["bul_show_temporary"]:
|
||||
# requete les décisions de jury
|
||||
decision_sem = res.etud_has_decision(etudid)
|
||||
return get_filigranne(etud_etat, prefs, decision_sem=decision_sem)
|
||||
return get_filigranne(etud_etat, prefs)
|
||||
|
@ -396,3 +396,13 @@ class ValidationsSemestreCache(ScoDocCache):
|
||||
|
||||
prefix = "VSC"
|
||||
timeout = 60 * 60 # ttl 1 heure (en phase de mise au point)
|
||||
|
||||
|
||||
class RequeteTableauAssiduiteCache(ScoDocCache):
|
||||
"""
|
||||
clé : "<titre_tableau>:<type_obj>:<show_pres>:<show_retard>:<show_desc>:<order_col>:<order>"
|
||||
Valeur = liste de dicts
|
||||
"""
|
||||
|
||||
prefix = "TABASSI"
|
||||
timeout = 60 * 60 # Une heure
|
||||
|
@ -39,7 +39,6 @@ from app import log
|
||||
from app.scodoc.scolog import logdb
|
||||
from app.scodoc import sco_cache, sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc.codes_cursus import (
|
||||
CMP,
|
||||
ADC,
|
||||
|
@ -134,10 +134,10 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
||||
"nom": etud["nom"],
|
||||
"prenom": etud["prenom"],
|
||||
"_nom_target": url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
),
|
||||
"_prenom_target": url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
),
|
||||
"_nom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]),
|
||||
# 'debouche' : etud['debouche'],
|
||||
|
@ -177,12 +177,26 @@ def formsemestre_edt_dict(
|
||||
TODO: spécifier intervalle de dates start et end
|
||||
"""
|
||||
t0 = time.time()
|
||||
group_ids_set = set(group_ids) if group_ids else set()
|
||||
try:
|
||||
events_scodoc, _ = load_and_convert_ics(formsemestre)
|
||||
except ScoValueError as exc:
|
||||
return exc.args[0]
|
||||
# Génération des événements pour le calendrier html
|
||||
edt_dict = translate_calendar(
|
||||
events_scodoc, group_ids, show_modules_titles=show_modules_titles
|
||||
)
|
||||
log(
|
||||
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
|
||||
)
|
||||
return edt_dict
|
||||
|
||||
|
||||
def translate_calendar(
|
||||
events_scodoc: list[dict],
|
||||
group_ids: list[int] = None,
|
||||
show_modules_titles=True,
|
||||
) -> list[dict]:
|
||||
"""Génération des événements pour le calendrier html"""
|
||||
group_ids_set = set(group_ids) if group_ids else set()
|
||||
promo_icon = f"""<img height="18px" src="{scu.STATIC_DIR}/icons/promo.svg"
|
||||
title="promotion complète" alt="promotion"/>"""
|
||||
abs_icon = f"""<img height="28px" src="{scu.STATIC_DIR}/icons/absences.svg"
|
||||
@ -211,7 +225,6 @@ def formsemestre_edt_dict(
|
||||
url_for(
|
||||
"assiduites.signal_assiduites_group",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
heure_deb=event["heure_deb"],
|
||||
heure_fin=event["heure_fin"],
|
||||
@ -271,17 +284,17 @@ def formsemestre_edt_dict(
|
||||
"start": event["start"],
|
||||
"end": event["end"],
|
||||
"backgroundColor": event["group_bg_color"],
|
||||
"raw": event["raw"],
|
||||
# Infos brutes pour usage API éventuel
|
||||
"edt_ens_ids": event["edt_ens_ids"],
|
||||
"ens_user_names": ens_user_names,
|
||||
"group_id": group.id if group else None,
|
||||
"group_edt_id": event["edt_group"],
|
||||
"moduleimpl_id": modimpl.id if modimpl else None,
|
||||
"UID": event["UID"], # icalendar event UID
|
||||
}
|
||||
events_cal.append(d)
|
||||
log(
|
||||
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
|
||||
)
|
||||
|
||||
return events_cal
|
||||
|
||||
|
||||
@ -302,7 +315,35 @@ def get_ics_uid_pattern() -> re.Pattern:
|
||||
|
||||
|
||||
def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[str]]:
|
||||
"""Chargement fichier ics, filtrage et extraction des identifiants.
|
||||
"""Chargement fichier ics.
|
||||
Renvoie une liste d'évènements, et la liste des identifiants de groupes
|
||||
trouvés (utilisée pour l'aide).
|
||||
"""
|
||||
# Chargement du calendier ics
|
||||
_, calendar = formsemestre_load_calendar(formsemestre)
|
||||
if not calendar:
|
||||
return [], []
|
||||
# --- Correspondances id edt -> id scodoc pour groupes
|
||||
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
||||
default_group = formsemestre.get_default_group()
|
||||
# --- Correspondances id edt -> id scodoc pour modimpls
|
||||
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
|
||||
return convert_ics(
|
||||
calendar,
|
||||
edt2group=edt2group,
|
||||
default_group=default_group,
|
||||
edt2modimpl=edt2modimpl,
|
||||
)
|
||||
|
||||
|
||||
def convert_ics(
|
||||
calendar: icalendar.cal.Calendar,
|
||||
edt2group: dict[str, GroupDescr] = None,
|
||||
default_group: GroupDescr = None,
|
||||
edt2modimpl: dict[str, ModuleImpl] = None,
|
||||
) -> tuple[list[dict], list[str]]:
|
||||
"""Filtrage et extraction des identifiants des évènements calendrier.
|
||||
|
||||
Renvoie une liste d'évènements, et la liste des identifiants de groupes
|
||||
trouvés (utilisée pour l'aide).
|
||||
|
||||
@ -310,10 +351,6 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
- False si extraction regexp non configuré
|
||||
- "tous" (promo) si pas de correspondance trouvée.
|
||||
"""
|
||||
# Chargement du calendier ics
|
||||
_, calendar = formsemestre_load_calendar(formsemestre)
|
||||
if not calendar:
|
||||
return []
|
||||
# --- Paramètres d'extraction
|
||||
edt_ics_title_field = ScoDocSiteConfig.get("edt_ics_title_field")
|
||||
edt_ics_title_regexp = ScoDocSiteConfig.get("edt_ics_title_regexp")
|
||||
@ -348,15 +385,13 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
edt_ics_uid_field = ScoDocSiteConfig.get("edt_ics_uid_field")
|
||||
edt_ics_uid_pattern = get_ics_uid_pattern()
|
||||
|
||||
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants
|
||||
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
||||
# --- Groupes
|
||||
group_colors = {
|
||||
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
|
||||
for i, group_name in enumerate(edt2group)
|
||||
}
|
||||
edt_groups_ids = set() # les ids de groupes normalisés tels que dans l'ics
|
||||
default_group = formsemestre.get_default_group()
|
||||
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
|
||||
|
||||
edt2user: dict[str, User | None] = {} # construit au fur et à mesure (cache)
|
||||
# ---
|
||||
events = [e for e in calendar.walk() if e.name == "VEVENT"]
|
||||
@ -371,29 +406,6 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
)
|
||||
# title remplacé par le nom du module scodoc quand il est trouvé
|
||||
title = title_edt
|
||||
# --- Group
|
||||
if edt_ics_group_pattern:
|
||||
edt_group = extract_event_edt_id(
|
||||
event, edt_ics_group_field, edt_ics_group_pattern
|
||||
)
|
||||
edt_groups_ids.add(edt_group)
|
||||
# si pas de groupe dans l'event, ou si groupe non reconnu,
|
||||
# prend toute la promo ("tous")
|
||||
group: GroupDescr = (
|
||||
edt2group.get(edt_group, default_group)
|
||||
if edt_group
|
||||
else default_group
|
||||
)
|
||||
group_bg_color = (
|
||||
group_colors.get(edt_group, _EVENT_DEFAULT_COLOR)
|
||||
if group
|
||||
else "lightgrey"
|
||||
)
|
||||
else:
|
||||
edt_group = ""
|
||||
group = False
|
||||
group_bg_color = _EVENT_DEFAULT_COLOR
|
||||
|
||||
# --- ModuleImpl
|
||||
if edt_ics_mod_pattern:
|
||||
edt_module = extract_event_edt_id(
|
||||
@ -405,6 +417,34 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
else:
|
||||
modimpl = False
|
||||
edt_module = ""
|
||||
# --- Group
|
||||
if edt_ics_group_pattern:
|
||||
edt_group = extract_event_edt_id(
|
||||
event, edt_ics_group_field, edt_ics_group_pattern
|
||||
)
|
||||
edt_groups_ids.add(edt_group)
|
||||
# si pas de groupe dans l'event, ou si groupe non reconnu,
|
||||
# prend toute la promo ("tous")
|
||||
event_default_group = (
|
||||
default_group
|
||||
if default_group
|
||||
else (modimpl.formsemestre.get_default_group() if modimpl else None)
|
||||
)
|
||||
group: GroupDescr = (
|
||||
edt2group.get(edt_group, event_default_group)
|
||||
if edt_group
|
||||
else event_default_group
|
||||
)
|
||||
group_bg_color = (
|
||||
group_colors.get(edt_group, _EVENT_DEFAULT_COLOR)
|
||||
if group
|
||||
else "lightgrey"
|
||||
)
|
||||
else:
|
||||
edt_group = ""
|
||||
group = False
|
||||
group_bg_color = _EVENT_DEFAULT_COLOR
|
||||
|
||||
# --- Enseignants
|
||||
users: list[User] = []
|
||||
if edt_ics_uid_pattern:
|
||||
@ -446,6 +486,8 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
"jour": event.decoded("dtstart").date().isoformat(),
|
||||
"start": event.decoded("dtstart").isoformat(),
|
||||
"end": event.decoded("dtend").isoformat(),
|
||||
"UID": event.decoded("UID").decode("utf-8"),
|
||||
"raw": event.to_ical().decode("utf-8"),
|
||||
}
|
||||
)
|
||||
return events_sco, sorted(edt_groups_ids)
|
||||
|
@ -542,7 +542,9 @@ def view_scodoc_etuds(semset_id, title="", nip_list="", fmt="html"):
|
||||
etuds = [sco_etud.get_etud_info(code_nip=nip, filled=True)[0] for nip in nips]
|
||||
|
||||
for e in etuds:
|
||||
tgt = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"])
|
||||
tgt = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
|
||||
)
|
||||
e["_nom_target"] = tgt
|
||||
e["_prenom_target"] = tgt
|
||||
e["_nom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """
|
||||
@ -769,10 +771,10 @@ def view_apo_csv(etape_apo="", semset_id="", fmt="html"):
|
||||
e["in_scodoc"] = e["nip"] not in nips_no_sco
|
||||
e["in_scodoc_str"] = {True: "oui", False: "non"}[e["in_scodoc"]]
|
||||
if e["in_scodoc"]:
|
||||
e["_in_scodoc_str_target"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, code_nip=e["nip"]
|
||||
)
|
||||
e.update(sco_etud.get_etud_info(code_nip=e["nip"], filled=True)[0])
|
||||
e["_in_scodoc_str_target"] = url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=e["etudid"]
|
||||
)
|
||||
e["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"],)
|
||||
e["_prenom_td_attrs"] = 'id="pre-%s" class="etudinfo"' % (e["etudid"],)
|
||||
else:
|
||||
|
@ -692,7 +692,7 @@ class EtapeBilan:
|
||||
@staticmethod
|
||||
def link_etu(etudid, nom):
|
||||
return '<a class="stdlink" href="%s">%s</a>' % (
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
nom,
|
||||
)
|
||||
|
||||
|
@ -64,7 +64,7 @@ def format_etud_ident(etud: dict):
|
||||
|
||||
Note: par rapport à Identite.to_dict_bul(),
|
||||
ajoute les champs:
|
||||
'email_default', 'nom_disp', 'nom_usuel', 'civilite_etat_civil_str', 'ne', 'civilite_str'
|
||||
'nom_disp', 'nom_usuel', 'civilite_etat_civil_str', 'ne', 'civilite_str'
|
||||
"""
|
||||
etud["nom"] = format_nom(etud["nom"])
|
||||
if "nom_usuel" in etud:
|
||||
@ -98,10 +98,6 @@ def format_etud_ident(etud: dict):
|
||||
etud["ne"] = "e"
|
||||
else: # 'X'
|
||||
etud["ne"] = "(e)"
|
||||
# Mail à utiliser pour les envois vers l'étudiant:
|
||||
# choix qui pourrait être controé par une preference
|
||||
# ici priorité au mail institutionnel:
|
||||
etud["email_default"] = etud.get("email", "") or etud.get("emailperso", "")
|
||||
|
||||
|
||||
def force_uppercase(s):
|
||||
@ -117,36 +113,6 @@ def _format_etat_civil(etud: dict) -> str:
|
||||
return etud["nomprenom"]
|
||||
|
||||
|
||||
def format_lycee(nomlycee):
|
||||
nomlycee = nomlycee.strip()
|
||||
s = nomlycee.lower()
|
||||
if s[:5] == "lycee" or s[:5] == "lycée":
|
||||
return nomlycee[5:]
|
||||
else:
|
||||
return nomlycee
|
||||
|
||||
|
||||
def format_telephone(n):
|
||||
if n is None:
|
||||
return ""
|
||||
if len(n) < 7:
|
||||
return n
|
||||
else:
|
||||
n = n.replace(" ", "").replace(".", "")
|
||||
i = 0
|
||||
r = ""
|
||||
j = len(n) - 1
|
||||
while j >= 0:
|
||||
r = n[j] + r
|
||||
if i % 2 == 1 and j != 0:
|
||||
r = " " + r
|
||||
i += 1
|
||||
j -= 1
|
||||
if len(r) == 13 and r[0] != "0":
|
||||
r = "0" + r
|
||||
return r
|
||||
|
||||
|
||||
def format_pays(s):
|
||||
"laisse le pays seulement si != FRANCE"
|
||||
if s.upper() != "FRANCE":
|
||||
@ -283,14 +249,14 @@ def _check_duplicate_code(cnx, args, code_name, disable_notify=False, edit=True)
|
||||
listh.append(
|
||||
f"""Autre étudiant: <a href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=e["etudid"]
|
||||
)}">{e['nom']} {e['prenom']}</a>"""
|
||||
)
|
||||
if etudid:
|
||||
OK = "retour à la fiche étudiant"
|
||||
dest_endpoint = "scolar.ficheEtud"
|
||||
dest_endpoint = "scolar.fiche_etud"
|
||||
parameters = {"etudid": etudid}
|
||||
else:
|
||||
if "tf_submitted" in args:
|
||||
@ -619,7 +585,7 @@ def create_etud(cnx, args: dict = None):
|
||||
etud_dict = etudident_list(cnx, {"etudid": etudid})[0]
|
||||
fill_etuds_info([etud_dict])
|
||||
etud_dict["url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
@ -724,19 +690,28 @@ def get_etablissements():
|
||||
|
||||
|
||||
def get_lycee_infos(codelycee):
|
||||
E = get_etablissements()
|
||||
return E.get(codelycee, None)
|
||||
etablissements = get_etablissements()
|
||||
return etablissements.get(codelycee, None)
|
||||
|
||||
|
||||
def format_lycee_from_code(codelycee):
|
||||
def format_lycee_from_code(codelycee: str) -> str:
|
||||
"Description lycee à partir du code"
|
||||
E = get_etablissements()
|
||||
if codelycee in E:
|
||||
e = E[codelycee]
|
||||
etablissements = get_etablissements()
|
||||
if codelycee in etablissements:
|
||||
e = etablissements[codelycee]
|
||||
nomlycee = e["name"]
|
||||
return "%s (%s)" % (nomlycee, e["commune"])
|
||||
return f"{nomlycee} ({e['commune']})"
|
||||
return f"{codelycee} (établissement inconnu)"
|
||||
|
||||
|
||||
def format_lycee(nomlycee: str) -> str:
|
||||
"mise en forme nom de lycée"
|
||||
nomlycee = nomlycee.strip()
|
||||
s = nomlycee.lower()
|
||||
if s[:5] == "lycee" or s[:5] == "lycée":
|
||||
return nomlycee[5:]
|
||||
else:
|
||||
return "%s (établissement inconnu)" % codelycee
|
||||
return nomlycee
|
||||
|
||||
|
||||
def etud_add_lycee_infos(etud):
|
||||
@ -821,36 +796,6 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
|
||||
# nettoyage champs souvent vides
|
||||
etud["codepostallycee"] = etud.get("codepostallycee", "") or ""
|
||||
etud["nomlycee"] = etud.get("nomlycee", "") or ""
|
||||
if etud.get("nomlycee"):
|
||||
etud["ilycee"] = "Lycée " + format_lycee(etud["nomlycee"])
|
||||
if etud["villelycee"]:
|
||||
etud["ilycee"] += " (%s)" % etud.get("villelycee", "")
|
||||
etud["ilycee"] += "<br>"
|
||||
else:
|
||||
if etud.get("codelycee"):
|
||||
etud["ilycee"] = format_lycee_from_code(etud["codelycee"])
|
||||
else:
|
||||
etud["ilycee"] = ""
|
||||
rap = ""
|
||||
if etud.get("rapporteur") or etud.get("commentaire"):
|
||||
rap = "Note du rapporteur"
|
||||
if etud.get("rapporteur"):
|
||||
rap += " (%s)" % etud["rapporteur"]
|
||||
rap += ": "
|
||||
if etud.get("commentaire"):
|
||||
rap += "<em>%s</em>" % etud["commentaire"]
|
||||
etud["rap"] = rap
|
||||
|
||||
if etud.get("telephone"):
|
||||
etud["telephonestr"] = "<b>Tél.:</b> " + format_telephone(etud["telephone"])
|
||||
else:
|
||||
etud["telephonestr"] = ""
|
||||
if etud.get("telephonemobile"):
|
||||
etud["telephonemobilestr"] = "<b>Mobile:</b> " + format_telephone(
|
||||
etud["telephonemobile"]
|
||||
)
|
||||
else:
|
||||
etud["telephonemobilestr"] = ""
|
||||
|
||||
|
||||
def etud_inscriptions_infos(etudid: int, ne="") -> dict:
|
||||
|
@ -156,7 +156,7 @@ def evaluation_check_absences_html(
|
||||
H.append(
|
||||
f"""<li><a class="discretelink" href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
}">{etud.nomprenom}</a>"""
|
||||
)
|
||||
|
@ -178,9 +178,7 @@ def evaluation_create_form(
|
||||
{
|
||||
"title": "Heure de début",
|
||||
"explanation": "heure du début de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
"input_type": "time",
|
||||
},
|
||||
),
|
||||
(
|
||||
@ -188,9 +186,7 @@ def evaluation_create_form(
|
||||
{
|
||||
"title": "Heure de fin",
|
||||
"explanation": "heure de fin de l'épreuve",
|
||||
"input_type": "menu",
|
||||
"allowed_values": heures,
|
||||
"labels": heures,
|
||||
"input_type": "time",
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -335,6 +331,7 @@ def evaluation_create_form(
|
||||
+ "\n"
|
||||
+ tf[1]
|
||||
+ render_template("scodoc/help/evaluations.j2", is_apc=is_apc)
|
||||
+ render_template("sco_timepicker.j2")
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
elif tf[0] == -1:
|
||||
|
@ -691,7 +691,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, link_saisie=True)
|
||||
group_ids=group_id,
|
||||
evaluation_id=evaluation.id,
|
||||
date_debut=evaluation.date_debut.isoformat(),
|
||||
date_fin=evaluation.date_fin.isoformat(),
|
||||
date_fin=evaluation.date_fin.isoformat() if evaluation.date_fin else "",
|
||||
)
|
||||
}">absences ce jour</a>
|
||||
</span>
|
||||
|
@ -173,9 +173,10 @@ def _build_results_list(dpv_by_sem, etuds_infos):
|
||||
"nom_usuel": etud["nom_usuel"],
|
||||
"prenom": etud["prenom"],
|
||||
"civilite_str": etud["civilite_str"],
|
||||
"_nom_target": "%s"
|
||||
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
"_nom_td_attrs": 'id="%s" class="etudinfo"' % etudid,
|
||||
"_nom_target": url_for(
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
),
|
||||
"_nom_td_attrs": f'id="{etudid}" class="etudinfo"',
|
||||
"bac": bac.abbrev(),
|
||||
"parcours": dec["parcours"],
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ def search_etud_in_dept(expnom=""):
|
||||
if "dest_url" in vals:
|
||||
endpoint = vals["dest_url"]
|
||||
else:
|
||||
endpoint = "scolar.ficheEtud"
|
||||
endpoint = "scolar.fiche_etud"
|
||||
if "parameters_keys" in vals:
|
||||
for key in vals["parameters_keys"].split(","):
|
||||
url_args[key] = vals[key]
|
||||
@ -328,8 +328,9 @@ def table_etud_in_accessible_depts(expnom=None):
|
||||
"""
|
||||
result, accessible_depts = search_etud_in_accessible_depts(expnom=expnom)
|
||||
H = [
|
||||
"""<div class="table_etud_in_accessible_depts">""",
|
||||
"""<h3>Recherche multi-département de "<tt>%s</tt>"</h3>""" % expnom,
|
||||
f"""<div class="table_etud_in_accessible_depts">
|
||||
<h3>Recherche multi-département de "<tt>{expnom}</tt>"</h3>
|
||||
""",
|
||||
]
|
||||
for etuds in result:
|
||||
if etuds:
|
||||
@ -337,9 +338,9 @@ def table_etud_in_accessible_depts(expnom=None):
|
||||
# H.append('<h3>Département %s</h3>' % DeptId)
|
||||
for e in etuds:
|
||||
e["_nomprenom_target"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=dept_id, etudid=e["etudid"]
|
||||
"scolar.fiche_etud", scodoc_dept=dept_id, etudid=e["etudid"]
|
||||
)
|
||||
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
|
||||
e["_nomprenom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """
|
||||
|
||||
tab = GenTable(
|
||||
titles={"nomprenom": "Étudiants en " + dept_id},
|
||||
|
@ -102,7 +102,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid, only_ext=1) }">
|
||||
inscrire à un autre semestre</a>"
|
||||
</p>
|
||||
<h3><a href="{ url_for('scolar.ficheEtud',
|
||||
<h3><a href="{ url_for('scolar.fiche_etud',
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" class="stdlink">Étudiant {etud.nomprenom}</a></h3>
|
||||
""",
|
||||
@ -221,7 +221,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
|
||||
tf[2]["formation_id"] = orig_sem["formation_id"]
|
||||
formsemestre_ext_create(etudid, tf[2])
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
|
||||
|
||||
|
@ -400,7 +400,7 @@ def formsemestre_inscription_with_modules_form(etudid, only_ext=False):
|
||||
H.append("<p>aucune session de formation !</p>")
|
||||
H.append(
|
||||
f"""<h3>ou</h3> <a class="stdlink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">retour à la fiche de {etud.nomprenom}</a>"""
|
||||
)
|
||||
return "\n".join(H) + footer
|
||||
@ -440,7 +440,7 @@ def formsemestre_inscription_with_modules(
|
||||
dans le semestre {formsemestre.titre_mois()}
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="{url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
<li><a href="{url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}" class="stdlink">retour à la fiche de {etud.nomprenom}</a>
|
||||
</li>
|
||||
<li><a href="{url_for(
|
||||
@ -501,7 +501,7 @@ def formsemestre_inscription_with_modules(
|
||||
method="formsemestre_inscription_with_modules",
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
# formulaire choix groupe
|
||||
@ -656,7 +656,7 @@ function chkbx_select(field_id, state) {
|
||||
return "\n".join(H) + "\n" + tf[1] + footer
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
# Inscriptions aux modules choisis
|
||||
@ -697,7 +697,7 @@ function chkbx_select(field_id, state) {
|
||||
"""<h3>Aucune modification à effectuer</h3>
|
||||
<p><a class="stdlink" href="%s">retour à la fiche étudiant</a></p>
|
||||
"""
|
||||
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
return "\n".join(H) + footer
|
||||
|
||||
@ -755,7 +755,7 @@ function chkbx_select(field_id, state) {
|
||||
etudid,
|
||||
modulesimpls_ainscrire,
|
||||
modulesimpls_adesinscrire,
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
)
|
||||
)
|
||||
return "\n".join(H) + footer
|
||||
@ -820,7 +820,7 @@ def do_moduleimpl_incription_options(
|
||||
<p><a class="stdlink" href="%s">
|
||||
Retour à la fiche étudiant</a></p>
|
||||
"""
|
||||
% url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
% url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
html_sco_header.sco_footer(),
|
||||
]
|
||||
return "\n".join(H)
|
||||
@ -885,7 +885,7 @@ def formsemestre_inscrits_ailleurs(formsemestre_id):
|
||||
'<li><a href="%s" class="discretelink">%s</a> : '
|
||||
% (
|
||||
url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
),
|
||||
|
@ -51,13 +51,14 @@ from app.models import (
|
||||
NotesNotes,
|
||||
)
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
ScoInvalidIdType,
|
||||
)
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_archives_formsemestre
|
||||
@ -75,6 +76,7 @@ from app.scodoc import sco_moduleimpl
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_users
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.html_sidebar import retreive_formsemestre_from_request
|
||||
from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html
|
||||
import sco_version
|
||||
|
||||
@ -109,7 +111,7 @@ def _build_menu_stats(formsemestre_id):
|
||||
"title": "Lycées d'origine",
|
||||
"endpoint": "notes.formsemestre_etuds_lycees",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
"enabled": True,
|
||||
"enabled": current_user.has_permission(Permission.ViewEtudData),
|
||||
},
|
||||
{
|
||||
"title": 'Table "poursuite"',
|
||||
@ -336,6 +338,7 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
||||
formsemestre_id, fix_if_missing=True
|
||||
),
|
||||
},
|
||||
"enabled": current_user.has_permission(Permission.ViewEtudData),
|
||||
},
|
||||
{
|
||||
"title": "Vérifier inscriptions multiples",
|
||||
@ -474,57 +477,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
||||
return "\n".join(H)
|
||||
|
||||
|
||||
def retreive_formsemestre_from_request() -> int:
|
||||
"""Cherche si on a de quoi déduire le semestre affiché à partir des
|
||||
arguments de la requête:
|
||||
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
|
||||
Returns None si pas défini.
|
||||
"""
|
||||
if request.method == "GET":
|
||||
args = request.args
|
||||
elif request.method == "POST":
|
||||
args = request.form
|
||||
else:
|
||||
return None
|
||||
formsemestre_id = None
|
||||
# Search formsemestre
|
||||
group_ids = args.get("group_ids", [])
|
||||
if "formsemestre_id" in args:
|
||||
formsemestre_id = args["formsemestre_id"]
|
||||
elif "moduleimpl_id" in args and args["moduleimpl_id"]:
|
||||
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=args["moduleimpl_id"])
|
||||
if not modimpl:
|
||||
return None # suppressed ?
|
||||
modimpl = modimpl[0]
|
||||
formsemestre_id = modimpl["formsemestre_id"]
|
||||
elif "evaluation_id" in args:
|
||||
E = sco_evaluation_db.get_evaluations_dict(
|
||||
{"evaluation_id": args["evaluation_id"]}
|
||||
)
|
||||
if not E:
|
||||
return None # evaluation suppressed ?
|
||||
E = E[0]
|
||||
modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
|
||||
formsemestre_id = modimpl["formsemestre_id"]
|
||||
elif "group_id" in args:
|
||||
group = sco_groups.get_group(args["group_id"])
|
||||
formsemestre_id = group["formsemestre_id"]
|
||||
elif group_ids:
|
||||
if isinstance(group_ids, str):
|
||||
group_ids = group_ids.split(",")
|
||||
group_id = group_ids[0]
|
||||
group = sco_groups.get_group(group_id)
|
||||
formsemestre_id = group["formsemestre_id"]
|
||||
elif "partition_id" in args:
|
||||
partition = sco_groups.get_partition(args["partition_id"])
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
|
||||
if not formsemestre_id:
|
||||
return None # no current formsemestre
|
||||
|
||||
return int(formsemestre_id)
|
||||
|
||||
|
||||
# Element HTML decrivant un semestre (barre de menu et infos)
|
||||
def formsemestre_page_title(formsemestre_id=None):
|
||||
"""Element HTML decrivant un semestre (barre de menu et infos)
|
||||
@ -917,7 +869,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
||||
<a class="btn" href="{
|
||||
url_for("assiduites.bilan_dept",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre=formsemestre.id,
|
||||
formsemestre_id=formsemestre.id,
|
||||
group_ids=group.id,
|
||||
)}">
|
||||
<button>Justificatifs en attente</button></a>
|
||||
@ -1457,7 +1409,7 @@ def formsemestre_warning_etuds_sans_note(
|
||||
noms = ", ".join(
|
||||
[
|
||||
f"""<a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}" class="discretelink">{etud.nomprenom}</a>"""
|
||||
for etud in etuds
|
||||
]
|
||||
@ -1519,13 +1471,13 @@ def formsemestre_note_etuds_sans_notes(
|
||||
a déjà des notes"""
|
||||
)
|
||||
return redirect(
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
)
|
||||
else:
|
||||
noms = "</li><li>".join(
|
||||
[
|
||||
f"""<a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
|
||||
}" class="discretelink">{etud.nomprenom}</a>"""
|
||||
for etud in etuds
|
||||
]
|
||||
|
@ -117,8 +117,8 @@ def formsemestre_validation_etud_form(
|
||||
if read_only:
|
||||
check = True
|
||||
|
||||
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud, formsemestre_id)
|
||||
etud_d = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud_d, formsemestre_id)
|
||||
if not Se.sem["etat"]:
|
||||
raise ScoValueError("validation: semestre verrouille")
|
||||
|
||||
@ -132,7 +132,7 @@ def formsemestre_validation_etud_form(
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
page_title=f"Parcours {etud['nomprenom']}",
|
||||
page_title=f"Parcours {etud.nomprenom}",
|
||||
javascripts=["js/recap_parcours.js"],
|
||||
)
|
||||
]
|
||||
@ -177,26 +177,22 @@ def formsemestre_validation_etud_form(
|
||||
H.append('<table style="width: 100%"><tr><td>')
|
||||
if not check:
|
||||
H.append(
|
||||
'<h2 class="formsemestre">%s: validation %s%s</h2>Parcours: %s'
|
||||
% (
|
||||
etud["nomprenom"],
|
||||
Se.parcours.SESSION_NAME_A,
|
||||
Se.parcours.SESSION_NAME,
|
||||
Se.get_cursus_descr(),
|
||||
)
|
||||
f"""<h2 class="formsemestre">{etud.nomprenom}: validation {
|
||||
Se.parcours.SESSION_NAME_A}{Se.parcours.SESSION_NAME
|
||||
}</h2>Parcours: {Se.get_cursus_descr()}
|
||||
"""
|
||||
)
|
||||
else:
|
||||
H.append(
|
||||
'<h2 class="formsemestre">Parcours de %s</h2>%s'
|
||||
% (etud["nomprenom"], Se.get_cursus_descr())
|
||||
f"""<h2 class="formsemestre">Parcours de {etud.nomprenom}</h2>{Se.get_cursus_descr()}"""
|
||||
)
|
||||
|
||||
H.append(
|
||||
'</td><td style="text-align: right;"><a href="%s">%s</a></td></tr></table>'
|
||||
% (
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid),
|
||||
sco_photos.etud_photo_html(etud, title="fiche de %s" % etud["nom"]),
|
||||
)
|
||||
f"""</td><td style="text-align: right;"><a href="{
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">{etud.photo_html(title="fiche de " + etud.nomprenom)}</a></td></tr>
|
||||
</table>
|
||||
"""
|
||||
)
|
||||
|
||||
etud_etat = nt.get_etud_etat(etudid)
|
||||
@ -210,7 +206,7 @@ def formsemestre_validation_etud_form(
|
||||
<div class="warning">
|
||||
Impossible de statuer sur cet étudiant:
|
||||
il est démissionnaire ou défaillant (voir <a href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">sa fiche</a>)
|
||||
</div>
|
||||
"""
|
||||
@ -289,7 +285,7 @@ def formsemestre_validation_etud_form(
|
||||
etudid=etudid, origin_formsemestre_id=formsemestre_id
|
||||
).all()
|
||||
if autorisations:
|
||||
H.append(". Autorisé%s à s'inscrire en " % etud["ne"])
|
||||
H.append(f". Autorisé{etud.e} à s'inscrire en ")
|
||||
H.append(", ".join([f"S{aut.semestre_id}" for aut in autorisations]) + ".")
|
||||
H.append("</p>")
|
||||
|
||||
|
@ -52,7 +52,7 @@ from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_etud import etud_sort_key
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoPermissionDenied
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + [
|
||||
@ -118,6 +118,16 @@ def groups_view(
|
||||
init_qtip=True,
|
||||
)
|
||||
}
|
||||
<style>
|
||||
div.multiselect-container.dropdown-menu {{
|
||||
min-width: 180px;
|
||||
}}
|
||||
span.warning_unauthorized {{
|
||||
color: pink;
|
||||
font-style: italic;
|
||||
margin-left: 12px;
|
||||
}}
|
||||
</style>
|
||||
<div id="group-tabs">
|
||||
<!-- Menu choix groupe -->
|
||||
{form_groups_choice(groups_infos, submit_on_change=True)}
|
||||
@ -474,15 +484,12 @@ def groups_table(
|
||||
"""
|
||||
from app.scodoc import sco_report
|
||||
|
||||
# log(
|
||||
# "enter groups_table %s: %s"
|
||||
# % (groups_infos.members[0]["nom"], groups_infos.members[0].get("etape", "-"))
|
||||
# )
|
||||
can_view_etud_data = int(current_user.has_permission(Permission.ViewEtudData))
|
||||
with_codes = int(with_codes)
|
||||
with_paiement = int(with_paiement)
|
||||
with_archives = int(with_archives)
|
||||
with_annotations = int(with_annotations)
|
||||
with_bourse = int(with_bourse)
|
||||
with_paiement = int(with_paiement) and can_view_etud_data
|
||||
with_archives = int(with_archives) and can_view_etud_data
|
||||
with_annotations = int(with_annotations) and can_view_etud_data
|
||||
with_bourse = int(with_bourse) and can_view_etud_data
|
||||
|
||||
base_url_np = groups_infos.base_url + f"&with_codes={with_codes}"
|
||||
base_url = (
|
||||
@ -527,6 +534,7 @@ def groups_table(
|
||||
if fmt != "html": # ne mentionne l'état que en Excel (style en html)
|
||||
columns_ids.append("etat")
|
||||
columns_ids.append("email")
|
||||
if can_view_etud_data:
|
||||
columns_ids.append("emailperso")
|
||||
|
||||
if fmt == "moodlecsv":
|
||||
@ -561,7 +569,7 @@ def groups_table(
|
||||
else:
|
||||
etud["_emailperso_target"] = ""
|
||||
fiche_url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
)
|
||||
etud["_nom_disp_target"] = fiche_url
|
||||
etud["_nom_disp_order"] = etud_sort_key(etud)
|
||||
@ -616,7 +624,7 @@ def groups_table(
|
||||
+ "+".join(sorted(moodle_groupenames))
|
||||
)
|
||||
else:
|
||||
filename = "etudiants_%s" % groups_infos.groups_filename
|
||||
filename = f"etudiants_{groups_infos.groups_filename}"
|
||||
|
||||
prefs = sco_preferences.SemPreferences(groups_infos.formsemestre_id)
|
||||
tab = GenTable(
|
||||
@ -664,28 +672,33 @@ def groups_table(
|
||||
"""
|
||||
]
|
||||
if groups_infos.members:
|
||||
Of = []
|
||||
menu_options = []
|
||||
options = {
|
||||
"with_codes": "Affiche codes",
|
||||
}
|
||||
if can_view_etud_data:
|
||||
options.update(
|
||||
{
|
||||
"with_paiement": "Paiement inscription",
|
||||
"with_archives": "Fichiers archivés",
|
||||
"with_annotations": "Annotations",
|
||||
"with_codes": "Codes",
|
||||
"with_bourse": "Statut boursier",
|
||||
}
|
||||
for option in options:
|
||||
)
|
||||
for option, label in options.items():
|
||||
if locals().get(option, False):
|
||||
selected = "selected"
|
||||
else:
|
||||
selected = ""
|
||||
Of.append(
|
||||
"""<option value="%s" %s>%s</option>"""
|
||||
% (option, selected, options[option])
|
||||
menu_options.append(
|
||||
f"""<option value="{option}" {selected}>{label}</option>"""
|
||||
)
|
||||
|
||||
H.extend(
|
||||
[
|
||||
"""<span style="margin-left: 2em;"><select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
|
||||
"\n".join(Of),
|
||||
"""<span style="margin-left: 2em;">
|
||||
<select name="group_list_options" id="group_list_options" class="multiselect" multiple="multiple">""",
|
||||
"\n".join(menu_options),
|
||||
"""</select></span>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
@ -701,6 +714,9 @@ def groups_table(
|
||||
});
|
||||
</script>
|
||||
""",
|
||||
"""<span class="warning_unauthorized">accès aux données personnelles interdit</span>"""
|
||||
if not can_view_etud_data
|
||||
else "",
|
||||
]
|
||||
)
|
||||
H.append("</div></form>")
|
||||
@ -708,41 +724,45 @@ def groups_table(
|
||||
H.extend(
|
||||
[
|
||||
tab.html(),
|
||||
"<ul>",
|
||||
'<li><a class="stdlink" href="%s&fmt=xlsappel">Feuille d\'appel Excel</a></li>'
|
||||
% (tab.base_url,),
|
||||
'<li><a class="stdlink" href="%s&fmt=xls">Table Excel</a></li>'
|
||||
% (tab.base_url,),
|
||||
'<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>
|
||||
f"""
|
||||
<ul>
|
||||
<li><a class="stdlink" href="{tab.base_url}&fmt=xlsappel">Feuille d'appel Excel</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{tab.base_url}&fmt=xls">Table Excel</a>
|
||||
</li>
|
||||
<li><a class="stdlink" href="{tab.base_url}&fmt=moodlecsv">Fichier CSV pour Moodle (groupe sélectionné)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="stdlink" href="export_groups_as_moodle_csv?formsemestre_id={groups_infos.formsemestre_id}">
|
||||
Fichier CSV pour Moodle (tous les groupes)</a>
|
||||
<em>(voir le paramétrage pour modifier le format des fichiers Moodle exportés)</em>
|
||||
</li>"""
|
||||
% groups_infos.formsemestre_id,
|
||||
</li>""",
|
||||
]
|
||||
)
|
||||
if amail_inst:
|
||||
H.append(
|
||||
'<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses institutionnelles)</a></li>'
|
||||
% (
|
||||
",".join(amail_inst),
|
||||
groups_infos.groups_titles,
|
||||
len(amail_inst),
|
||||
)
|
||||
f"""<li>
|
||||
<a class="stdlink" href="mailto:?bcc={','.join(amail_inst)
|
||||
}">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
|
||||
(via {len(amail_inst)} adresses institutionnelles)</a>
|
||||
</li>"""
|
||||
)
|
||||
|
||||
if can_view_etud_data:
|
||||
if amail_perso:
|
||||
H.append(
|
||||
'<li><a class="stdlink" href="mailto:?bcc=%s">Envoyer un mail collectif au groupe de %s (via %d adresses personnelles)</a></li>'
|
||||
% (
|
||||
",".join(amail_perso),
|
||||
groups_infos.groups_titles,
|
||||
len(amail_perso),
|
||||
)
|
||||
f"""<li>
|
||||
<a class="stdlink" href="mailto:?bcc={','.join(amail_perso)
|
||||
}">Envoyer un mail collectif au groupe de {groups_infos.groups_titles}
|
||||
(via {len(amail_perso)} adresses personnelles)</a>
|
||||
</li>"""
|
||||
)
|
||||
else:
|
||||
H.append("<li><em>Adresses personnelles non renseignées</em></li>")
|
||||
else:
|
||||
H.append(
|
||||
"""<li class="unauthorized">adresses mail personnelles protégées</li>"""
|
||||
)
|
||||
|
||||
H.append("</ul>")
|
||||
|
||||
@ -772,6 +792,10 @@ def groups_table(
|
||||
filename = "liste_%s" % groups_infos.groups_filename
|
||||
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, scu.XLSX_MIMETYPE)
|
||||
elif fmt == "allxls":
|
||||
if not can_view_etud_data:
|
||||
raise ScoPermissionDenied(
|
||||
"Vous n'avez pas la permission requise (ViewEtudData)"
|
||||
)
|
||||
# feuille Excel avec toutes les infos etudiants
|
||||
if not groups_infos.members:
|
||||
return ""
|
||||
@ -829,7 +853,9 @@ def groups_table(
|
||||
etud, groups_infos.formsemestre_id
|
||||
)
|
||||
m["parcours"] = Se.get_cursus_descr()
|
||||
m["code_cursus"], _ = sco_report.get_code_cursus_etud(etud)
|
||||
m["code_cursus"], _ = sco_report.get_code_cursus_etud(
|
||||
etud["etudid"], sems=etud["sems"]
|
||||
)
|
||||
rows = [[m.get(k, "") for k in keys] for m in groups_infos.members]
|
||||
title = "etudiants_%s" % groups_infos.groups_filename
|
||||
xls = sco_excel.excel_simple_table(titles=titles, lines=rows, sheet_name=title)
|
||||
@ -879,8 +905,9 @@ def tab_absences_html(groups_infos, etat=None):
|
||||
% groups_infos.groups_query_args,
|
||||
"""<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,
|
||||
f"""<li><a class="stdlink" href="groups_export_annotations?{groups_infos.groups_query_args}">Liste des annotations</a></li>"""
|
||||
if authuser.has_permission(Permission.ViewEtudData)
|
||||
else """<li class="unauthorized" title="non autorisé">Liste des annotations</li>""",
|
||||
"</ul>",
|
||||
]
|
||||
)
|
||||
@ -899,14 +926,19 @@ def tab_absences_html(groups_infos, etat=None):
|
||||
"""
|
||||
)
|
||||
# Lien pour ajout fichiers étudiants
|
||||
if authuser.has_permission(Permission.EtudAddAnnotations):
|
||||
text = "Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)"
|
||||
if authuser.has_permission(
|
||||
Permission.EtudAddAnnotations
|
||||
) and authuser.has_permission(Permission.ViewEtudData):
|
||||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for('scolar.etudarchive_import_files_form',
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
group_id=group_id
|
||||
)}">Télécharger des fichiers associés aux étudiants (e.g. dossiers d'admission)</a></li>"""
|
||||
)}">{text}</a></li>"""
|
||||
)
|
||||
else:
|
||||
H.append(f"""<li class="unauthorized" title="non autorisé">{text}</li>""")
|
||||
|
||||
H.append("</ul></div>")
|
||||
return "".join(H)
|
||||
|
@ -669,7 +669,7 @@ def etuds_select_boxes(
|
||||
elink = """<a class="discretelink %s" href="%s">%s</a>""" % (
|
||||
c,
|
||||
url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
),
|
||||
|
@ -29,6 +29,7 @@
|
||||
"""
|
||||
from collections import defaultdict
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import flask
|
||||
from flask import url_for, g, request
|
||||
@ -264,7 +265,7 @@ def _make_table_notes(
|
||||
if e.moduleimpl_id != modimpl.id:
|
||||
raise ValueError("invalid evaluations list")
|
||||
|
||||
if fmt == "xls":
|
||||
if fmt == "xls" or fmt == "json":
|
||||
keep_numeric = True # pas de conversion des notes en strings
|
||||
else:
|
||||
keep_numeric = False
|
||||
@ -279,11 +280,12 @@ def _make_table_notes(
|
||||
if anonymous_listing:
|
||||
columns_ids = ["code"] # cols in table
|
||||
else:
|
||||
if fmt == "xls" or fmt == "xml":
|
||||
columns_ids = ["nom", "prenom"]
|
||||
if fmt in {"xls", "xml", "json"}:
|
||||
columns_ids = ["etudid", "nom", "prenom"]
|
||||
else:
|
||||
columns_ids = ["nomprenom"]
|
||||
if not hide_groups:
|
||||
if not hide_groups and fmt not in {"xls", "xml", "json"}:
|
||||
# n'indique pas les groupes en xls, json car notation "humaine" ici
|
||||
columns_ids.append("group")
|
||||
|
||||
titles = {
|
||||
@ -476,7 +478,7 @@ def _make_table_notes(
|
||||
if with_emails:
|
||||
columns_ids += ["email", "emailperso"]
|
||||
# Ajoute lignes en tête et moyennes
|
||||
if len(evaluations) > 0 and fmt != "bordereau":
|
||||
if len(evaluations) > 0 and fmt != "bordereau" and fmt != "json":
|
||||
rows_head = [row_coefs]
|
||||
if is_apc:
|
||||
rows_head.append(row_poids)
|
||||
@ -683,7 +685,7 @@ def _make_table_notes(
|
||||
def _add_eval_columns(
|
||||
evaluation: Evaluation,
|
||||
eval_state,
|
||||
evals_poids,
|
||||
evals_poids: pd.DataFrame | None,
|
||||
ues,
|
||||
rows,
|
||||
titles,
|
||||
@ -833,14 +835,22 @@ def _add_eval_columns(
|
||||
return notes, nb_abs, nb_att # pour histogramme
|
||||
|
||||
|
||||
def _mini_table_eval_ue_poids(evaluation_id, evals_poids, ues):
|
||||
def _mini_table_eval_ue_poids(
|
||||
evaluation_id: int, evals_poids: pd.DataFrame, ues
|
||||
) -> str:
|
||||
"contenu de la cellule: poids"
|
||||
ue_poids = [
|
||||
(ue.acronyme, evals_poids[ue.id][evaluation_id])
|
||||
for ue in ues
|
||||
if (evals_poids[ue.id][evaluation_id] or 0) > 0
|
||||
]
|
||||
|
||||
return (
|
||||
"""<table class="eval_poids" title="poids vers les UE"><tr><td>"""
|
||||
+ "</td><td>".join([f"{ue.acronyme}" for ue in ues])
|
||||
+ "</td><td>".join([f"{up[0]}" for up in ue_poids])
|
||||
+ "</td></tr>"
|
||||
+ "<tr><td>"
|
||||
+ "</td><td>".join([f"{evals_poids[ue.id][evaluation_id]}" for ue in ues])
|
||||
+ "</td><td>".join([f"{up[1]}" for up in ue_poids])
|
||||
+ "</td></tr></table>"
|
||||
)
|
||||
|
||||
|
@ -143,7 +143,9 @@ def _table_etuds_lycees(etuds, group_lycees, title, preferences, no_links=False)
|
||||
if not no_links:
|
||||
for etud in etuds:
|
||||
fiche_url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
)
|
||||
etud["_nom_target"] = fiche_url
|
||||
etud["_prenom_target"] = fiche_url
|
||||
@ -232,7 +234,7 @@ def js_coords_lycees(etuds_by_lycee):
|
||||
'<a class="discretelink" href="%s" title="">%s</a>'
|
||||
% (
|
||||
url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=e["etudid"],
|
||||
),
|
||||
|
@ -40,6 +40,7 @@ from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import (
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ModuleImpl,
|
||||
Partition,
|
||||
ScolarFormSemestreValidation,
|
||||
UniteEns,
|
||||
@ -52,7 +53,6 @@ from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_edit_module
|
||||
from app.scodoc import sco_edit_ue
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_inscriptions
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_moduleimpl
|
||||
@ -63,7 +63,9 @@ import app.scodoc.sco_utils as scu
|
||||
from app.tables import list_etuds
|
||||
|
||||
|
||||
def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
def moduleimpl_inscriptions_edit(
|
||||
moduleimpl_id, etudids: list[int] | None = None, submitted=False
|
||||
):
|
||||
"""Formulaire inscription des etudiants a ce module
|
||||
* Gestion des inscriptions
|
||||
Nom TD TA TP (triable)
|
||||
@ -75,12 +77,12 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
|
||||
* Si pas les droits: idem en readonly
|
||||
"""
|
||||
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
etudids = etudids or []
|
||||
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
|
||||
module = modimpl.module
|
||||
formsemestre = modimpl.formsemestre
|
||||
# -- check lock
|
||||
if not sem["etat"]:
|
||||
if not formsemestre.etat:
|
||||
raise ScoValueError("opération impossible: semestre verrouille")
|
||||
header = html_sco_header.sco_header(
|
||||
page_title="Inscription au module",
|
||||
@ -90,25 +92,23 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
footer = html_sco_header.sco_footer()
|
||||
H = [
|
||||
header,
|
||||
"""<h2>Inscriptions au module <a href="moduleimpl_status?moduleimpl_id=%s">%s</a> (%s)</a></h2>
|
||||
f"""<h2>Inscriptions au module <a class="stdlink" href="{
|
||||
url_for("notes.moduleimpl_status", scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=moduleimpl_id)
|
||||
}">{module.titre or "(module sans titre)"}</a> ({module.code})</a></h2>
|
||||
<p class="help">Cette page permet d'éditer les étudiants inscrits à ce module
|
||||
(ils doivent évidemment être inscrits au semestre).
|
||||
Les étudiants cochés sont (ou seront) inscrits. Vous pouvez facilement inscrire ou
|
||||
Les étudiants cochés sont (ou seront) inscrits. Vous pouvez inscrire ou
|
||||
désinscrire tous les étudiants d'un groupe à l'aide des menus "Ajouter" et "Enlever".
|
||||
</p>
|
||||
<p class="help">Aucune modification n'est prise en compte tant que l'on n'appuie pas sur le bouton
|
||||
"Appliquer les modifications".
|
||||
<p class="help">Aucune modification n'est prise en compte tant que l'on n'appuie pas
|
||||
sur le bouton "Appliquer les modifications".
|
||||
</p>
|
||||
"""
|
||||
% (
|
||||
moduleimpl_id,
|
||||
mod["titre"] or "(module sans titre)",
|
||||
mod["code"] or "(module sans code)",
|
||||
),
|
||||
""",
|
||||
]
|
||||
# Liste des inscrits à ce semestre
|
||||
inscrits = sco_formsemestre_inscriptions.do_formsemestre_inscription_listinscrits(
|
||||
formsemestre_id
|
||||
formsemestre.id
|
||||
)
|
||||
for ins in inscrits:
|
||||
etuds_info = sco_etud.get_etud_info(etudid=ins["etudid"], filled=1)
|
||||
@ -121,12 +121,10 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
)
|
||||
ins["etud"] = etuds_info[0]
|
||||
inscrits.sort(key=lambda inscr: sco_etud.etud_sort_key(inscr["etud"]))
|
||||
in_m = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||
moduleimpl_id=M["moduleimpl_id"]
|
||||
)
|
||||
in_module = set([x["etudid"] for x in in_m])
|
||||
in_m = sco_moduleimpl.do_moduleimpl_inscription_list(moduleimpl_id=modimpl.id)
|
||||
in_module = {x["etudid"] for x in in_m}
|
||||
#
|
||||
partitions = sco_groups.get_partitions_list(formsemestre_id)
|
||||
partitions = sco_groups.get_partitions_list(formsemestre.id)
|
||||
#
|
||||
if not submitted:
|
||||
H.append(
|
||||
@ -149,27 +147,32 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
}
|
||||
}
|
||||
|
||||
</script>"""
|
||||
</script>
|
||||
<style>
|
||||
table.mi_table td, table.mi_table th {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
)
|
||||
H.append(
|
||||
f"""<form method="post" id="mi_form" action="{request.base_url}">
|
||||
<input type="hidden" name="moduleimpl_id" value="{M['moduleimpl_id']}"/>
|
||||
<input type="hidden" name="moduleimpl_id" value="{modimpl.id}"/>
|
||||
<input type="submit" name="submitted" value="Appliquer les modifications"/>
|
||||
<p></p>
|
||||
<table><tr>
|
||||
<div>
|
||||
{ _make_menu(partitions, "Ajouter", "true") }
|
||||
{ _make_menu(partitions, "Enlever", "false")}
|
||||
</tr></table>
|
||||
<p><br></p>
|
||||
<table class="sortable" id="mi_table">
|
||||
</div>
|
||||
<table class="gt_table mi_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th class="etud">Nom</th>
|
||||
"""
|
||||
)
|
||||
for partition in partitions:
|
||||
if partition["partition_name"]:
|
||||
H.append("<th>%s</th>" % partition["partition_name"])
|
||||
H.append("</tr>")
|
||||
H.append(f"<th>{partition['partition_name']}</th>")
|
||||
H.append("</tr></thead><tbody>")
|
||||
|
||||
for ins in inscrits:
|
||||
etud = ins["etud"]
|
||||
@ -178,24 +181,20 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
else:
|
||||
checked = ""
|
||||
H.append(
|
||||
"""<tr><td><input type="checkbox" name="etuds:list" value="%s" %s>"""
|
||||
% (etud["etudid"], checked)
|
||||
f"""<tr><td class="etud"><input type="checkbox" name="etudids:list" value="{etud['etudid']}" {checked}>"""
|
||||
)
|
||||
H.append(
|
||||
"""<a class="discretelink etudinfo" href="%s" id="%s">%s</a>"""
|
||||
% (
|
||||
f"""<a class="discretelink etudinfo" href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
),
|
||||
etud["etudid"],
|
||||
etud["nomprenom"],
|
||||
)
|
||||
}" id="{etud['etudid']}">{etud['nomprenom']}</a>"""
|
||||
)
|
||||
H.append("""</input></td>""")
|
||||
|
||||
groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre_id)
|
||||
groups = sco_groups.get_etud_groups(etud["etudid"], formsemestre.id)
|
||||
for partition in partitions:
|
||||
if partition["partition_name"]:
|
||||
gr_name = ""
|
||||
@ -205,11 +204,11 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
break
|
||||
# gr_name == '' si etud non inscrit dans un groupe de cette partition
|
||||
H.append(f"<td>{gr_name}</td>")
|
||||
H.append("""</table></form>""")
|
||||
H.append("""</tbody></table></form>""")
|
||||
else: # SUBMISSION
|
||||
# inscrit a ce module tous les etuds selectionnes
|
||||
sco_moduleimpl.do_moduleimpl_inscrit_etuds(
|
||||
moduleimpl_id, formsemestre_id, etuds, reset=True
|
||||
moduleimpl_id, formsemestre.id, etudids, reset=True
|
||||
)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
@ -225,10 +224,10 @@ def moduleimpl_inscriptions_edit(moduleimpl_id, etuds=[], submitted=False):
|
||||
|
||||
def _make_menu(partitions: list[dict], title="", check="true") -> str:
|
||||
"""Menu with list of all groups"""
|
||||
items = [{"title": "Tous", "attr": "onclick=\"group_select('', -1, %s)\"" % check}]
|
||||
items = [{"title": "Tous", "attr": f"onclick=\"group_select('', -1, {check})\""}]
|
||||
p_idx = 0
|
||||
for partition in partitions:
|
||||
if partition["partition_name"] != None:
|
||||
if partition["partition_name"] is not None:
|
||||
p_idx += 1
|
||||
for group in sco_groups.get_partition_groups(partition):
|
||||
items.append(
|
||||
@ -240,9 +239,9 @@ def _make_menu(partitions: list[dict], title="", check="true") -> str:
|
||||
}
|
||||
)
|
||||
return (
|
||||
'<td class="inscr_addremove_menu">'
|
||||
'<div class="inscr_addremove_menu">'
|
||||
+ htmlutils.make_menu(title, items, alone=True)
|
||||
+ "</td>"
|
||||
+ "</div>"
|
||||
)
|
||||
|
||||
|
||||
@ -420,9 +419,11 @@ def moduleimpl_inscriptions_stats(formsemestre_id):
|
||||
for info in ues_cap_info[ue["ue_id"]]:
|
||||
etud = sco_etud.get_etud_info(etudid=info["etudid"], filled=True)[0]
|
||||
H.append(
|
||||
f"""<li class="etud"><a class="discretelink" href="{
|
||||
f"""<li class="etud"><a class="discretelink etudinfo"
|
||||
id="{info['etudid']}"
|
||||
href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud["etudid"],
|
||||
)
|
||||
@ -543,7 +544,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
||||
H.append(
|
||||
f"""<tr><td><a class="discretelink etudinfo" id={etud.id}
|
||||
href="{url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=etud.id,
|
||||
)}"
|
||||
@ -695,7 +696,7 @@ def _fmt_etud_set(etudids, max_list_size=7) -> str:
|
||||
[
|
||||
f"""<a class="discretelink" href="{
|
||||
url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud.id
|
||||
)
|
||||
}">{etud.nomprenom}</a>"""
|
||||
for etud in sorted(etuds, key=attrgetter("sort_key"))
|
||||
|
@ -134,7 +134,8 @@ def moduleimpl_evaluation_menu(evaluation: Evaluation, nbnotes: int = 0) -> str:
|
||||
if evaluation.date_fin
|
||||
else "",
|
||||
},
|
||||
"enabled": evaluation.date_debut is not None,
|
||||
"enabled": evaluation.date_debut is not None
|
||||
and evaluation.date_fin is not None,
|
||||
},
|
||||
{
|
||||
"title": "Vérifier notes vs absents",
|
||||
@ -167,6 +168,7 @@ def _ue_coefs_html(coefs_lst) -> str:
|
||||
{'background-color: ' + ue.color + ';' if ue.color else ''}
|
||||
"><div>{coef}</div>{ue.acronyme}</div>"""
|
||||
for ue, coef in coefs_lst
|
||||
if coef > 0
|
||||
]
|
||||
)
|
||||
+ "</div>"
|
||||
|
@ -25,38 +25,37 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""ScoDoc ficheEtud
|
||||
"""ScoDoc fiche_etud
|
||||
|
||||
Fiche description d'un étudiant et de son parcours
|
||||
|
||||
"""
|
||||
from flask import abort, url_for, g, render_template, request
|
||||
from flask import url_for, g, render_template, request
|
||||
from flask_login import current_user
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import db, log
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.but import cursus_but
|
||||
from app.models.etudiants import make_etud_args
|
||||
from app.models import Identite, FormSemestre, ScoDocSiteConfig
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import htmlutils
|
||||
from app.scodoc import sco_archives_etud
|
||||
from app.scodoc import sco_bac
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_status
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_cursus
|
||||
from app.scodoc import sco_permissions_check
|
||||
from app.scodoc import sco_photos
|
||||
from app.scodoc import sco_users
|
||||
from app.scodoc import sco_report
|
||||
from app.scodoc import sco_etud
|
||||
from app.models import Adresse, EtudAnnotation, FormSemestre, Identite, ScoDocSiteConfig
|
||||
from app.scodoc import (
|
||||
codes_cursus,
|
||||
html_sco_header,
|
||||
htmlutils,
|
||||
sco_archives_etud,
|
||||
sco_bac,
|
||||
sco_cursus,
|
||||
sco_etud,
|
||||
sco_groups,
|
||||
sco_permissions_check,
|
||||
sco_report,
|
||||
)
|
||||
from app.scodoc.html_sidebar import retreive_formsemestre_from_request
|
||||
from app.scodoc.sco_bulletins import etud_descr_situation_semestre
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
|
||||
|
||||
def _menu_scolarite(
|
||||
@ -157,29 +156,18 @@ def _menu_scolarite(
|
||||
)
|
||||
|
||||
|
||||
def ficheEtud(etudid=None):
|
||||
def fiche_etud(etudid=None):
|
||||
"fiche d'informations sur un etudiant"
|
||||
authuser = current_user
|
||||
cnx = ndb.GetDBConnexion()
|
||||
if etudid:
|
||||
try: # pour les bookmarks avec d'anciens ids...
|
||||
etudid = int(etudid)
|
||||
except ValueError:
|
||||
raise ScoValueError("id invalide !") from ValueError
|
||||
restrict_etud_data = not current_user.has_permission(Permission.ViewEtudData)
|
||||
try:
|
||||
etud = Identite.get_etud(etudid)
|
||||
except Exception as exc:
|
||||
log(f"fiche_etud: etudid={etudid!r} request.args={request.args!r}")
|
||||
raise ScoValueError("Étudiant inexistant !") from exc
|
||||
# la sidebar est differente s'il y a ou pas un etudid
|
||||
# voir html_sidebar.sidebar()
|
||||
g.etudid = etudid
|
||||
args = make_etud_args(etudid=etudid)
|
||||
etuds = sco_etud.etudident_list(cnx, args)
|
||||
if not etuds:
|
||||
log(f"ficheEtud: etudid={etudid!r} request.args={request.args!r}")
|
||||
raise ScoValueError("Étudiant inexistant !")
|
||||
etud_ = etuds[0] # transition: etud_ à éliminer et remplacer par etud
|
||||
etudid = etud_["etudid"]
|
||||
etud = Identite.get_etud(etudid)
|
||||
sco_etud.fill_etuds_info([etud_])
|
||||
#
|
||||
info = etud_
|
||||
info = etud.to_dict_scodoc7(restrict=restrict_etud_data)
|
||||
if etud.prenom_etat_civil:
|
||||
info["etat_civil"] = (
|
||||
"<h3>Etat-civil: "
|
||||
@ -193,45 +181,26 @@ def ficheEtud(etudid=None):
|
||||
else:
|
||||
info["etat_civil"] = ""
|
||||
info["ScoURL"] = scu.ScoURL()
|
||||
info["authuser"] = authuser
|
||||
info["authuser"] = current_user
|
||||
if restrict_etud_data:
|
||||
info["info_naissance"] = ""
|
||||
adresse = None
|
||||
else:
|
||||
info["info_naissance"] = info["date_naissance"]
|
||||
if info["lieu_naissance"]:
|
||||
info["info_naissance"] += " à " + info["lieu_naissance"]
|
||||
if info["dept_naissance"]:
|
||||
info["info_naissance"] += f" ({info['dept_naissance']})"
|
||||
info["etudfoto"] = sco_photos.etud_photo_html(etud_)
|
||||
if (
|
||||
(not info["domicile"])
|
||||
and (not info["codepostaldomicile"])
|
||||
and (not info["villedomicile"])
|
||||
):
|
||||
info["domicile"] = "<em>inconnue</em>"
|
||||
if info["paysdomicile"]:
|
||||
pays = sco_etud.format_pays(info["paysdomicile"])
|
||||
if pays:
|
||||
info["paysdomicile"] = "(%s)" % pays
|
||||
else:
|
||||
info["paysdomicile"] = ""
|
||||
if info["telephone"] or info["telephonemobile"]:
|
||||
info["telephones"] = "<br>%s %s" % (
|
||||
info["telephonestr"],
|
||||
info["telephonemobilestr"],
|
||||
)
|
||||
else:
|
||||
info["telephones"] = ""
|
||||
# e-mail:
|
||||
if info["email_default"]:
|
||||
info["emaillink"] = ", ".join(
|
||||
[
|
||||
'<a class="stdlink" href="mailto:%s">%s</a>' % (m, m)
|
||||
for m in [etud_["email"], etud_["emailperso"]]
|
||||
if m
|
||||
]
|
||||
)
|
||||
else:
|
||||
info["emaillink"] = "<em>(pas d'adresse e-mail)</em>"
|
||||
adresse = etud.adresses.first()
|
||||
info.update(_format_adresse(adresse))
|
||||
|
||||
info.update(etud.inscription_descr())
|
||||
info["etudfoto"] = etud.photo_html()
|
||||
|
||||
# Champ dépendant des permissions:
|
||||
if authuser.has_permission(Permission.EtudChangeAdr):
|
||||
if current_user.has_permission(
|
||||
Permission.EtudChangeAdr
|
||||
) and current_user.has_permission(Permission.ViewEtudData):
|
||||
info[
|
||||
"modifadresse"
|
||||
] = f"""<a class="stdlink" href="{
|
||||
@ -242,36 +211,33 @@ def ficheEtud(etudid=None):
|
||||
info["modifadresse"] = ""
|
||||
|
||||
# Groupes:
|
||||
inscription_courante = etud.inscription_courante()
|
||||
sco_groups.etud_add_group_infos(
|
||||
info,
|
||||
info["cursem"]["formsemestre_id"] if info["cursem"] else None,
|
||||
inscription_courante.formsemestre.id if inscription_courante else None,
|
||||
only_to_show=True,
|
||||
)
|
||||
# Parcours de l'étudiant
|
||||
if info["sems"]:
|
||||
info["last_formsemestre_id"] = info["sems"][0]["formsemestre_id"]
|
||||
else:
|
||||
info["last_formsemestre_id"] = ""
|
||||
sem_info = {}
|
||||
for sem in info["sems"]:
|
||||
formsemestre: FormSemestre = db.session.get(
|
||||
FormSemestre, sem["formsemestre_id"]
|
||||
last_formsemestre = None
|
||||
inscriptions = etud.inscriptions()
|
||||
info["last_formsemestre_id"] = (
|
||||
inscriptions[0].formsemestre.id if inscriptions else ""
|
||||
)
|
||||
if sem["ins"]["etat"] != scu.INSCRIT:
|
||||
|
||||
sem_info = {}
|
||||
for inscription in inscriptions:
|
||||
formsemestre = inscription.formsemestre
|
||||
if inscription.etat != scu.INSCRIT:
|
||||
descr, _ = etud_descr_situation_semestre(
|
||||
etudid,
|
||||
formsemestre,
|
||||
info["ne"],
|
||||
etud.e,
|
||||
show_date_inscr=False,
|
||||
)
|
||||
grlink = f"""<span class="fontred">{descr["situation"]}</span>"""
|
||||
else:
|
||||
e = {"etudid": etudid}
|
||||
sco_groups.etud_add_group_infos(
|
||||
e,
|
||||
sem["formsemestre_id"],
|
||||
only_to_show=True,
|
||||
)
|
||||
sco_groups.etud_add_group_infos(e, formsemestre.id, only_to_show=True)
|
||||
|
||||
grlinks = []
|
||||
for partition in e["partitions"].values():
|
||||
@ -289,16 +255,16 @@ def ficheEtud(etudid=None):
|
||||
)
|
||||
grlink = ", ".join(grlinks)
|
||||
# infos ajoutées au semestre dans le parcours (groupe, menu)
|
||||
menu = _menu_scolarite(authuser, formsemestre, etudid, sem["ins"]["etat"])
|
||||
menu = _menu_scolarite(current_user, formsemestre, etudid, inscription.etat)
|
||||
if menu:
|
||||
sem_info[sem["formsemestre_id"]] = (
|
||||
sem_info[formsemestre.id] = (
|
||||
"<table><tr><td>" + grlink + "</td><td>" + menu + "</td></tr></table>"
|
||||
)
|
||||
else:
|
||||
sem_info[sem["formsemestre_id"]] = grlink
|
||||
sem_info[formsemestre.id] = grlink
|
||||
|
||||
if info["sems"]:
|
||||
Se = sco_cursus.get_situation_etud_cursus(etud_, info["last_formsemestre_id"])
|
||||
if inscriptions:
|
||||
Se = sco_cursus.get_situation_etud_cursus(info, info["last_formsemestre_id"])
|
||||
info["liste_inscriptions"] = formsemestre_recap_parcours_table(
|
||||
Se,
|
||||
etudid,
|
||||
@ -318,20 +284,19 @@ def ficheEtud(etudid=None):
|
||||
</span>
|
||||
"""
|
||||
)
|
||||
last_formsemestre: FormSemestre = db.session.get(
|
||||
FormSemestre, info["sems"][0]["formsemestre_id"]
|
||||
)
|
||||
last_formsemestre: FormSemestre = inscriptions[0].formsemestre
|
||||
if last_formsemestre.formation.is_apc() and last_formsemestre.semestre_id > 2:
|
||||
info[
|
||||
"link_bul_pdf"
|
||||
] += f"""
|
||||
<span class="link_bul_pdf">
|
||||
<a class="stdlink" href="{
|
||||
url_for("notes.validation_rcues", scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
|
||||
url_for("notes.validation_rcues",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=last_formsemestre.id)
|
||||
}">Visualiser les compétences BUT</a>
|
||||
</span>
|
||||
"""
|
||||
if authuser.has_permission(Permission.EtudInscrit):
|
||||
if current_user.has_permission(Permission.EtudInscrit):
|
||||
info[
|
||||
"link_inscrire_ailleurs"
|
||||
] = f"""<span class="link_bul_pdf"><a class="stdlink" href="{
|
||||
@ -348,8 +313,8 @@ def ficheEtud(etudid=None):
|
||||
info["link_inscrire_ailleurs"] = ""
|
||||
else:
|
||||
# non inscrit
|
||||
l = [f"""<p><b>Étudiant{info["ne"]} non inscrit{info["ne"]}"""]
|
||||
if authuser.has_permission(Permission.EtudInscrit):
|
||||
l = [f"""<p><b>Étudiant{etud.e} non inscrit{etud.e}"""]
|
||||
if current_user.has_permission(Permission.EtudInscrit):
|
||||
l.append(
|
||||
f"""<a href="{
|
||||
url_for("notes.formsemestre_inscription_with_modules_form",
|
||||
@ -362,44 +327,50 @@ def ficheEtud(etudid=None):
|
||||
info["link_inscrire_ailleurs"] = ""
|
||||
|
||||
# Liste des annotations
|
||||
alist = []
|
||||
annos = sco_etud.etud_annotations_list(cnx, args={"etudid": etudid})
|
||||
for a in annos:
|
||||
if not sco_permissions_check.can_suppress_annotation(a["id"]):
|
||||
a["dellink"] = ""
|
||||
else:
|
||||
a["dellink"] = (
|
||||
'<td class="annodel"><a href="doSuppressAnnotation?etudid=%s&annotation_id=%s">%s</a></td>'
|
||||
% (
|
||||
etudid,
|
||||
a["id"],
|
||||
annotations_list = []
|
||||
annotations = EtudAnnotation.query.filter_by(etudid=etud.id).order_by(
|
||||
sa.desc(EtudAnnotation.date)
|
||||
)
|
||||
for annot in annotations:
|
||||
del_link = (
|
||||
f"""<td class="annodel"><a href="{
|
||||
url_for("scolar.doSuppressAnnotation",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid, annotation_id=annot.id)}">{
|
||||
scu.icontag(
|
||||
"delete_img",
|
||||
border="0",
|
||||
alt="suppress",
|
||||
title="Supprimer cette annotation",
|
||||
),
|
||||
)
|
||||
}</a></td>"""
|
||||
if sco_permissions_check.can_suppress_annotation(annot.id)
|
||||
else ""
|
||||
)
|
||||
author = sco_users.user_info(a["author"])
|
||||
alist.append(
|
||||
f"""<tr><td><span class="annodate">Le {a['date']} par {author['prenomnom']} :
|
||||
</span><span class="annoc">{a['comment']}</span></td>{a['dellink']}</tr>
|
||||
|
||||
author = User.query.filter_by(user_name=annot.author).first()
|
||||
annotations_list.append(
|
||||
f"""<tr><td><span class="annodate">Le {annot.date.strftime("%d/%m/%Y") if annot.date else "?"}
|
||||
par {author.get_prenomnom() if author else "?"} :
|
||||
</span><span class="annoc">{annot.comment or ""}</span></td>{del_link}</tr>
|
||||
"""
|
||||
)
|
||||
info["liste_annotations"] = "\n".join(alist)
|
||||
info["liste_annotations"] = "\n".join(annotations_list)
|
||||
# fiche admission
|
||||
has_adm_notes = (
|
||||
info["math"] or info["physique"] or info["anglais"] or info["francais"]
|
||||
infos_admission = _infos_admission(etud, restrict_etud_data)
|
||||
has_adm_notes = any(
|
||||
infos_admission[k] for k in ("math", "physique", "anglais", "francais")
|
||||
)
|
||||
has_bac_info = any(
|
||||
infos_admission[k]
|
||||
for k in (
|
||||
"bac_specialite",
|
||||
"annee_bac",
|
||||
"rapporteur",
|
||||
"commentaire",
|
||||
"classement",
|
||||
"type_admission",
|
||||
"rap",
|
||||
)
|
||||
has_bac_info = (
|
||||
info["bac"]
|
||||
or info["specialite"]
|
||||
or info["annee_bac"]
|
||||
or info["rapporteur"]
|
||||
or info["commentaire"]
|
||||
or info["classement"]
|
||||
or info["type_admission"]
|
||||
)
|
||||
if has_bac_info or has_adm_notes:
|
||||
adm_tmpl = """<!-- Donnees admission -->
|
||||
@ -411,7 +382,7 @@ def ficheEtud(etudid=None):
|
||||
<tr><th>Bac</th><th>Année</th><th>Rg</th>
|
||||
<th>Math</th><th>Physique</th><th>Anglais</th><th>Français</th></tr>
|
||||
<tr>
|
||||
<td>%(bac)s (%(specialite)s)</td>
|
||||
<td>%(bac_specialite)s</td>
|
||||
<td>%(annee_bac)s </td>
|
||||
<td>%(classement)s</td>
|
||||
<td>%(math)s</td><td>%(physique)s</td><td>%(anglais)s</td><td>%(francais)s</td>
|
||||
@ -419,28 +390,32 @@ def ficheEtud(etudid=None):
|
||||
</table>
|
||||
"""
|
||||
adm_tmpl += """
|
||||
<div>Bac %(bac)s (%(specialite)s) obtenu en %(annee_bac)s </div>
|
||||
<div class="ilycee">%(ilycee)s</div>"""
|
||||
if info["type_admission"] or info["classement"]:
|
||||
<div>Bac %(bac_specialite)s obtenu en %(annee_bac)s </div>
|
||||
<div class="info_lycee">%(info_lycee)s</div>"""
|
||||
if infos_admission["type_admission"] or infos_admission["classement"]:
|
||||
adm_tmpl += """<div class="vadmission">"""
|
||||
if info["type_admission"]:
|
||||
if infos_admission["type_admission"]:
|
||||
adm_tmpl += """<span>Voie d'admission: <span class="etud_type_admission">%(type_admission)s</span></span> """
|
||||
if info["classement"]:
|
||||
if infos_admission["classement"]:
|
||||
adm_tmpl += """<span>Rang admission: <span class="etud_type_admission">%(classement)s</span></span>"""
|
||||
if info["type_admission"] or info["classement"]:
|
||||
if infos_admission["type_admission"] or infos_admission["classement"]:
|
||||
adm_tmpl += "</div>"
|
||||
if info["rap"]:
|
||||
if infos_admission["rap"]:
|
||||
adm_tmpl += """<div class="note_rapporteur">%(rap)s</div>"""
|
||||
adm_tmpl += """</div>"""
|
||||
else:
|
||||
adm_tmpl = "" # pas de boite "info admission"
|
||||
info["adm_data"] = adm_tmpl % info
|
||||
info["adm_data"] = adm_tmpl % infos_admission
|
||||
|
||||
# Fichiers archivés:
|
||||
info["fichiers_archive_htm"] = (
|
||||
""
|
||||
if restrict_etud_data
|
||||
else (
|
||||
'<div class="fichetitre">Fichiers associés</div>'
|
||||
+ sco_archives_etud.etud_list_archives_html(etud)
|
||||
)
|
||||
)
|
||||
|
||||
# Devenir de l'étudiant:
|
||||
has_debouche = True
|
||||
@ -455,18 +430,16 @@ def ficheEtud(etudid=None):
|
||||
if has_debouche:
|
||||
info[
|
||||
"debouche_html"
|
||||
] = """<div id="fichedebouche" data-readonly="%s" data-etudid="%s">
|
||||
] = f"""<div id="fichedebouche"
|
||||
data-readonly="{suivi_readonly}"
|
||||
data-etudid="{info['etudid']}">
|
||||
<span class="debouche_tit">Devenir:</span>
|
||||
<div><form>
|
||||
<ul class="listdebouches">
|
||||
%s
|
||||
{link_add_suivi}
|
||||
</ul>
|
||||
</form></div>
|
||||
</div>""" % (
|
||||
suivi_readonly,
|
||||
info["etudid"],
|
||||
link_add_suivi,
|
||||
)
|
||||
</div>"""
|
||||
else:
|
||||
info["debouche_html"] = "" # pas de boite "devenir"
|
||||
#
|
||||
@ -492,20 +465,14 @@ def ficheEtud(etudid=None):
|
||||
else:
|
||||
info["groupes_row"] = ""
|
||||
info["menus_etud"] = menus_etud(etudid)
|
||||
if info["boursier"]:
|
||||
if info["boursier"] and not restrict_etud_data:
|
||||
info["bourse_span"] = """<span class="boursier">boursier</span>"""
|
||||
else:
|
||||
info["bourse_span"] = ""
|
||||
|
||||
# raccordement provisoire pour juillet 2022, avant refonte complète de cette fiche...
|
||||
# info["but_infos_mkup"] = jury_but_view.infos_fiche_etud_html(etudid)
|
||||
|
||||
# XXX dev
|
||||
info["but_cursus_mkup"] = ""
|
||||
if info["sems"]:
|
||||
last_sem = FormSemestre.query.get_or_404(info["sems"][0]["formsemestre_id"])
|
||||
if last_sem.formation.is_apc():
|
||||
but_cursus = cursus_but.EtudCursusBUT(etud, last_sem.formation)
|
||||
# Liens vers compétences BUT
|
||||
if last_formsemestre and last_formsemestre.formation.is_apc():
|
||||
but_cursus = cursus_but.EtudCursusBUT(etud, last_formsemestre.formation)
|
||||
info[
|
||||
"but_cursus_mkup"
|
||||
] = f"""
|
||||
@ -516,46 +483,74 @@ def ficheEtud(etudid=None):
|
||||
scu=scu,
|
||||
)}
|
||||
<div class="link_validation_rcues">
|
||||
<a href="{url_for("notes.validation_rcues",
|
||||
<a class="stdlink" href="{url_for("notes.validation_rcues",
|
||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
||||
formsemestre_id=last_formsemestre.id)}"
|
||||
title="Visualiser les compétences BUT"
|
||||
>
|
||||
<img src="/ScoDoc/static/icons/parcours-but.png" alt="validation_rcues" height="100px"/>
|
||||
<div>Compétences BUT</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
else:
|
||||
info["but_cursus_mkup"] = ""
|
||||
|
||||
tmpl = """<div class="menus_etud">%(menus_etud)s</div>
|
||||
<div class="ficheEtud" id="ficheEtud"><table>
|
||||
<tr><td>
|
||||
<h2>%(nomprenom)s (%(inscription)s)</h2>
|
||||
%(etat_civil)s
|
||||
<span>%(emaillink)s</span>
|
||||
</td><td class="photocell">
|
||||
<a href="etud_photo_orig_page?etudid=%(etudid)s">%(etudfoto)s</a>
|
||||
</td></tr></table>
|
||||
adresse_template = (
|
||||
""
|
||||
if restrict_etud_data
|
||||
else """
|
||||
<!-- Adresse -->
|
||||
<div class="ficheadresse" id="ficheadresse">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="fichetitre2">Adresse :</td>
|
||||
<td> %(domicile)s %(codepostaldomicile)s %(villedomicile)s %(paysdomicile)s
|
||||
%(modifadresse)s
|
||||
%(telephones)s
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
info_naissance = (
|
||||
f"""<tr><td class="fichetitre2">Né{etud.e} le :</td><td>{info["info_naissance"]}</td></tr>"""
|
||||
if info["info_naissance"]
|
||||
else ""
|
||||
)
|
||||
situation_template = (
|
||||
f"""
|
||||
<div class="fichesituation">
|
||||
<div class="fichetablesitu">
|
||||
<table>
|
||||
<tr><td class="fichetitre2">Situation :</td><td>%(situation)s %(bourse_span)s</td></tr>
|
||||
%(groupes_row)s
|
||||
<tr><td class="fichetitre2">Né%(ne)s le :</td><td>%(info_naissance)s</td></tr>
|
||||
{info_naissance}
|
||||
</table>
|
||||
"""
|
||||
+ adresse_template
|
||||
+ """
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
<!-- Adresse -->
|
||||
<div class="ficheadresse" id="ficheadresse">
|
||||
<table><tr>
|
||||
<td class="fichetitre2">Adresse :</td><td> %(domicile)s %(codepostaldomicile)s %(villedomicile)s %(paysdomicile)s
|
||||
%(modifadresse)s
|
||||
%(telephones)s
|
||||
tmpl = (
|
||||
"""<div class="menus_etud">%(menus_etud)s</div>
|
||||
<div class="fiche_etud" id="fiche_etud"><table>
|
||||
<tr><td>
|
||||
<h2>%(nomprenom)s (%(inscription)s)</h2>
|
||||
%(etat_civil)s
|
||||
<span>%(email_link)s</span>
|
||||
</td><td class="photocell">
|
||||
<a href="etud_photo_orig_page?etudid=%(etudid)s">%(etudfoto)s</a>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
+ situation_template
|
||||
+ """
|
||||
|
||||
%(inscriptions_mkup)s
|
||||
|
||||
@ -595,8 +590,9 @@ def ficheEtud(etudid=None):
|
||||
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
header = html_sco_header.sco_header(
|
||||
page_title="Fiche étudiant %(prenom)s %(nom)s" % info,
|
||||
page_title=f"Fiche étudiant {etud.nomprenom}",
|
||||
cssstyles=[
|
||||
"libjs/jQuery-tagEditor/jquery.tag-editor.css",
|
||||
"css/jury_but.css",
|
||||
@ -614,6 +610,92 @@ def ficheEtud(etudid=None):
|
||||
return header + tmpl % info + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
def _format_adresse(adresse: Adresse | None) -> dict:
|
||||
"""{ "telephonestr" : ..., "telephonemobilestr" : ... } (formats html)"""
|
||||
d = {
|
||||
"telephonestr": ("<b>Tél.:</b> " + scu.format_telephone(adresse.telephone))
|
||||
if (adresse and adresse.telephone)
|
||||
else "",
|
||||
"telephonemobilestr": (
|
||||
"<b>Mobile:</b> " + scu.format_telephone(adresse.telephonemobile)
|
||||
)
|
||||
if (adresse and adresse.telephonemobile)
|
||||
else "",
|
||||
# e-mail:
|
||||
"email_link": ", ".join(
|
||||
[
|
||||
f"""<a class="stdlink" href="mailto:{m}">{m}</a>"""
|
||||
for m in [adresse.email, adresse.emailperso]
|
||||
if m
|
||||
]
|
||||
)
|
||||
if adresse and (adresse.email or adresse.emailperso)
|
||||
else "",
|
||||
"domicile": (adresse.domicile or "")
|
||||
if adresse
|
||||
and (adresse.domicile or adresse.codepostaldomicile or adresse.villedomicile)
|
||||
else "<em>inconnue</em>",
|
||||
"paysdomicile": f"{sco_etud.format_pays(adresse.paysdomicile)}"
|
||||
if adresse and adresse.paysdomicile
|
||||
else "",
|
||||
}
|
||||
d["telephones"] = (
|
||||
f"<br>{d['telephonestr']} {d['telephonemobilestr']}"
|
||||
if adresse and (adresse.telephone or adresse.telephonemobile)
|
||||
else ""
|
||||
)
|
||||
return d
|
||||
|
||||
|
||||
def _infos_admission(etud: Identite, restrict_etud_data: bool) -> dict:
|
||||
"""dict with adminission data, restricted or not"""
|
||||
# info sur rapporteur et son commentaire
|
||||
rap = ""
|
||||
if not restrict_etud_data:
|
||||
if etud.admission.rapporteur or etud.admission.commentaire:
|
||||
rap = "Note du rapporteur"
|
||||
if etud.admission.rapporteur:
|
||||
rap += f" ({etud.admission.rapporteur})"
|
||||
rap += ": "
|
||||
if etud.admission.commentaire:
|
||||
rap += f"<em>{etud.admission.commentaire}</em>"
|
||||
# nom du lycée
|
||||
if restrict_etud_data:
|
||||
info_lycee = ""
|
||||
elif etud.admission.nomlycee:
|
||||
info_lycee = "Lycée " + sco_etud.format_lycee(etud.admission.nomlycee)
|
||||
if etud.admission.villelycee:
|
||||
info_lycee += f" ({etud.admission.villelycee})"
|
||||
info_lycee += "<br>"
|
||||
elif etud.admission.codelycee:
|
||||
info_lycee = sco_etud.format_lycee_from_code(etud.admission.codelycee)
|
||||
else:
|
||||
info_lycee = ""
|
||||
|
||||
return {
|
||||
# infos accessibles à tous:
|
||||
"bac_specialite": f"{etud.admission.bac or ''}{(' '+(etud.admission.specialite or '')) if etud.admission.specialite else ''}",
|
||||
"annee_bac": etud.admission.annee_bac or "",
|
||||
# infos protégées par ViewEtudData:
|
||||
"info_lycee": info_lycee,
|
||||
"rapporteur": etud.admission.rapporteur if not restrict_etud_data else "",
|
||||
"rap": rap,
|
||||
"commentaire": (etud.admission.commentaire or "")
|
||||
if not restrict_etud_data
|
||||
else "",
|
||||
"classement": (etud.admission.classement or "")
|
||||
if not restrict_etud_data
|
||||
else "",
|
||||
"type_admission": (etud.admission.type_admission or "")
|
||||
if not restrict_etud_data
|
||||
else "",
|
||||
"math": (etud.admission.math or "") if not restrict_etud_data else "",
|
||||
"physique": (etud.admission.physique or "") if not restrict_etud_data else "",
|
||||
"anglais": (etud.admission.anglais or "") if not restrict_etud_data else "",
|
||||
"francais": (etud.admission.francais or "") if not restrict_etud_data else "",
|
||||
}
|
||||
|
||||
|
||||
def menus_etud(etudid):
|
||||
"""Menu etudiant (operations sur l'etudiant)"""
|
||||
authuser = current_user
|
||||
@ -623,7 +705,7 @@ def menus_etud(etudid):
|
||||
menuEtud = [
|
||||
{
|
||||
"title": etud["nomprenom"],
|
||||
"endpoint": "scolar.ficheEtud",
|
||||
"endpoint": "scolar.fiche_etud",
|
||||
"args": {"etudid": etud["etudid"]},
|
||||
"enabled": True,
|
||||
"helpmsg": "Fiche étudiant",
|
||||
@ -638,7 +720,8 @@ def menus_etud(etudid):
|
||||
"title": "Changer les données identité/admission",
|
||||
"endpoint": "scolar.etudident_edit_form",
|
||||
"args": {"etudid": etud["etudid"]},
|
||||
"enabled": authuser.has_permission(Permission.EtudInscrit),
|
||||
"enabled": authuser.has_permission(Permission.EtudInscrit)
|
||||
and authuser.has_permission(Permission.ViewEtudData),
|
||||
},
|
||||
{
|
||||
"title": "Copier dans un autre département...",
|
||||
@ -669,38 +752,39 @@ def etud_info_html(etudid, with_photo="1", debug=False):
|
||||
"""An HTML div with basic information and links about this etud.
|
||||
Used for popups information windows.
|
||||
"""
|
||||
formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request()
|
||||
formsemestre_id = retreive_formsemestre_from_request()
|
||||
with_photo = int(with_photo)
|
||||
etuds = sco_etud.get_etud_info(filled=True)
|
||||
if etuds:
|
||||
etud = etuds[0]
|
||||
else:
|
||||
abort(404, "etudiant inconnu")
|
||||
photo_html = sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"])
|
||||
# experimental: may be too slow to be here
|
||||
code_cursus, _ = sco_report.get_code_cursus_etud(etud, prefix="S", separator=", ")
|
||||
etud = Identite.get_etud(etudid)
|
||||
|
||||
bac = sco_bac.Baccalaureat(etud["bac"], etud["specialite"])
|
||||
photo_html = etud.photo_html(title="fiche de " + etud.nomprenom)
|
||||
code_cursus, _ = sco_report.get_code_cursus_etud(
|
||||
etud, formsemestres=etud.get_formsemestres(), prefix="S", separator=", "
|
||||
)
|
||||
bac = sco_bac.Baccalaureat(etud.admission.bac, etud.admission.specialite)
|
||||
bac_abbrev = bac.abbrev()
|
||||
H = f"""<div class="etud_info_div">
|
||||
<div class="eid_left">
|
||||
<div class="eid_nom"><div><a class="stdlink" target="_blank" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">{etud["nomprenom"]}</a></div></div>
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">{etud.nomprenom}</a></div></div>
|
||||
<div class="eid_info eid_bac">Bac: <span class="eid_bac">{bac_abbrev}</span></div>
|
||||
<div class="eid_info eid_parcours">{code_cursus}</div>
|
||||
"""
|
||||
|
||||
# Informations sur l'etudiant dans le semestre courant:
|
||||
sem = None
|
||||
if formsemestre_id: # un semestre est spécifié par la page
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
elif etud["cursem"]: # le semestre "en cours" pour l'étudiant
|
||||
sem = etud["cursem"]
|
||||
if sem:
|
||||
groups = sco_groups.get_etud_groups(etudid, formsemestre_id)
|
||||
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||
else:
|
||||
# le semestre "en cours" pour l'étudiant
|
||||
inscription_courante = etud.inscription_courante()
|
||||
formsemestre = (
|
||||
inscription_courante.formsemestre if inscription_courante else None
|
||||
)
|
||||
|
||||
if formsemestre:
|
||||
groups = sco_groups.get_etud_groups(etudid, formsemestre.id)
|
||||
grc = sco_groups.listgroups_abbrev(groups)
|
||||
H += f"""<div class="eid_info">En <b>S{sem["semestre_id"]}</b>: {grc}</div>"""
|
||||
H += f"""<div class="eid_info">En <b>S{formsemestre.semestre_id}</b>: {grc}</div>"""
|
||||
H += "</div>" # fin partie gauche (eid_left)
|
||||
if with_photo:
|
||||
H += '<span class="eid_right">' + photo_html + "</span>"
|
||||
|
@ -24,7 +24,7 @@ _SCO_PERMISSIONS = (
|
||||
(1 << 10, "EditAllNotes", "Modifier toutes les notes"),
|
||||
(1 << 11, "EditAllEvals", "Modifier toutes les évaluations"),
|
||||
(1 << 12, "EditFormSemestre", "Mettre en place une formation (créer un semestre)"),
|
||||
(1 << 13, "AbsChange", "Saisir des absences"),
|
||||
(1 << 13, "AbsChange", "Saisir des absences ou justificatifs"),
|
||||
(1 << 14, "AbsAddBillet", "Saisir des billets d'absences"),
|
||||
# changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche
|
||||
(1 << 15, "EtudChangeAdr", "Changer les adresses d'étudiants"),
|
||||
@ -37,7 +37,11 @@ _SCO_PERMISSIONS = (
|
||||
# aussi pour demissions, diplomes:
|
||||
(1 << 17, "EtudInscrit", "Inscrire des étudiants"),
|
||||
# aussi pour archives:
|
||||
(1 << 18, "EtudAddAnnotations", "Éditer les annotations"),
|
||||
(
|
||||
1 << 18,
|
||||
"EtudAddAnnotations",
|
||||
"Éditer les annotations (et fichiers) sur étudiants",
|
||||
),
|
||||
# inutilisée (1 << 19, "ScoEntrepriseView", "Voir la section 'entreprises'"),
|
||||
# inutilisée (1 << 20, "EntrepriseChange", "Modifier les entreprises"),
|
||||
# XXX inutilisée ? (1 << 21, "EditPVJury", "Éditer les PV de jury"),
|
||||
@ -55,10 +59,15 @@ _SCO_PERMISSIONS = (
|
||||
"Exporter les données de l'application relations entreprises",
|
||||
),
|
||||
(1 << 29, "UsersChangeCASId", "Paramétrer l'id CAS"),
|
||||
(1 << 30, "ViewEtudData", "Accéder aux données personnelles des étudiants"),
|
||||
#
|
||||
# XXX inutilisée ? (1 << 40, "EtudChangePhoto", "Modifier la photo d'un étudiant"),
|
||||
# Permissions du module Assiduité)
|
||||
(1 << 50, "AbsJustifView", "Visualisation des fichiers justificatifs"),
|
||||
(
|
||||
1 << 50,
|
||||
"AbsJustifView",
|
||||
"Visualisation du détail des justificatifs (motif, fichiers)",
|
||||
),
|
||||
# Attention: les permissions sont codées sur 64 bits.
|
||||
)
|
||||
|
||||
|
@ -38,20 +38,19 @@ from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import FormSemestre
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_assiduites
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_etud
|
||||
import sco_version
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.codes_cursus import code_semestre_validant, code_semestre_attente
|
||||
import sco_version
|
||||
|
||||
|
||||
def etud_get_poursuite_info(sem, etud):
|
||||
def etud_get_poursuite_info(sem: dict, etud: dict) -> dict:
|
||||
"""{ 'nom' : ..., 'semlist' : [ { 'semestre_id': , 'moy' : ... }, {}, ...] }"""
|
||||
I = {}
|
||||
I.update(etud) # copie nom, prenom, civilite, ...
|
||||
infos = {}
|
||||
infos.update(etud) # copie nom, prenom, civilite, ...
|
||||
|
||||
# Now add each semester, starting from the first one
|
||||
semlist = []
|
||||
@ -92,25 +91,28 @@ def etud_get_poursuite_info(sem, etud):
|
||||
for ue in ues: # on parcourt chaque UE
|
||||
for modimpl in modimpls: # dans chaque UE les modules
|
||||
if modimpl["module"]["ue_id"] == ue["ue_id"]:
|
||||
codeModule = modimpl["module"]["code"] or ""
|
||||
noteModule = scu.fmt_note(
|
||||
code_module = modimpl["module"]["code"] or ""
|
||||
note_module = scu.fmt_note(
|
||||
nt.get_etud_mod_moy(modimpl["moduleimpl_id"], etudid)
|
||||
)
|
||||
if noteModule != "NI": # si étudiant inscrit au module
|
||||
# si étudiant inscrit au module, sauf BUT
|
||||
if (note_module != "NI") and not nt.is_apc:
|
||||
if nt.mod_rangs is not None:
|
||||
rangModule = nt.mod_rangs[modimpl["moduleimpl_id"]][
|
||||
0
|
||||
][etudid]
|
||||
rang_module = nt.mod_rangs[
|
||||
modimpl["moduleimpl_id"]
|
||||
][0][etudid]
|
||||
else:
|
||||
rangModule = ""
|
||||
modules.append([codeModule, noteModule])
|
||||
rangs.append(["rang_" + codeModule, rangModule])
|
||||
rang_module = ""
|
||||
modules.append([code_module, note_module])
|
||||
rangs.append(["rang_" + code_module, rang_module])
|
||||
|
||||
# Absences
|
||||
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, nt.sem)
|
||||
if (
|
||||
# En BUT, prend tout, sinon ne prend que les semestre validés par le jury
|
||||
if nt.is_apc or (
|
||||
dec
|
||||
and not sem_descr # not sem_descr pour ne prendre que le semestre validé le plus récent
|
||||
# not sem_descr pour ne prendre que le semestre validé le plus récent:
|
||||
and not sem_descr
|
||||
and (
|
||||
code_semestre_validant(dec["code"])
|
||||
or code_semestre_attente(dec["code"])
|
||||
@ -128,9 +130,8 @@ def etud_get_poursuite_info(sem, etud):
|
||||
("AbsNonJust", nbabs - nbabsjust),
|
||||
("AbsJust", nbabsjust),
|
||||
]
|
||||
d += (
|
||||
moy_ues + rg_ues + modules + rangs
|
||||
) # ajout des 2 champs notes des modules et classement dans chaque module
|
||||
# ajout des 2 champs notes des modules et classement dans chaque module
|
||||
d += moy_ues + rg_ues + modules + rangs
|
||||
sem_descr = collections.OrderedDict(d)
|
||||
if not sem_descr:
|
||||
sem_descr = collections.OrderedDict(
|
||||
@ -147,13 +148,14 @@ def etud_get_poursuite_info(sem, etud):
|
||||
sem_descr["semestre_id"] = sem_id
|
||||
semlist.append(sem_descr)
|
||||
|
||||
I["semlist"] = semlist
|
||||
return I
|
||||
infos["semlist"] = semlist
|
||||
return infos
|
||||
|
||||
|
||||
def _flatten_info(info):
|
||||
# met la liste des infos semestres "a plat"
|
||||
# S1_moy, S1_rang, ..., S2_moy, ...
|
||||
"""met la liste des infos semestres "a plat"
|
||||
S1_moy, S1_rang, ..., S2_moy, ...
|
||||
"""
|
||||
ids = []
|
||||
for s in info["semlist"]:
|
||||
for k, v in s.items():
|
||||
@ -164,7 +166,7 @@ def _flatten_info(info):
|
||||
return ids
|
||||
|
||||
|
||||
def _getEtudInfoGroupes(group_ids, etat=None):
|
||||
def _get_etud_info_groupes(group_ids, etat=None):
|
||||
"""liste triée d'infos (dict) sur les etudiants du groupe indiqué.
|
||||
Attention: lent, car plusieurs requetes SQL par etudiant !
|
||||
"""
|
||||
@ -181,17 +183,17 @@ def _getEtudInfoGroupes(group_ids, etat=None):
|
||||
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)])
|
||||
etuds = _get_etud_info_groupes([sco_groups.get_default_group(formsemestre_id)])
|
||||
|
||||
infos = []
|
||||
ids = []
|
||||
for etud in etuds:
|
||||
fiche_url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
)
|
||||
etud["_nom_target"] = fiche_url
|
||||
etud["_prenom_target"] = fiche_url
|
||||
etud["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
|
||||
etud["_nom_td_attrs"] = f"""id="{etud['etudid']}" class="etudinfo" """
|
||||
info = etud_get_poursuite_info(sem, etud)
|
||||
idd = _flatten_info(info)
|
||||
# On recupere la totalite des UEs dans ids
|
||||
|
@ -147,6 +147,7 @@ def get_preference(name, formsemestre_id=None, dept_id=None):
|
||||
"""Returns value of named preference.
|
||||
All preferences have a sensible default value, so this
|
||||
function always returns a usable value for all defined preferences names.
|
||||
If dept_id is None, use current dept (g.scodoc_dept_id)
|
||||
"""
|
||||
return get_base_preferences(dept_id=dept_id).get(formsemestre_id, name)
|
||||
|
||||
@ -541,18 +542,6 @@ class BasePreferences:
|
||||
"category": "abs",
|
||||
},
|
||||
),
|
||||
(
|
||||
"abs_notify_max_freq",
|
||||
{
|
||||
"initvalue": 7,
|
||||
"title": "Fréquence maximale de notification",
|
||||
"explanation": "nb de jours minimum entre deux mails envoyés au même destinataire à propos d'un même étudiant ",
|
||||
"size": 4,
|
||||
"type": "int",
|
||||
"convert_numbers": True,
|
||||
"category": "abs",
|
||||
},
|
||||
),
|
||||
(
|
||||
"abs_notify_abs_threshold",
|
||||
{
|
||||
@ -710,11 +699,23 @@ class BasePreferences:
|
||||
"size": 10,
|
||||
"title": "Seuil d'alerte des absences",
|
||||
"type": "int",
|
||||
"explanation": "Nombres d'absences limite avant alerte dans le bilan (utilisation de l'unité métrique ↑ )",
|
||||
"explanation": "Nombres d'absences limite avant alerte (utilisation de l'unité métrique ↑ )",
|
||||
"category": "assi",
|
||||
"only_global": True,
|
||||
},
|
||||
),
|
||||
(
|
||||
"abs_notify_max_freq",
|
||||
{
|
||||
"initvalue": 7,
|
||||
"title": "Fréquence maximale de notification",
|
||||
"explanation": "nb de jours minimum entre deux mails envoyés au même destinataire à propos d'un même étudiant ",
|
||||
"size": 4,
|
||||
"type": "int",
|
||||
"convert_numbers": True,
|
||||
"category": "abs",
|
||||
},
|
||||
),
|
||||
# portal
|
||||
(
|
||||
"portal_url",
|
||||
|
@ -144,7 +144,7 @@ def pvjury_table(
|
||||
"code_nip": e["identite"]["code_nip"],
|
||||
"nomprenom": e["identite"]["nomprenom"],
|
||||
"_nomprenom_target": url_for(
|
||||
"scolar.ficheEtud",
|
||||
"scolar.fiche_etud",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
etudid=e["identite"]["etudid"],
|
||||
),
|
||||
@ -351,7 +351,7 @@ def formsemestre_pvjury_pdf(formsemestre_id, group_ids: list[int] = None, etudid
|
||||
# PV pour ce seul étudiant:
|
||||
etud = Identite.get_etud(etudid)
|
||||
etuddescr = f"""<a class="discretelink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
}">{etud.nomprenom}</a>"""
|
||||
etudids = [etudid]
|
||||
else:
|
||||
|
@ -1017,34 +1017,60 @@ EXP_LIC = re.compile(r"licence", re.I)
|
||||
EXP_LPRO = re.compile(r"professionnelle", re.I)
|
||||
|
||||
|
||||
def _codesem(sem, short=True, prefix=""):
|
||||
def _code_sem(
|
||||
semestre_id: int, titre: str, mois_debut: int, short=True, prefix=""
|
||||
) -> str:
|
||||
"code semestre: S1 ou S1d"
|
||||
idx = sem["semestre_id"]
|
||||
idx = semestre_id
|
||||
# semestre décalé ?
|
||||
# les semestres pairs normaux commencent entre janvier et mars
|
||||
# les impairs normaux entre aout et decembre
|
||||
d = ""
|
||||
if idx and idx > 0 and sem["date_debut"]:
|
||||
mois_debut = int(sem["date_debut"].split("/")[1])
|
||||
if idx > 0:
|
||||
if (idx % 2 and mois_debut < 3) or (idx % 2 == 0 and mois_debut >= 8):
|
||||
d = "d"
|
||||
if idx == -1:
|
||||
if short:
|
||||
idx = "Autre "
|
||||
else:
|
||||
idx = sem["titre"] + " "
|
||||
idx = titre + " "
|
||||
idx = EXP_LPRO.sub("pro.", idx)
|
||||
idx = EXP_LIC.sub("Lic.", idx)
|
||||
prefix = "" # indique titre au lieu de Sn
|
||||
return "%s%s%s" % (prefix, idx, d)
|
||||
return prefix + str(idx) + d
|
||||
|
||||
|
||||
def get_code_cursus_etud(etud, prefix="", separator=""):
|
||||
def _code_sem_formsemestre(formsemestre: FormSemestre, short=True, prefix="") -> str:
|
||||
"code semestre: S1 ou S1d"
|
||||
titre = formsemestre.titre
|
||||
mois_debut = formsemestre.date_debut.month
|
||||
semestre_id = formsemestre.semestre_id
|
||||
return _code_sem(semestre_id, titre, mois_debut, short=short, prefix=prefix)
|
||||
|
||||
|
||||
def _code_sem_dict(sem, short=True, prefix="") -> str:
|
||||
"code semestre: S1 ou S1d, à parit d'un dict (sem ScoDoc 7)"
|
||||
titre = sem["titre"]
|
||||
mois_debut = int(sem["date_debut"].split("/")[1]) if sem["date_debut"] else 0
|
||||
semestre_id = sem["semestre_id"]
|
||||
return _code_sem(semestre_id, titre, mois_debut, short=short, prefix=prefix)
|
||||
|
||||
|
||||
def get_code_cursus_etud(
|
||||
etudid: int,
|
||||
sems: list[dict] = None,
|
||||
formsemestres: list[FormSemestre] | None = None,
|
||||
prefix="",
|
||||
separator="",
|
||||
) -> tuple[str, dict]:
|
||||
"""calcule un code de cursus (parcours) pour un etudiant
|
||||
exemples:
|
||||
1234A pour un etudiant ayant effectué S1, S2, S3, S4 puis diplome
|
||||
12D pour un étudiant en S1, S2 puis démission en S2
|
||||
12R pour un etudiant en S1, S2 réorienté en fin de S2
|
||||
|
||||
On peut passer soir la liste des semestres dict (anciennes fonctions ScoDoc7)
|
||||
soit la liste des FormSemestre.
|
||||
Construit aussi un dict: { semestre_id : decision_jury | None }
|
||||
"""
|
||||
# Nota: approche plus moderne:
|
||||
@ -1054,36 +1080,42 @@ def get_code_cursus_etud(etud, prefix="", separator=""):
|
||||
#
|
||||
p = []
|
||||
decisions_jury = {}
|
||||
# élimine les semestres spéciaux hors cursus (LP en 1 sem., ...)
|
||||
sems = [s for s in etud["sems"] if s["semestre_id"] >= 0]
|
||||
i = len(sems) - 1
|
||||
while i >= 0:
|
||||
s = sems[i] # 'sems' est a l'envers, du plus recent au plus ancien
|
||||
s_formsemestre = FormSemestre.query.get_or_404(s["formsemestre_id"])
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(s_formsemestre)
|
||||
|
||||
p.append(_codesem(s, prefix=prefix))
|
||||
if formsemestres is None:
|
||||
formsemestres = [
|
||||
FormSemestre.query.get_or_404(s["formsemestre_id"]) for s in (sems or [])
|
||||
]
|
||||
|
||||
# élimine les semestres spéciaux hors cursus (LP en 1 sem., ...)
|
||||
formsemestres = [s for s in formsemestres if s.semestre_id >= 0]
|
||||
i = len(formsemestres) - 1
|
||||
while i >= 0:
|
||||
# 'sems' est a l'envers, du plus recent au plus ancien
|
||||
formsemestre = formsemestres[i]
|
||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||
|
||||
p.append(_code_sem_formsemestre(formsemestre, prefix=prefix))
|
||||
# code decisions jury de chaque semestre:
|
||||
if nt.get_etud_etat(etud["etudid"]) == "D":
|
||||
decisions_jury[s["semestre_id"]] = "DEM"
|
||||
if nt.get_etud_etat(etudid) == "D":
|
||||
decisions_jury[formsemestre.semestre_id] = "DEM"
|
||||
else:
|
||||
dec = nt.get_etud_decision_sem(etud["etudid"])
|
||||
dec = nt.get_etud_decision_sem(etudid)
|
||||
if not dec:
|
||||
decisions_jury[s["semestre_id"]] = ""
|
||||
decisions_jury[formsemestre.semestre_id] = ""
|
||||
else:
|
||||
decisions_jury[s["semestre_id"]] = dec["code"]
|
||||
decisions_jury[formsemestre.semestre_id] = dec["code"]
|
||||
# code etat dans le code_cursus sur dernier semestre seulement
|
||||
if i == 0:
|
||||
# Démission
|
||||
if nt.get_etud_etat(etud["etudid"]) == "D":
|
||||
if nt.get_etud_etat(etudid) == "D":
|
||||
p.append(":D")
|
||||
else:
|
||||
dec = nt.get_etud_decision_sem(etud["etudid"])
|
||||
dec = nt.get_etud_decision_sem(etudid)
|
||||
if dec and dec["code"] in codes_cursus.CODES_SEM_REO:
|
||||
p.append(":R")
|
||||
if (
|
||||
dec
|
||||
and s["semestre_id"] == nt.parcours.NB_SEM
|
||||
and formsemestre.semestre_id == nt.parcours.NB_SEM
|
||||
and code_semestre_validant(dec["code"])
|
||||
):
|
||||
p.append(":A")
|
||||
@ -1176,14 +1208,16 @@ def table_suivi_cursus(formsemestre_id, only_primo=False, grouped_parcours=True)
|
||||
) = tsp_etud_list(formsemestre_id, only_primo=only_primo)
|
||||
codes_etuds = collections.defaultdict(list)
|
||||
for etud in etuds:
|
||||
etud["code_cursus"], etud["decisions_jury"] = get_code_cursus_etud(etud)
|
||||
etud["code_cursus"], etud["decisions_jury"] = get_code_cursus_etud(
|
||||
etud["etudid"], sems=etud["sems"]
|
||||
)
|
||||
codes_etuds[etud["code_cursus"]].append(etud)
|
||||
fiche_url = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"]
|
||||
)
|
||||
etud["_nom_target"] = fiche_url
|
||||
etud["_prenom_target"] = fiche_url
|
||||
etud["_nom_td_attrs"] = 'id="%s" class="etudinfo"' % (etud["etudid"])
|
||||
etud["_nom_td_attrs"] = f'''id="{etud['etudid']}" class="etudinfo"'''
|
||||
|
||||
titles = {
|
||||
"parcours": "Code cursus",
|
||||
@ -1461,7 +1495,7 @@ def graph_cursus(
|
||||
else:
|
||||
modalite = ""
|
||||
label = "%s%s\\n%d/%s - %d/%s\\n%d" % (
|
||||
_codesem(s, short=False, prefix="S"),
|
||||
_code_sem_dict(s, short=False, prefix="S"),
|
||||
modalite,
|
||||
s["mois_debut_ord"],
|
||||
s["annee_debut"][2:],
|
||||
|
@ -13,8 +13,9 @@ SCO_ROLES_DEFAULTS = {
|
||||
p.EnsView,
|
||||
p.EtudAddAnnotations,
|
||||
p.Observateur,
|
||||
p.UsersView,
|
||||
p.ScoView,
|
||||
p.ViewEtudData,
|
||||
p.UsersView,
|
||||
),
|
||||
"Secr": (
|
||||
p.AbsAddBillet,
|
||||
@ -23,8 +24,9 @@ SCO_ROLES_DEFAULTS = {
|
||||
p.EtudAddAnnotations,
|
||||
p.EtudChangeAdr,
|
||||
p.Observateur,
|
||||
p.UsersView,
|
||||
p.ScoView,
|
||||
p.UsersView,
|
||||
p.ViewEtudData,
|
||||
),
|
||||
# Admin est le chef du département, pas le "super admin"
|
||||
# on doit donc lister toutes ses permissions:
|
||||
@ -44,9 +46,10 @@ SCO_ROLES_DEFAULTS = {
|
||||
p.EtudInscrit,
|
||||
p.EditFormSemestre,
|
||||
p.Observateur,
|
||||
p.ScoView,
|
||||
p.UsersAdmin,
|
||||
p.UsersView,
|
||||
p.ScoView,
|
||||
p.ViewEtudData,
|
||||
),
|
||||
# Rôles pour l'application relations entreprises
|
||||
# ObservateurEntreprise est un observateur de l'application entreprise
|
||||
@ -57,7 +60,8 @@ SCO_ROLES_DEFAULTS = {
|
||||
p.RelationsEntrepEdit,
|
||||
p.RelationsEntrepViewCorrs,
|
||||
),
|
||||
# AdminEntreprise est un admin de l'application entreprise (toutes les actions possibles de l'application)
|
||||
# AdminEntreprise est un admin de l'application entreprise
|
||||
# (toutes les actions possibles de l'application)
|
||||
"AdminEntreprise": (
|
||||
p.RelationsEntrepView,
|
||||
p.RelationsEntrepEdit,
|
||||
|
@ -156,7 +156,7 @@ def trombino_html(groups_infos):
|
||||
'<a href="%s">%s</a>'
|
||||
% (
|
||||
url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=t["etudid"]
|
||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=t["etudid"]
|
||||
),
|
||||
foto,
|
||||
)
|
||||
|
@ -279,7 +279,7 @@ class NonWorkDays(int, BiDirectionalEnum):
|
||||
]
|
||||
|
||||
|
||||
def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or None:
|
||||
def is_iso_formated(date: str, convert=False) -> bool | datetime.datetime | None:
|
||||
"""
|
||||
Vérifie si une date est au format iso
|
||||
|
||||
@ -298,12 +298,11 @@ def is_iso_formated(date: str, convert=False) -> bool or datetime.datetime or No
|
||||
return None if convert else False
|
||||
|
||||
|
||||
def localize_datetime(date: datetime.datetime or str) -> datetime.datetime:
|
||||
"""Ajoute un timecode UTC à la date donnée.
|
||||
XXX semble faire autre chose... TODO fix this comment
|
||||
def localize_datetime(date: datetime.datetime) -> datetime.datetime:
|
||||
"""Transforme une date sans offset en une date avec offset
|
||||
Tente de mettre l'offset de la timezone du serveur (ex : UTC+1)
|
||||
Si erreur, mettra l'offset UTC
|
||||
"""
|
||||
if isinstance(date, str):
|
||||
date = is_iso_formated(date, convert=True)
|
||||
|
||||
new_date: datetime.datetime = date
|
||||
if new_date.tzinfo is None:
|
||||
@ -428,7 +427,7 @@ APO_MISSING_CODE_STR = "----" # shown in HTML pages in place of missing code Ap
|
||||
EDIT_NB_ETAPES = 6 # Nombre max de codes étapes / semestre presentés dans l'UI
|
||||
|
||||
IT_SITUATION_MISSING_STR = (
|
||||
"____" # shown on ficheEtud (devenir) in place of empty situation
|
||||
"____" # shown on fiche_etud (devenir) in place of empty situation
|
||||
)
|
||||
|
||||
RANG_ATTENTE_STR = "(attente)" # rang affiché sur bulletins quand notes en attente
|
||||
@ -476,7 +475,7 @@ MONTH_NAMES_ABBREV = (
|
||||
"Avr ",
|
||||
"Mai ",
|
||||
"Juin",
|
||||
"Jul ",
|
||||
"Juil ",
|
||||
"Août",
|
||||
"Sept",
|
||||
"Oct ",
|
||||
@ -1282,6 +1281,27 @@ def format_prenom(s):
|
||||
return " ".join(r)
|
||||
|
||||
|
||||
def format_telephone(n: str | None) -> str:
|
||||
"Format a phone number for display"
|
||||
if n is None:
|
||||
return ""
|
||||
if len(n) < 7:
|
||||
return n
|
||||
n = n.replace(" ", "").replace(".", "")
|
||||
i = 0
|
||||
r = ""
|
||||
j = len(n) - 1
|
||||
while j >= 0:
|
||||
r = n[j] + r
|
||||
if i % 2 == 1 and j != 0:
|
||||
r = " " + r
|
||||
i += 1
|
||||
j -= 1
|
||||
if len(r) == 13 and r[0] != "0":
|
||||
r = "0" + r
|
||||
return r
|
||||
|
||||
|
||||
#
|
||||
def timedate_human_repr():
|
||||
"representation du temps courant pour utilisateur"
|
||||
@ -1610,20 +1630,12 @@ def is_entreprises_enabled():
|
||||
def is_assiduites_module_forced(
|
||||
formsemestre_id: int = None, dept_id: int = None
|
||||
) -> bool:
|
||||
"""Vrai si préférence "imposer la saisie du module" sur les assiduités est vraie."""
|
||||
from app.scodoc import sco_preferences
|
||||
|
||||
retour: bool
|
||||
|
||||
if dept_id is None:
|
||||
dept_id = g.scodoc_dept_id
|
||||
|
||||
try:
|
||||
retour = sco_preferences.get_preference(
|
||||
"forcer_module", formsemestre_id=int(formsemestre_id)
|
||||
return sco_preferences.get_preference(
|
||||
"forcer_module", formsemestre_id=formsemestre_id, dept_id=dept_id
|
||||
)
|
||||
except (TypeError, ValueError):
|
||||
retour = sco_preferences.get_preference("forcer_module", dept_id=dept_id)
|
||||
return retour
|
||||
|
||||
|
||||
def get_assiduites_time_config(config_type: str) -> str | int:
|
||||
|
@ -256,17 +256,17 @@
|
||||
background-color: var(--color-conflit);
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar .absent,
|
||||
.etud_row .assiduites_bar>.absent,
|
||||
.demo.absent {
|
||||
background-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar .present,
|
||||
.etud_row .assiduites_bar>.present,
|
||||
.demo.present {
|
||||
background-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar .retard,
|
||||
.etud_row .assiduites_bar>.retard,
|
||||
.demo.retard {
|
||||
background-color: var(--color-retard) !important;
|
||||
}
|
||||
@ -275,12 +275,12 @@
|
||||
background-color: var(--color-nonwork) !important;
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar .justified,
|
||||
.etud_row .assiduites_bar>.justified,
|
||||
.demo.justified {
|
||||
background-image: var(--motif-justi);
|
||||
}
|
||||
|
||||
.etud_row .assiduites_bar .invalid_justified,
|
||||
.etud_row .assiduites_bar>.invalid_justified,
|
||||
.demo.invalid_justified {
|
||||
background-image: var(--motif-justi-invalide);
|
||||
}
|
||||
|
@ -144,3 +144,7 @@ span.ens-non-reconnu {
|
||||
.btn:active {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.raw-event {
|
||||
display: none;
|
||||
}
|
212
app/static/css/minitimeline.css
Normal file
212
app/static/css/minitimeline.css
Normal file
@ -0,0 +1,212 @@
|
||||
.day .dayline {
|
||||
position: absolute;
|
||||
display: none;
|
||||
top: 100%;
|
||||
z-index: 50;
|
||||
width: max-content;
|
||||
height: 75px;
|
||||
background-color: #dedede;
|
||||
border-radius: 15px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.day:hover .dayline {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dayline .mini-timeline {
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
.dayline-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dayline .mini_tick {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
transform: translateY(-110%);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.dayline .mini_tick::after {
|
||||
display: block;
|
||||
content: "|";
|
||||
position: absolute;
|
||||
bottom: -69%;
|
||||
z-index: 2;
|
||||
transform: translateX(200%);
|
||||
}
|
||||
|
||||
#label-nom,
|
||||
#label-justi {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.demi .day {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.demi .day>span {
|
||||
display: block;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
border: 1px solid #d5d5d5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.demi .day>span:first-of-type {
|
||||
width: 3em;
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.options>* {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.options input {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.options label {
|
||||
font-weight: normal;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
|
||||
/*Gestion des bubbles*/
|
||||
.assiduite-bubble {
|
||||
position: relative;
|
||||
display: none;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
border: 3px solid #ccc;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
z-index: 3;
|
||||
min-width: max-content;
|
||||
top: 200%;
|
||||
}
|
||||
|
||||
.mini-timeline-block:hover .assiduite-bubble {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.assiduite-bubble::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #f9f9f9 transparent;
|
||||
}
|
||||
|
||||
.assiduite-bubble::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #ccc transparent;
|
||||
}
|
||||
|
||||
.assiduite-id,
|
||||
.assiduite-period,
|
||||
.assiduite-state,
|
||||
.assiduite-user_id {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.assiduite-bubble.absent {
|
||||
border-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.assiduite-bubble.present {
|
||||
border-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.assiduite-bubble.retard {
|
||||
border-color: var(--color-retard) !important;
|
||||
}
|
||||
|
||||
/*Gestion des minitimelines*/
|
||||
.mini-timeline {
|
||||
height: 7px;
|
||||
border: 1px solid black;
|
||||
position: relative;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.mini-timeline.single {
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.mini-timeline-block {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mini-timeline-block {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mini_tick {
|
||||
position: absolute;
|
||||
text-align: start;
|
||||
top: -40px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
|
||||
}
|
||||
|
||||
.mini_tick::after {
|
||||
display: block;
|
||||
content: "|";
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mini-timeline-block.creneau {
|
||||
outline: 3px solid var(--color-primary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mini-timeline-block.absent {
|
||||
background-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.present {
|
||||
background-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.retard {
|
||||
background-color: var(--color-retard) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.justified {
|
||||
background-image: var(--motif-justi);
|
||||
}
|
||||
|
||||
.mini-timeline-block.invalid_justified {
|
||||
background-image: var(--motif-justi-invalide);
|
||||
}
|
@ -172,6 +172,11 @@ form#group_selector {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
/* Text lien ou itms ,non autorisés pour l'utilisateur courant */
|
||||
.unauthorized {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
/* ----- bandeau haut ------ */
|
||||
span.bandeaugtr {
|
||||
width: 100%;
|
||||
@ -724,7 +729,7 @@ div.scoinfos {
|
||||
|
||||
/* ----- fiches etudiants ------ */
|
||||
|
||||
div.ficheEtud {
|
||||
div.fiche_etud {
|
||||
background-color: #f5edc8;
|
||||
/* rgb(255,240,128); */
|
||||
border: 1px solid gray;
|
||||
@ -739,7 +744,7 @@ div.menus_etud {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
div.ficheEtud h2 {
|
||||
div.fiche_etud h2 {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
@ -925,7 +930,7 @@ td.fichetitre2 {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.ficheEtud span.boursier {
|
||||
.fiche_etud span.boursier {
|
||||
background-color: red;
|
||||
color: white;
|
||||
margin-left: 12px;
|
||||
@ -963,6 +968,7 @@ div.section_but {
|
||||
|
||||
div.section_but > div.link_validation_rcues {
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ficheannotations {
|
||||
@ -1736,7 +1742,9 @@ formsemestre_page_title .lock img {
|
||||
width: 200px !important;
|
||||
}
|
||||
|
||||
span.inscr_addremove_menu {
|
||||
div.inscr_addremove_menu {
|
||||
display: inline-block;
|
||||
margin: 8px 0px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
// TODO : Supprimer les fonctions non utilisées + optimiser les fonctions utilisées
|
||||
// <=== CONSTANTS and GLOBALS ===>
|
||||
|
||||
let url;
|
||||
@ -68,6 +69,25 @@ function setupCheckBox(parent = document) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateEtudList() {
|
||||
const group_ids = getGroupIds();
|
||||
etuds = {};
|
||||
group_ids.forEach((group_id) => {
|
||||
sync_get(getUrl() + `/api/group/${group_id}/etudiants`, (data, status) => {
|
||||
if (status === "success") {
|
||||
data.forEach((etud) => {
|
||||
if (!(etud.id in etuds)) {
|
||||
etuds[etud.id] = etud;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
getAssiduitesFromEtuds(true);
|
||||
generateAllEtudRow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation préalable puis désactivation des chammps :
|
||||
* - Groupe
|
||||
@ -108,14 +128,16 @@ function validateSelectors(btn) {
|
||||
return;
|
||||
}
|
||||
|
||||
getAssiduitesFromEtuds(true);
|
||||
|
||||
// document.querySelector(".selectors").disabled = true;
|
||||
// $("#tl_date").datepicker("option", "disabled", true);
|
||||
generateMassAssiduites();
|
||||
|
||||
getAssiduitesFromEtuds(true);
|
||||
generateAllEtudRow();
|
||||
// btn.remove();
|
||||
btn.textContent = "Actualiser";
|
||||
|
||||
btn.remove();
|
||||
// Auto actualisation
|
||||
$("#tl_date").on("change", updateEtudList);
|
||||
$("#group_ids_sel").on("change", updateEtudList);
|
||||
|
||||
onlyAbs();
|
||||
};
|
||||
|
||||
@ -648,16 +670,15 @@ function updateDate() {
|
||||
);
|
||||
openAlertModal("Attention", div, "", "#eec660");
|
||||
|
||||
/* BUG TODO MATHIAS
|
||||
$(dateInput).datepicker("setDate", date_fra); // XXX ??? non définie
|
||||
dateInput.value = date_fra;
|
||||
*/
|
||||
date = lastWorkDay;
|
||||
|
||||
dateStr = formatDate(lastWorkDay, {
|
||||
dateStyle: "full",
|
||||
timeZone: SCO_TIMEZONE,
|
||||
}).capitalize();
|
||||
|
||||
$(dateInput).datepicker("setDate", date);
|
||||
$(dateInput).change();
|
||||
}
|
||||
|
||||
document.querySelector("#datestr").textContent = dateStr;
|
||||
@ -697,6 +718,61 @@ function setupDate(onchange = null) {
|
||||
}
|
||||
});
|
||||
|
||||
//Initialisation du datepicker
|
||||
// sinon on ne peut pas le mettre à jour
|
||||
// XXX TODO-assiduite : finir tester + éviter duplication code avec scodoc.js
|
||||
$(input).datepicker({
|
||||
showOn: "button",
|
||||
buttonImage: "/ScoDoc/static/icons/calendar_img.png",
|
||||
buttonImageOnly: true,
|
||||
dateFormat: "dd/mm/yy",
|
||||
duration: "fast",
|
||||
firstDay: 1, // Start with Monday
|
||||
dayNames: [
|
||||
"Dimanche",
|
||||
"Lundi",
|
||||
"Mardi",
|
||||
"Mercredi",
|
||||
"Jeudi",
|
||||
"Vendredi",
|
||||
"Samedi",
|
||||
],
|
||||
dayNamesMin: ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa"],
|
||||
dayNamesShort: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"],
|
||||
monthNames: [
|
||||
"Janvier",
|
||||
"Février",
|
||||
"Mars",
|
||||
"Avril",
|
||||
"May",
|
||||
"Juin",
|
||||
"Juillet",
|
||||
"Août",
|
||||
"Septembre",
|
||||
"Octobre",
|
||||
"Novembre",
|
||||
"Décembre",
|
||||
],
|
||||
monthNamesShort: [
|
||||
"Jan",
|
||||
"Fév",
|
||||
"Mar",
|
||||
"Avr",
|
||||
"Mai",
|
||||
"Juin",
|
||||
"Juil",
|
||||
"Aoû",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Déc",
|
||||
],
|
||||
});
|
||||
$(input).datepicker(
|
||||
"option",
|
||||
$.extend({ showMonthAfterYear: false }, $.datepicker.regional["fr"])
|
||||
);
|
||||
|
||||
if (onchange != null) {
|
||||
$(input).change(onchange);
|
||||
}
|
||||
@ -1262,19 +1338,14 @@ function getAllAssiduitesFromEtud(
|
||||
.replace("°", courant ? "&courant" : "")
|
||||
: ""
|
||||
}`;
|
||||
//TODO Utiliser async_get au lieu de jquery
|
||||
$.ajax({
|
||||
async: true,
|
||||
type: "GET",
|
||||
url: url_api,
|
||||
success: (data, status) => {
|
||||
if (status === "success") {
|
||||
async_get(
|
||||
url_api,
|
||||
(data) => {
|
||||
assiduites[etudid] = data;
|
||||
action(data);
|
||||
}
|
||||
},
|
||||
error: () => {},
|
||||
});
|
||||
(_) => {}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1844,18 +1915,13 @@ function getAllJustificatifsFromEtud(
|
||||
order ? "/query?order°".replace("°", courant ? "&courant" : "") : ""
|
||||
}`;
|
||||
|
||||
//TODO Utiliser async_get au lieu de jquery
|
||||
$.ajax({
|
||||
async: true,
|
||||
type: "GET",
|
||||
url: url_api,
|
||||
success: (data, status) => {
|
||||
if (status === "success") {
|
||||
async_get(
|
||||
url_api,
|
||||
(data) => {
|
||||
action(data);
|
||||
}
|
||||
},
|
||||
error: () => {},
|
||||
});
|
||||
() => {}
|
||||
);
|
||||
}
|
||||
|
||||
function deleteJustificatif(justif_id) {
|
||||
|
@ -39,7 +39,7 @@ $(function () {
|
||||
"Avril",
|
||||
"May",
|
||||
"Juin",
|
||||
"Juilet",
|
||||
"Juillet",
|
||||
"Août",
|
||||
"Septembre",
|
||||
"Octobre",
|
||||
|
@ -1,14 +1,76 @@
|
||||
from datetime import datetime
|
||||
|
||||
from flask import url_for
|
||||
from flask_sqlalchemy.query import Pagination, Query
|
||||
from sqlalchemy import desc, literal, union
|
||||
from flask_login import current_user
|
||||
from flask_sqlalchemy.query import Query
|
||||
from sqlalchemy import desc, literal, union, asc
|
||||
|
||||
from app import db, g
|
||||
from app.auth.models import User
|
||||
from app.models import Assiduite, Identite, Justificatif
|
||||
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
|
||||
from app.tables import table_builder as tb
|
||||
from app.scodoc.sco_cache import RequeteTableauAssiduiteCache
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
|
||||
class Pagination:
|
||||
"""
|
||||
Pagination d'une collection de données
|
||||
|
||||
On donne :
|
||||
- une collection de données (de préférence une liste / tuple)
|
||||
- le numéro de page à afficher
|
||||
- le nombre d'éléments par page
|
||||
|
||||
On peut ensuite récupérer les éléments de la page courante avec la méthode `items()`
|
||||
|
||||
Cette classe ne permet pas de changer de page.
|
||||
(Pour cela, il faut créer une nouvelle instance, avec la collection originelle et la nouvelle page)
|
||||
|
||||
l'intéret est de ne pas garder en mémoire toute la collection, mais seulement la page courante
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, collection: list, page: int = 1, per_page: int = -1):
|
||||
"""
|
||||
__init__ Instancie un nouvel objet Pagination
|
||||
|
||||
Args:
|
||||
collection (list): La collection à paginer. Il s'agit par exemple d'une requête
|
||||
page (int, optional): le numéro de la page à voir. Defaults to 1.
|
||||
per_page (int, optional): le nombre d'éléments par page. Defaults to -1. (-1 = pas de pagination/tout afficher)
|
||||
"""
|
||||
# par défaut le total des pages est 1 (même si la collection est vide)
|
||||
self.total_pages = 1
|
||||
|
||||
if per_page != -1:
|
||||
# on récupère le nombre de page complète et le reste
|
||||
# q => nombre de page
|
||||
# r => le nombre d'éléments restants (dernière page si != 0)
|
||||
q, r = len(collection) // per_page, len(collection) % per_page
|
||||
self.total_pages = q if r == 0 else q + 1 # q + 1 s'il reste des éléments
|
||||
|
||||
# On s'assure que la page demandée est dans les limites
|
||||
current_page: int = min(self.total_pages, page if page > 0 else 1)
|
||||
|
||||
# On récupère la collection de la page courante
|
||||
self.collection = (
|
||||
collection # toute la collection si pas de pagination
|
||||
if per_page == -1
|
||||
else collection[
|
||||
per_page * (current_page - 1) : per_page * (current_page)
|
||||
] # sinon on récupère la page
|
||||
)
|
||||
|
||||
def items(self) -> list:
|
||||
"""
|
||||
items Renvoi la collection de la page courante
|
||||
|
||||
Returns:
|
||||
list: la collection de la page courante
|
||||
"""
|
||||
return self.collection
|
||||
|
||||
|
||||
class ListeAssiJusti(tb.Table):
|
||||
@ -18,13 +80,15 @@ class ListeAssiJusti(tb.Table):
|
||||
"""
|
||||
|
||||
NB_PAR_PAGE: int = 25
|
||||
MAX_PAR_PAGE: int = 200
|
||||
MAX_PAR_PAGE: int = 1000
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
table_data: "AssiJustifData",
|
||||
filtre: "AssiFiltre" = None,
|
||||
options: "AssiDisplayOptions" = None,
|
||||
no_pagination: bool = False,
|
||||
titre: str = "",
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""
|
||||
@ -41,11 +105,21 @@ class ListeAssiJusti(tb.Table):
|
||||
# Gestion des options, par défaut un objet Options vide
|
||||
self.options = options if options is not None else AssiDisplayOptions()
|
||||
|
||||
self.no_pagination: bool = no_pagination
|
||||
|
||||
self.total_page: int = None
|
||||
|
||||
# Accès aux détail des justificatifs ?
|
||||
self.can_view_justif_detail = current_user.has_permission(
|
||||
Permission.AbsJustifView
|
||||
)
|
||||
|
||||
# les lignes du tableau
|
||||
self.rows: list["RowAssiJusti"] = []
|
||||
|
||||
# Titre du tableau, utilisé pour le cache
|
||||
self.titre = titre
|
||||
|
||||
# Instanciation de la classe parent
|
||||
super().__init__(
|
||||
row_class=RowAssiJusti,
|
||||
@ -65,16 +139,39 @@ class ListeAssiJusti(tb.Table):
|
||||
# Récupération du filtrage des objets -> 0 : tout, 1 : Assi, 2: Justi
|
||||
type_obj = self.filtre.type_obj()
|
||||
|
||||
cle_cache: str = ":".join(
|
||||
map(
|
||||
str,
|
||||
[
|
||||
self.titre,
|
||||
type_obj,
|
||||
self.options.show_pres,
|
||||
self.options.show_reta,
|
||||
self.options.show_desc,
|
||||
self.options.order[0],
|
||||
self.options.order[1],
|
||||
],
|
||||
)
|
||||
)
|
||||
r = RequeteTableauAssiduiteCache().get(cle_cache)
|
||||
|
||||
if r is None:
|
||||
if type_obj in [0, 1]:
|
||||
assiduites_query_etudiants = self.table_data.assiduites_query
|
||||
|
||||
# Non affichage des présences
|
||||
if not self.options.show_pres:
|
||||
if (
|
||||
not self.options.show_pres
|
||||
and assiduites_query_etudiants is not None
|
||||
):
|
||||
assiduites_query_etudiants = assiduites_query_etudiants.filter(
|
||||
Assiduite.etat != EtatAssiduite.PRESENT
|
||||
)
|
||||
# Non affichage des retards
|
||||
if not self.options.show_reta:
|
||||
if (
|
||||
not self.options.show_reta
|
||||
and assiduites_query_etudiants is not None
|
||||
):
|
||||
assiduites_query_etudiants = assiduites_query_etudiants.filter(
|
||||
Assiduite.etat != EtatAssiduite.RETARD
|
||||
)
|
||||
@ -89,35 +186,46 @@ class ListeAssiJusti(tb.Table):
|
||||
query_justificatif=justificatifs_query_etudiants,
|
||||
)
|
||||
|
||||
# Tri de la query si option
|
||||
if self.options.order is not None:
|
||||
order_sort: str = asc if self.options.order[1] else desc
|
||||
order_col: str = self.options.order[0]
|
||||
query_finale: Query = query_finale.order_by(order_sort(order_col))
|
||||
|
||||
r = query_finale.all()
|
||||
RequeteTableauAssiduiteCache.set(cle_cache, r)
|
||||
|
||||
# Paginer la requête pour ne pas envoyer trop d'informations au client
|
||||
pagination: Pagination = self.paginer(query_finale)
|
||||
self.total_pages: int = pagination.pages
|
||||
pagination: Pagination = self.paginer(r, no_pagination=self.no_pagination)
|
||||
self.total_pages = pagination.total_pages
|
||||
# Générer les lignes de la page
|
||||
for ligne in pagination.items:
|
||||
for ligne in pagination.items():
|
||||
row: RowAssiJusti = self.row_class(self, ligne._asdict())
|
||||
row.ajouter_colonnes()
|
||||
self.add_row(row)
|
||||
|
||||
def paginer(self, query: Query) -> Pagination:
|
||||
def paginer(self, collection: list, no_pagination: bool = False) -> Pagination:
|
||||
"""
|
||||
Applique la pagination à une requête SQLAlchemy en fonction des paramètres de la classe.
|
||||
Applique une pagination à une collection en fonction des paramètres de la classe.
|
||||
|
||||
Cette méthode prend une requête SQLAlchemy et applique la pagination en utilisant les
|
||||
Cette méthode prend une collection et applique la pagination en utilisant les
|
||||
attributs `page` et `NB_PAR_PAGE` de la classe `ListeAssiJusti`.
|
||||
|
||||
Args:
|
||||
query (Query): La requête SQLAlchemy à paginer. Il s'agit d'une requête qui a déjà
|
||||
collection (list): La collection à paginer. Il s'agit par exemple d'une requête qui a déjà
|
||||
été construite et qui est prête à être exécutée.
|
||||
|
||||
Returns:
|
||||
Pagination: Un objet Pagination qui encapsule les résultats de la requête paginée.
|
||||
|
||||
Note:
|
||||
Cette méthode ne modifie pas la requête originale; elle renvoie plutôt un nouvel
|
||||
Cette méthode ne modifie pas la collection originelle; elle renvoie plutôt un nouvel
|
||||
objet qui contient les résultats paginés.
|
||||
"""
|
||||
return query.paginate(
|
||||
page=self.options.page, per_page=self.options.nb_ligne_page, error_out=False
|
||||
return Pagination(
|
||||
collection,
|
||||
self.options.page,
|
||||
-1 if no_pagination else self.options.nb_ligne_page,
|
||||
)
|
||||
|
||||
def joindre(self, query_assiduite: Query = None, query_justificatif: Query = None):
|
||||
@ -172,7 +280,7 @@ class ListeAssiJusti(tb.Table):
|
||||
]
|
||||
|
||||
if self.options.show_desc:
|
||||
assiduites_entities.append(Assiduite.description.label("description"))
|
||||
assiduites_entities.append(Assiduite.description.label("desc"))
|
||||
|
||||
query_assiduite = query_assiduite.with_entities(*assiduites_entities)
|
||||
queries.append(query_assiduite)
|
||||
@ -194,7 +302,7 @@ class ListeAssiJusti(tb.Table):
|
||||
]
|
||||
|
||||
if self.options.show_desc:
|
||||
justificatifs_entities.append(Justificatif.raison.label("description"))
|
||||
justificatifs_entities.append(Justificatif.raison.label("desc"))
|
||||
|
||||
query_justificatif = query_justificatif.with_entities(
|
||||
*justificatifs_entities
|
||||
@ -210,7 +318,7 @@ class ListeAssiJusti(tb.Table):
|
||||
# Combiner les requêtes avec une union
|
||||
query_combinee = union(*queries).alias("combinee")
|
||||
|
||||
query_combinee = db.session.query(query_combinee).order_by(desc("date_debut"))
|
||||
query_combinee = db.session.query(query_combinee)
|
||||
|
||||
return query_combinee
|
||||
|
||||
@ -241,30 +349,46 @@ class RowAssiJusti(tb.Row):
|
||||
# Type d'objet
|
||||
self._type()
|
||||
|
||||
# Date de début
|
||||
multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
|
||||
# En excel, on export les "vraes dates".
|
||||
# En excel, on export les "vraies dates".
|
||||
# En HTML, on écrit en français (on laisse les dates pour le tri)
|
||||
|
||||
multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
|
||||
|
||||
date_affichees: list[str] = [
|
||||
self.ligne["date_debut"].strftime("%d/%m/%y de %H:%M"), # date début
|
||||
self.ligne["date_fin"].strftime("%d/%m/%y de %H:%M"), # date fin
|
||||
]
|
||||
|
||||
if multi_days:
|
||||
date_affichees[0] = self.ligne["date_debut"].strftime("%d/%m/%y")
|
||||
date_affichees[1] = self.ligne["date_fin"].strftime("%d/%m/%y")
|
||||
|
||||
self.add_cell(
|
||||
"date_debut",
|
||||
"Date de début",
|
||||
self.ligne["date_debut"].strftime("%d/%m/%y")
|
||||
if multi_days
|
||||
else self.ligne["date_debut"].strftime("%d/%m/%y de %H:%M"),
|
||||
date_affichees[0],
|
||||
data={"order": self.ligne["date_debut"]},
|
||||
raw_content=self.ligne["date_debut"],
|
||||
column_classes={"date", "date-debut"},
|
||||
column_classes={
|
||||
"date",
|
||||
"date-debut",
|
||||
"external-sort",
|
||||
"external-type:date_debut",
|
||||
},
|
||||
)
|
||||
# Date de fin
|
||||
self.add_cell(
|
||||
"date_fin",
|
||||
"Date de fin",
|
||||
self.ligne["date_fin"].strftime("%d/%m/%y")
|
||||
if multi_days
|
||||
else self.ligne["date_fin"].strftime("à %H:%M"),
|
||||
date_affichees[1],
|
||||
raw_content=self.ligne["date_fin"], # Pour excel
|
||||
data={"order": self.ligne["date_fin"]},
|
||||
column_classes={"date", "date-fin"},
|
||||
column_classes={
|
||||
"date",
|
||||
"date-fin",
|
||||
"external-sort",
|
||||
"external-type:date_fin",
|
||||
},
|
||||
)
|
||||
|
||||
# Ajout des colonnes optionnelles
|
||||
@ -283,7 +407,11 @@ class RowAssiJusti(tb.Row):
|
||||
data={"order": self.ligne["entry_date"] or ""},
|
||||
raw_content=self.ligne["entry_date"],
|
||||
classes=["small-font"],
|
||||
column_classes={"entry_date"},
|
||||
column_classes={
|
||||
"entry_date",
|
||||
"external-sort",
|
||||
"external-type:entry_date",
|
||||
},
|
||||
)
|
||||
|
||||
def _type(self) -> None:
|
||||
@ -349,10 +477,21 @@ class RowAssiJusti(tb.Row):
|
||||
|
||||
def _optionnelles(self) -> None:
|
||||
if self.table.options.show_desc:
|
||||
if self.ligne.get("type") == "justificatif":
|
||||
# protection de la "raison"
|
||||
if (
|
||||
self.ligne["user_id"] == current_user.id
|
||||
or self.table.can_view_justif_detail
|
||||
):
|
||||
description = self.ligne["desc"] if self.ligne["desc"] else ""
|
||||
else:
|
||||
description = "(cachée)"
|
||||
else:
|
||||
description = self.ligne["desc"] if self.ligne["desc"] else ""
|
||||
self.add_cell(
|
||||
"description",
|
||||
"Description",
|
||||
self.ligne["description"] if self.ligne["description"] else "",
|
||||
description,
|
||||
)
|
||||
if self.table.options.show_module:
|
||||
if self.ligne["type"] == "assiduite":
|
||||
@ -415,9 +554,13 @@ class RowAssiJusti(tb.Row):
|
||||
)
|
||||
html.append(f'<a title="Supprimer" href="{url}">❌</a>') # utiliser url_for
|
||||
|
||||
# Justifier (si type Assiduité et est_just faux)
|
||||
# Justifier (si type Assiduité, etat != Présent et est_just faux)
|
||||
|
||||
if self.ligne["type"] == "assiduite" and not self.ligne["est_just"]:
|
||||
if (
|
||||
self.ligne["type"] == "assiduite"
|
||||
and self.ligne["etat"] != EtatAssiduite.PRESENT
|
||||
and not self.ligne["est_just"]
|
||||
):
|
||||
url = url_for(
|
||||
"assiduites.tableau_assiduite_actions",
|
||||
type=self.ligne["type"],
|
||||
@ -541,6 +684,7 @@ class AssiDisplayOptions:
|
||||
show_etu: str | bool = True,
|
||||
show_actions: str | bool = True,
|
||||
show_module: str | bool = False,
|
||||
order: tuple[str, str | bool] = None,
|
||||
):
|
||||
self.page: int = page
|
||||
self.nb_ligne_page: int = nb_ligne_page
|
||||
@ -554,6 +698,10 @@ class AssiDisplayOptions:
|
||||
self.show_actions = to_bool(show_actions)
|
||||
self.show_module = to_bool(show_module)
|
||||
|
||||
self.order = (
|
||||
("date_debut", False) if order is None else (order[0], to_bool(order[1]))
|
||||
)
|
||||
|
||||
def remplacer(self, **kwargs):
|
||||
"Positionne options booléennes selon arguments"
|
||||
for k, v in kwargs.items():
|
||||
@ -565,6 +713,12 @@ class AssiDisplayOptions:
|
||||
self.nb_ligne_page = min(
|
||||
self.nb_ligne_page, ListeAssiJusti.MAX_PAR_PAGE
|
||||
)
|
||||
elif k == "order":
|
||||
setattr(
|
||||
self,
|
||||
k,
|
||||
("date_debut", False) if v is None else (v[0], to_bool(v[1])),
|
||||
)
|
||||
|
||||
|
||||
class AssiJustifData:
|
||||
|
@ -129,41 +129,44 @@ class RowAssi(tb.Row):
|
||||
)
|
||||
|
||||
def _get_etud_stats(self, etud: Identite) -> dict[str, list[str, float, float]]:
|
||||
# XXX TODO @iziram commentaire sur la fonction et la var. retour
|
||||
"""
|
||||
Renvoie le comptage (dans la métrique du département) des différents états d'assiduité d'un étudiant
|
||||
|
||||
Returns :
|
||||
{
|
||||
"<etat>" : [<Etat version lisible>, <nb total etat>, <nb just etat>]
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
# Préparation du retour
|
||||
retour: dict[str, tuple[str, float, float]] = {
|
||||
"absent": ["Absences", 0.0, 0.0],
|
||||
"retard": ["Retards", 0.0, 0.0],
|
||||
"present": ["Présences", 0.0, 0.0],
|
||||
}
|
||||
|
||||
# Récupération de la métrique du département
|
||||
assi_metric = scu.translate_assiduites_metric(
|
||||
sco_preferences.get_preference("assi_metrique", dept_id=g.scodoc_dept_id),
|
||||
)
|
||||
|
||||
compte_etat: dict[str, dict] = scass.get_assiduites_stats(
|
||||
assiduites=etud.assiduites,
|
||||
metric=assi_metric,
|
||||
filtered={
|
||||
"date_debut": self.dates[0],
|
||||
"date_fin": self.dates[1],
|
||||
"etat": "absent,present,retard", # pour tout compter d'un coup
|
||||
"split": 1, # afin d'avoir la division des stats en état, etatjust, etatnonjust
|
||||
},
|
||||
)
|
||||
|
||||
# Pour chaque état on met à jour les valeurs de retour
|
||||
for etat, valeur in retour.items():
|
||||
compte_etat = scass.get_assiduites_stats(
|
||||
assiduites=etud.assiduites,
|
||||
metric=assi_metric,
|
||||
filtered={
|
||||
"date_debut": self.dates[0],
|
||||
"date_fin": self.dates[1],
|
||||
"etat": etat,
|
||||
},
|
||||
)
|
||||
|
||||
compte_etat_just = scass.get_assiduites_stats(
|
||||
assiduites=etud.assiduites,
|
||||
metric=assi_metric,
|
||||
filtered={
|
||||
"date_debut": self.dates[0],
|
||||
"date_fin": self.dates[1],
|
||||
"etat": etat,
|
||||
"est_just": True,
|
||||
},
|
||||
)
|
||||
|
||||
valeur[1] = compte_etat[assi_metric]
|
||||
valeur[2] = compte_etat_just[assi_metric]
|
||||
valeur[1] = compte_etat[etat][assi_metric]
|
||||
if etat != "present":
|
||||
valeur[2] = compte_etat[etat]["justifie"][assi_metric]
|
||||
return retour
|
||||
|
||||
|
||||
|
@ -29,6 +29,15 @@
|
||||
</em>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<h2>Coordonnées du délégué à la protection des données (DPO)</h2>
|
||||
{% if ScoDocSiteConfig.get("rgpd_coordonnees_dpo") %}
|
||||
{{ ScoDocSiteConfig.get("rgpd_coordonnees_dpo") }}
|
||||
{% else %}
|
||||
<em>non renseigné</em>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h2>Dernières évolutions</h2>
|
||||
|
||||
{{ news|safe }}
|
||||
|
@ -6,7 +6,6 @@
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{% endblock %}
|
||||
|
||||
@ -88,6 +87,13 @@ div.submit > input {
|
||||
{{ form.modimpl }}
|
||||
{{ render_field_errors(form, 'modimpl') }}
|
||||
</div>
|
||||
{# Justifiée #}
|
||||
<div class="est-justifiee">
|
||||
{{ form.est_just.label }} :
|
||||
{{ form.est_just }}
|
||||
<span class="help">génère un justificatif valide ayant la même période que l'assiduité signalée</span>
|
||||
{{ render_field_errors(form, 'est_just') }}
|
||||
</div>
|
||||
{# Description #}
|
||||
<div>
|
||||
<div>{{ form.description.label }}</div>
|
||||
@ -114,19 +120,7 @@ div.submit > input {
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
<script>
|
||||
$('.timepicker').timepicker({
|
||||
timeFormat: 'HH:mm',
|
||||
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
|
||||
minTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
|
||||
maxTime: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
|
||||
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
|
||||
dynamic: false,
|
||||
dropdown: true,
|
||||
scrollbar: false
|
||||
});
|
||||
</script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
{% endblock scripts %}
|
||||
|
@ -1,246 +0,0 @@
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
|
||||
{% block pageContent %}
|
||||
<div class="pageContent">
|
||||
<h3>Signaler une absence, présence ou retard pour {{etud.html_link_fiche()|safe}}</h3>
|
||||
{% if saisie_eval %}
|
||||
<div id="saisie_eval">
|
||||
<br>
|
||||
<h3>
|
||||
La saisie a été préconfigurée en fonction de l'évaluation. <br>
|
||||
Une fois la saisie terminée, cliquez sur le lien ci-dessous
|
||||
</h3>
|
||||
<a href="{{redirect_url}}">retourner sur la page de l'évaluation</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<section class="assi-form page">
|
||||
<fieldset>
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="assi_date_debut" required>Date de début</legend>
|
||||
<input type="text" name="assi_date_debut" id="assi_date_debut" size="10"
|
||||
class="datepicker">
|
||||
<input type="text" name="assi_heure_debut" id="assi_heure_debut" size="5"
|
||||
class="timepicker">
|
||||
<span>Journée entière</span> <input type="checkbox" name="assi_journee" id="assi_journee">
|
||||
</div>
|
||||
<div class="assi-label" id="date_fin">
|
||||
<legend for="assi_date_fin" required>Date de fin</legend>
|
||||
<scodoc-datetime name="assi_date_fin" id="assi_date_fin"></scodoc-datetime>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="assi_etat" required>État de l'assiduité</legend>
|
||||
<select name="assi_etat" id="assi_etat">
|
||||
<option value="absent" selected>Absent</option>
|
||||
<option value="retard">Retard</option>
|
||||
<option value="present">Présent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="assi_module" required>Module</legend>
|
||||
{% with moduleid="ajout_assiduite_module_impl",label=false %}
|
||||
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="raison">Raison</legend>
|
||||
<textarea name="raison" id="raison" cols="75" rows="4" maxlength="500"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assi-row">
|
||||
<button onclick="validerFormulaire(this)">Enregistrer</button>
|
||||
<button onclick="effacerFormulaire()">Remettre à zero</button>
|
||||
</div>
|
||||
|
||||
|
||||
</fieldset>
|
||||
|
||||
</section>
|
||||
<section class="assi-liste">
|
||||
{{tableau | safe }}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.assi-row {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.assi-form fieldset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.pageContent {
|
||||
max-width: var(--sco-content-max-width);
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.assi-label {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
[required]::after {
|
||||
content: "*";
|
||||
color: var(--color-error);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
$('.timepicker').timepicker({
|
||||
timeFormat: 'HH:mm',
|
||||
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
|
||||
minTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
|
||||
maxTime: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
|
||||
defaultTime: 'now',
|
||||
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
|
||||
dynamic: false,
|
||||
dropdown: true,
|
||||
scrollbar: false
|
||||
});
|
||||
|
||||
function validateFields() {
|
||||
const field = document.querySelector('.assi-form')
|
||||
const { deb, fin } = getDates()
|
||||
const date_debut = new Date(deb);
|
||||
const date_fin = new Date(fin);
|
||||
|
||||
if (deb == "" || fin == "" || !date_debut.isValid() || !date_fin.isValid()) {
|
||||
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;
|
||||
}
|
||||
|
||||
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");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function fieldsToAssiduite() {
|
||||
const field = document.querySelector('.assi-form.page')
|
||||
|
||||
const { deb, fin } = getDates()
|
||||
|
||||
const etat = field.querySelector('#assi_etat').value;
|
||||
const raison = field.querySelector('#raison').value;
|
||||
const module = field.querySelector("#ajout_assiduite_module_impl").value;
|
||||
|
||||
return {
|
||||
date_debut: new Date(deb).toFakeIso(),
|
||||
date_fin: new Date(fin).toFakeIso(),
|
||||
etat: etat,
|
||||
description: raison,
|
||||
moduleimpl_id: module,
|
||||
}
|
||||
}
|
||||
|
||||
function validerFormulaire(btn) {
|
||||
if (!validateFields()) return
|
||||
|
||||
const assiduite = fieldsToAssiduite();
|
||||
let assiduite_id = null;
|
||||
|
||||
createAssiduiteComplete(assiduite, etudid);
|
||||
updateTableau();
|
||||
btn.disabled = true;
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
}, 1000)
|
||||
|
||||
}
|
||||
|
||||
function effacerFormulaire() {
|
||||
const field = document.querySelector('.assi-form')
|
||||
|
||||
field.querySelector('#assi_date_debut').value = "";
|
||||
field.querySelector('#assi_date_fin').value = "";
|
||||
field.querySelector('#assi_etat').value = "attente";
|
||||
field.querySelector('#raison').value = "";
|
||||
|
||||
}
|
||||
|
||||
function dayOnly() {
|
||||
const date_deb = document.getElementById("assi_date_debut");
|
||||
const date_fin = document.getElementById("assi_date_fin");
|
||||
|
||||
if (document.getElementById('assi_journee').checked) {
|
||||
date_deb.setAttribute("show", "date")
|
||||
date_fin.setAttribute("show", "date")
|
||||
document.querySelector(`legend[for="assi_date_fin"]`).removeAttribute("required")
|
||||
} else {
|
||||
date_deb.removeAttribute("show")
|
||||
date_fin.removeAttribute("show")
|
||||
document.querySelector(`legend[for="assi_date_fin"]`).setAttribute("required", "")
|
||||
}
|
||||
}
|
||||
|
||||
function getDates() {
|
||||
const date_deb = document.querySelector(".page #assi_date_debut")
|
||||
const date_fin = document.querySelector(".page #assi_date_fin")
|
||||
const journee = document.querySelector('.page #assi_journee').checked
|
||||
const deb = date_deb.valueAsObject.date + "T" + (journee ? assi_morning : date_deb.valueAsObject.time)
|
||||
let fin = "T" + (journee ? assi_evening : date_fin.valueAsObject.time)
|
||||
if (journee) {
|
||||
fin = (date_fin.valueAsObject.date || date_deb.valueAsObject.date) + fin
|
||||
} else {
|
||||
fin = date_fin.valueAsObject.date + fin
|
||||
}
|
||||
|
||||
return {
|
||||
"deb": deb,
|
||||
"fin": fin,
|
||||
}
|
||||
}
|
||||
|
||||
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}}';
|
||||
|
||||
{% if saisie_eval %}
|
||||
const saisie_eval = true;
|
||||
const date_deb = "{{date_deb}}";
|
||||
const date_fin = "{{date_fin}}";
|
||||
const moduleimpl = {{ moduleimpl_id }};
|
||||
{% else %}
|
||||
const saisie_eval = false;
|
||||
{% endif %}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
document.getElementById('assi_journee').addEventListener('click', () => { dayOnly() });
|
||||
dayOnly()
|
||||
|
||||
if (saisie_eval) {
|
||||
document.getElementById("assi_date_debut").value = Date.removeUTC(date_deb);
|
||||
document.getElementById("assi_date_fin").value = Date.removeUTC(date_fin);
|
||||
} else {
|
||||
const today = (new Date()).format("YYYY-MM-DD");
|
||||
document.getElementById("assi_date_debut").valueAsObject = { date: today, time: assi_morning }
|
||||
document.getElementById("assi_date_fin").valueAsObject = { time: assi_evening }
|
||||
}
|
||||
|
||||
|
||||
document.getElementById("assi_date_debut").addEventListener("blur", (event) => {
|
||||
updateSelect(null, "#ajout_assiduite_module_impl", event.target.valueAsObject.date)
|
||||
})
|
||||
|
||||
updateSelect(saisie_eval ? moduleimpl : "", "#ajout_assiduite_module_impl", document.getElementById("assi_date_debut").valueAsObject.date);
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock pageContent %}
|
@ -5,7 +5,6 @@ Si justif, edit #}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{% endblock %}
|
||||
|
||||
@ -18,6 +17,9 @@ form#ajout-justificatif-etud {
|
||||
form#ajout-justificatif-etud > div {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
fieldset > div {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
div.fichiers {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 32px;
|
||||
@ -34,9 +36,21 @@ div.submit {
|
||||
div.submit > input {
|
||||
margin-right: 16px;
|
||||
}
|
||||
.info-saisie {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
<div class="tab-content">
|
||||
<h2>Justifier des absences ou retards</h2>
|
||||
<h2>{{title|safe}}</h2>
|
||||
|
||||
{% if justif %}
|
||||
<div class="info-saisie">
|
||||
Saisie par {{justif.user.get_prenomnom() if justif.user else "inconnu"}}
|
||||
le {{justif.entry_date.strftime("%d/%m/%Y à %H:%M") if justif.entry_date else "?"}}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<section class="justi-form page">
|
||||
|
||||
@ -73,16 +87,24 @@ div.submit > input {
|
||||
</div>
|
||||
{# Raison #}
|
||||
<div>
|
||||
{% if (not justif) or can_view_justif_detail %}
|
||||
<div>{{ form.raison.label }}</div>
|
||||
{{ form.raison() }}
|
||||
{{ render_field_errors(form, 'raison') }}
|
||||
<div class="help">La raison sera visible aux utilisateurs ayant le droit
|
||||
<tt>AbsJustifView</tt> et à celui ayant déposé le justificatif
|
||||
{%- if justif %} (<b>{{justif.user.get_prenomnom()}}</b>){%- endif -%}.
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="unauthorized">raison confidentielle</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="fichiers">
|
||||
{# Liste des fichiers existants #}
|
||||
{% if justif and nb_files > 0 %}
|
||||
<div><b>{{nb_files}} fichiers justificatifs déposés
|
||||
{% if filenames|length < nb_files %}
|
||||
, dont {{filenames|length}} vous sont accessibles
|
||||
, dont {{filenames|length}} vous {{'sont accessibles' if filenames|length > 1 else 'est accessible'}}
|
||||
{% endif %}
|
||||
</b>
|
||||
</div>
|
||||
@ -105,6 +127,7 @@ div.submit > input {
|
||||
{{ form.entry_date.label }} : {{ form.entry_date }}
|
||||
<span class="help">laisser vide pour date courante</span>
|
||||
{{ render_field_errors(form, 'entry_date') }}
|
||||
|
||||
{# Submit #}
|
||||
<div class="submit">
|
||||
{{ form.submit }} {{ form.cancel }}
|
||||
@ -126,21 +149,9 @@ div.submit > input {
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
<script>
|
||||
$('.timepicker').timepicker({
|
||||
timeFormat: 'HH:mm',
|
||||
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
|
||||
minTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
|
||||
maxTime: "{{ scu.get_assiduites_time_config("assi_afternoon_time") }}",
|
||||
startTime: "{{ scu.get_assiduites_time_config("assi_morning_time") }}",
|
||||
dynamic: false,
|
||||
dropdown: true,
|
||||
scrollbar: false
|
||||
});
|
||||
</script>
|
||||
{% include "sco_timepicker.j2" %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
// Suppression d'un fichier justificatif
|
||||
|
@ -1,187 +1,27 @@
|
||||
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||
<section class="alerte invisible">
|
||||
<p>Attention, cet étudiant a trop d'absences</p>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="annee">
|
||||
<span>Année scolaire 2022-2023 Changer année: </span>
|
||||
<select name="" id="annee" onchange="setterAnnee(this.value)">
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="legende">
|
||||
<h3>Gestion des justificatifs</h3>
|
||||
<p>
|
||||
Faites
|
||||
<span style="font-style: italic;">clic droit</span> sur une ligne du tableau pour afficher le menu contextuel :
|
||||
<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>
|
||||
</ul>
|
||||
{% extends "sco_page.j2" %}
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{% endblock styles %}
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
{% endblock scripts %}
|
||||
{% block app_content %}
|
||||
<h2>Traitement de l'assiduité</h2>
|
||||
<p class="help">
|
||||
Pour saisir l'assiduité ou consulter les états, il est recommandé de passer par
|
||||
le semestre concerné (saisie par jour ou saisie différée).
|
||||
</p>
|
||||
</div>
|
||||
<script>
|
||||
<p class="help">Pour signaler, annuler ou justifier l'assiduité d'un seul étudiant,
|
||||
choisissez d'abord la personne concernée :</p>
|
||||
<br>
|
||||
{{search_etud | safe}}
|
||||
<br>
|
||||
{{billets | safe}}
|
||||
|
||||
let formsemestre_id = "{{formsemestre_id}}"
|
||||
let group_id = "{{group_id}}"
|
||||
|
||||
function getDeptJustificatifsFromPeriod(action) {
|
||||
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) => {
|
||||
if (action) {
|
||||
action(data)
|
||||
} else {
|
||||
justificatifCallBack(data);
|
||||
}
|
||||
},
|
||||
(data, status) => {
|
||||
console.error(data, status)
|
||||
errorAlert();
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function generate(annee) {
|
||||
|
||||
if (annee < 1999 || annee > 2999) {
|
||||
openAlertModal("Année impossible", document.createTextNode("L'année demandé n'existe pas."));
|
||||
return;
|
||||
}
|
||||
bornes = {
|
||||
deb: `${annee}-09-01T00:00`,
|
||||
fin: `${annee + 1}-08-31T23:59`
|
||||
}
|
||||
|
||||
defAnnee = annee;
|
||||
|
||||
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: `
|
||||
generate(annee)
|
||||
|
||||
}
|
||||
let defAnnee = {{ annee }};
|
||||
let bornes = {
|
||||
deb: `${defAnnee}-09-01T00:00`,
|
||||
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",
|
||||
"date_fin",
|
||||
"etat",
|
||||
"raison",
|
||||
"fichier"
|
||||
],
|
||||
"filters": {
|
||||
"etat": [
|
||||
"attente",
|
||||
"modifie"
|
||||
],
|
||||
}
|
||||
}
|
||||
const select = document.querySelector('#annee');
|
||||
|
||||
annees.forEach((a) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = a + "",
|
||||
opt.textContent = `${a} - ${a + 1}`;
|
||||
if (a === defAnnee) {
|
||||
opt.selected = true;
|
||||
}
|
||||
select.appendChild(opt)
|
||||
})
|
||||
setterAnnee(defAnnee)
|
||||
})
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.stats-values-item {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats {
|
||||
border: 1px solid #333;
|
||||
padding: 5px 2px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.stats-values {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stats-values-item h5 {
|
||||
font-weight: bold;
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.stats-values-part {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.alerte {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
border-radius: 7px;
|
||||
|
||||
background-color: var(--color-error);
|
||||
|
||||
}
|
||||
|
||||
.alerte.invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alerte p {
|
||||
font-size: larger;
|
||||
color: whitesmoke;
|
||||
|
||||
}
|
||||
|
||||
.suppr {
|
||||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
<br>
|
||||
<section class="nonvalide">
|
||||
{{tableau | safe }}
|
||||
</section>
|
||||
{% endblock app_content %}
|
@ -1,3 +1,70 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block title %}
|
||||
Bilan assiduité de {{sco.etud.nomprenom}}
|
||||
{% endblock title %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
<style>
|
||||
.stats-values-item {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stats {
|
||||
border: 1px solid #333;
|
||||
padding: 5px 2px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.stats-values {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stats-values-item h5 {
|
||||
font-weight: bold;
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.stats-values-part {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.alerte {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
border-radius: 7px;
|
||||
|
||||
background-color: var(--color-error);
|
||||
|
||||
}
|
||||
|
||||
.alerte.invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alerte p {
|
||||
font-size: larger;
|
||||
color: whitesmoke;
|
||||
|
||||
}
|
||||
|
||||
.suppr {
|
||||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock styles %}
|
||||
|
||||
{% block app_content %}
|
||||
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||
<div class="pageContent">
|
||||
@ -25,27 +92,7 @@
|
||||
</section>
|
||||
|
||||
<section class="nonvalide">
|
||||
<!-- Tableaux des assiduités (retard/abs) non justifiées -->
|
||||
<h4>Absences et retards non justifiés</h4>
|
||||
|
||||
{# XXX XXX XXX #}
|
||||
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
|
||||
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
|
||||
</div>
|
||||
|
||||
<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" %}
|
||||
|
||||
{{tableau | safe }}
|
||||
</section>
|
||||
|
||||
<section class="suppr">
|
||||
@ -60,36 +107,18 @@
|
||||
département)</p>
|
||||
<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>
|
||||
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 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 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'é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>
|
||||
|
||||
</div>
|
||||
{% endblock app_content %}
|
||||
|
||||
<script>
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
|
||||
<script>
|
||||
function stats() {
|
||||
const dd_val = document.getElementById('stats_date_debut').value;
|
||||
const df_val = document.getElementById('stats_date_fin').value;
|
||||
@ -111,42 +140,28 @@
|
||||
|
||||
}
|
||||
|
||||
function getAssiduitesCount(dateDeb, dateFin, query) {
|
||||
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&${query}`;
|
||||
function getAssiduitesCount(dateDeb, dateFin, action) {
|
||||
const url_api = getUrl() + `/api/assiduites/${etudid}/count/query?date_debut=${dateDeb}&date_fin=${dateFin}&etat=absent,retard,present&split`;
|
||||
//Utiliser async_get au lieu de Jquery
|
||||
return $.ajax({
|
||||
async: true,
|
||||
type: "GET",
|
||||
url: url_api,
|
||||
success: (data, status) => {
|
||||
if (status === "success") {
|
||||
}
|
||||
},
|
||||
error: () => { },
|
||||
});
|
||||
async_get(
|
||||
url_api,
|
||||
action,
|
||||
()=>{},
|
||||
);
|
||||
}
|
||||
|
||||
function countAssiduites(dateDeb, dateFin) {
|
||||
//TODO Utiliser Fetch when plutot que jquery
|
||||
$.when(
|
||||
getAssiduitesCount(dateDeb, dateFin, `etat=present`),
|
||||
getAssiduitesCount(dateDeb, dateFin, `etat=retard`),
|
||||
getAssiduitesCount(dateDeb, dateFin, `etat=retard&est_just=v`),
|
||||
getAssiduitesCount(dateDeb, dateFin, `etat=absent`),
|
||||
getAssiduitesCount(dateDeb, dateFin, `etat=absent&est_just=v`),
|
||||
).then(
|
||||
(pt, rt, rj, at, aj) => {
|
||||
function showStats(data){
|
||||
const counter = {
|
||||
"present": {
|
||||
"total": pt[0],
|
||||
"total": data["present"],
|
||||
},
|
||||
"retard": {
|
||||
"total": rt[0],
|
||||
"justi": rj[0],
|
||||
"total": data["retard"],
|
||||
"justi": data["retard"]["justifie"],
|
||||
},
|
||||
"absent": {
|
||||
"total": at[0],
|
||||
"justi": aj[0],
|
||||
"total": data["absent"],
|
||||
"justi": data["absent"]["justifie"],
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +181,7 @@
|
||||
}
|
||||
|
||||
const heure = document.createElement('span');
|
||||
heure.textContent = `${counter[key].total.heure} heure(s)${withJusti(key, "heure")}`;
|
||||
heure.textContent = `${counter[key].total.heure.toFixed(2)} heure(s)${withJusti(key, "heure")}`;
|
||||
|
||||
const demi = document.createElement('span');
|
||||
demi.textContent = `${counter[key].total.demi} demi-journée(s)${withJusti(key, "demi")}`;
|
||||
@ -184,7 +199,7 @@
|
||||
values.appendChild(item);
|
||||
});
|
||||
|
||||
const nbAbs = counter.absent.total[assi_metric] - counter.absent.justi[assi_metric];
|
||||
const nbAbs = data["absent"]["non_justifie"][assi_metric];
|
||||
if (nbAbs > assi_seuil) {
|
||||
document.querySelector('.alerte').classList.remove('invisible');
|
||||
document.querySelector('.alerte p').textContent = `Attention, cet étudiant a trop d'absences ${nbAbs} / ${assi_seuil} (${metriques[assi_metric]})`
|
||||
@ -192,8 +207,9 @@
|
||||
document.querySelector('.alerte').classList.add('invisible');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function countAssiduites(dateDeb, dateFin) {
|
||||
getAssiduitesCount(dateDeb, dateFin, showStats);
|
||||
}
|
||||
|
||||
function removeAllAssiduites() {
|
||||
@ -288,105 +304,12 @@
|
||||
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
filterAssiduites = {
|
||||
"columns": [
|
||||
"entry_date",
|
||||
"date_debut",
|
||||
"date_fin",
|
||||
"etat",
|
||||
"moduleimpl_id",
|
||||
"est_just"
|
||||
],
|
||||
"filters": {
|
||||
"etat": [
|
||||
"retard",
|
||||
"absent"
|
||||
],
|
||||
"moduleimpl_id": "",
|
||||
"est_just": "false"
|
||||
}
|
||||
};
|
||||
|
||||
filterJustificatifs = {
|
||||
"columns": [
|
||||
"entry_date",
|
||||
"date_debut",
|
||||
"date_fin",
|
||||
"etat",
|
||||
"raison",
|
||||
"fichier"
|
||||
],
|
||||
"filters": {
|
||||
"etat": [
|
||||
"attente",
|
||||
"modifie"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('stats_date_fin').value = assi_date_fin;
|
||||
document.getElementById('stats_date_debut').value = assi_date_debut;
|
||||
|
||||
|
||||
|
||||
loadAll();
|
||||
stats();
|
||||
})
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.stats-values-item {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
.stats {
|
||||
border: 1px solid #333;
|
||||
padding: 5px 2px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.stats-values {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.stats-values-item h5 {
|
||||
font-weight: bold;
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.stats-values-part {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.alerte {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
border-radius: 7px;
|
||||
|
||||
background-color: var(--color-error);
|
||||
|
||||
}
|
||||
|
||||
.alerte.invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.alerte p {
|
||||
font-size: larger;
|
||||
color: whitesmoke;
|
||||
|
||||
}
|
||||
|
||||
.suppr {
|
||||
margin: 5px 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,4 +1,14 @@
|
||||
{% block pageContent %}
|
||||
{% extends "sco_page.j2" %}
|
||||
{% block title %}
|
||||
Calendrier de l'assiduité
|
||||
{% endblock title %}
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block app_content %}
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
|
||||
<div class="pageContent">
|
||||
@ -250,219 +260,6 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
.day .dayline {
|
||||
position: absolute;
|
||||
display: none;
|
||||
top: 100%;
|
||||
z-index: 50;
|
||||
width: max-content;
|
||||
height: 75px;
|
||||
background-color: #dedede;
|
||||
border-radius: 15px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.day:hover .dayline {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dayline .mini-timeline {
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
.dayline-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dayline .mini_tick {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
transform: translateY(-110%);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.dayline .mini_tick::after {
|
||||
display: block;
|
||||
content: "|";
|
||||
position: absolute;
|
||||
bottom: -69%;
|
||||
z-index: 2;
|
||||
transform: translateX(200%);
|
||||
}
|
||||
|
||||
#label-nom,
|
||||
#label-justi {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.demi .day {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.demi .day>span {
|
||||
display: block;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
border: 1px solid #d5d5d5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.demi .day>span:first-of-type {
|
||||
width: 3em;
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.options>* {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.options input {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.options label {
|
||||
font-weight: normal;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
|
||||
/*Gestion des bubbles*/
|
||||
.assiduite-bubble {
|
||||
position: relative;
|
||||
display: none;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
border: 3px solid #ccc;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
z-index: 500;
|
||||
min-width: max-content;
|
||||
top: 200%;
|
||||
}
|
||||
|
||||
.mini-timeline-block:hover .assiduite-bubble {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.assiduite-bubble::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #f9f9f9 transparent;
|
||||
}
|
||||
|
||||
.assiduite-bubble::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #ccc transparent;
|
||||
}
|
||||
|
||||
.assiduite-id,
|
||||
.assiduite-period,
|
||||
.assiduite-state,
|
||||
.assiduite-user_id {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.assiduite-bubble.absent {
|
||||
border-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.assiduite-bubble.present {
|
||||
border-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.assiduite-bubble.retard {
|
||||
border-color: var(--color-retard) !important;
|
||||
}
|
||||
|
||||
/*Gestion des minitimelines*/
|
||||
.mini-timeline {
|
||||
height: 7px;
|
||||
border: 1px solid black;
|
||||
position: relative;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.mini-timeline.single {
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.mini-timeline-block {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mini-timeline-block {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mini_tick {
|
||||
position: absolute;
|
||||
text-align: start;
|
||||
top: -40px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 50;
|
||||
|
||||
}
|
||||
|
||||
.mini_tick::after {
|
||||
display: block;
|
||||
content: "|";
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mini-timeline-block.creneau {
|
||||
outline: 3px solid var(--color-primary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mini-timeline-block.absent {
|
||||
background-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.present {
|
||||
background-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.retard {
|
||||
background-color: var(--color-retard) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.justified {
|
||||
background-image: var(--motif-justi);
|
||||
}
|
||||
|
||||
.mini-timeline-block.invalid_justified {
|
||||
background-image: var(--motif-justi-invalide);
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
.couleurs.print {
|
||||
@ -593,4 +390,4 @@
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock pageContent %}
|
||||
{% endblock app_content %}
|
||||
|
@ -1,11 +1,6 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
{% import 'wtf.j2' as wtf %}
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
|
||||
{% endblock %}
|
||||
|
||||
{% block app_content %}
|
||||
{% for err_msg in form.error_messages %}
|
||||
<div class="wtf-error-messages">
|
||||
@ -24,7 +19,3 @@
|
||||
</form>
|
||||
|
||||
{% endblock app_content %}
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
|
||||
{% endblock scripts %}
|
@ -3,6 +3,7 @@
|
||||
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.css"/>
|
||||
<style>
|
||||
div.config-section {
|
||||
font-weight: bold;
|
||||
@ -31,8 +32,18 @@ div.config-section {
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/timepicker-1.3.5/jquery.timepicker.min.js"></script>
|
||||
|
||||
<script>
|
||||
$('.timepicker').timepicker({
|
||||
timeFormat: 'HH:mm',
|
||||
interval: {{ scu.get_assiduites_time_config("assi_tick_time") }},
|
||||
minTime: "00:00",
|
||||
maxTime: "23:59",
|
||||
dynamic: false,
|
||||
dropdown: true,
|
||||
scrollbar: false
|
||||
});
|
||||
function update_test_button_state() {
|
||||
var inputValue = document.getElementById('test_edt_id').value;
|
||||
document.getElementById('test_load_ics').disabled = inputValue.length === 0;
|
||||
@ -78,10 +89,9 @@ c'est à dire à la montre des étudiants.
|
||||
<div class="col-md-8">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ wtf.form_errors(form, hiddens="only") }}
|
||||
|
||||
{{ wtf.form_field(form.assi_morning_time) }}
|
||||
{{ wtf.form_field(form.assi_lunch_time) }}
|
||||
{{ wtf.form_field(form.assi_afternoon_time) }}
|
||||
{{ wtf.form_field(form.assi_morning_time, class="timepicker") }}
|
||||
{{ wtf.form_field(form.assi_lunch_time, class="timepicker") }}
|
||||
{{ wtf.form_field(form.assi_afternoon_time, class="timepicker") }}
|
||||
{{ wtf.form_field(form.assi_tick_time) }}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,3 +1,21 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block title %}
|
||||
Assiduité de {{etud.nomprenom}}
|
||||
{% endblock title %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block app_content %}
|
||||
<div class="pageContent">
|
||||
|
||||
|
@ -1,94 +0,0 @@
|
||||
{% 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 %}
|
@ -1,5 +1,34 @@
|
||||
{#
|
||||
|
||||
- TODO : revoir le fonctionnement de cette page (trop lente / complexe)
|
||||
- Utiliser majoritairement du python
|
||||
#}
|
||||
|
||||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block title %}
|
||||
{{title}}
|
||||
{% endblock title %}
|
||||
|
||||
{% block app_content %}
|
||||
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% include "assiduites/widgets/prompt.j2" %}
|
||||
{% include "assiduites/widgets/conflict.j2" %}
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
|
||||
<h2>Signalement différé de l'assiduité {{gr |safe}}</h2>
|
||||
|
||||
<div class="ue_warning">Attention, cette page utilise des couleurs et conventions différentes
|
||||
de celles des autres pages ScoDoc: elle sera prochainement modifée, merci de votre patience.
|
||||
</div>
|
||||
|
||||
<h3>{{sem | safe }}</h3>
|
||||
|
||||
{{diff | safe}}
|
||||
@ -18,7 +47,13 @@
|
||||
<p>Vous pouvez supprimer une colonne en appuyant sur la croix qui se situe dans le coin haut droit de la colonne.
|
||||
</p>
|
||||
</div>
|
||||
{% endblock app_content %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
<script>
|
||||
const etudsDefDem = {{ defdem | safe }}
|
||||
|
||||
@ -62,14 +97,5 @@
|
||||
createColumn();
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% include "assiduites/widgets/prompt.j2" %}
|
||||
{% include "assiduites/widgets/conflict.j2" %}
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% endblock scripts %}
|
||||
|
@ -1,160 +0,0 @@
|
||||
{# -*- mode: jinja-html -*- #}
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% include "assiduites/widgets/alert.j2" %}
|
||||
{% include "assiduites/widgets/prompt.j2" %}
|
||||
{% include "assiduites/widgets/conflict.j2" %}
|
||||
<div id="page-assiduite-content">
|
||||
{% block content %}
|
||||
<h2>Signalement de l'assiduité de <span class="rouge">{{sco.etud.nomprenom}}</span></h2>
|
||||
|
||||
<div class="infos">
|
||||
Date: <span id="datestr"></span>
|
||||
<input type="text" class="datepicker" name="tl_date" id="tl_date" value="{{ date }}">
|
||||
</div>
|
||||
|
||||
{{timeline|safe}}
|
||||
|
||||
|
||||
<div>
|
||||
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
|
||||
<button class="btn" onclick="fastJustify(getCurrentAssiduite(etudid))" id="justif-rapide">Justifier</button>
|
||||
</div>
|
||||
|
||||
<div class="btn_group">
|
||||
<button class="btn" onclick="setTimeLineTimes({{morning}},{{afternoon}})">Journée</button>
|
||||
<button class="btn" onclick="setTimeLineTimes({{morning}},{{lunch}})">Matin</button>
|
||||
<button class="btn" onclick="setTimeLineTimes({{lunch}},{{afternoon}})">Après-midi</button>
|
||||
</div>
|
||||
|
||||
<div class="etud_holder">
|
||||
<div id="etud_row_{{sco.etud.id}}">
|
||||
<div class="index"></div>
|
||||
</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">
|
||||
<h3>Explication de la timeline</h3>
|
||||
<p>
|
||||
Si la période indiquée par la timeline provoque un conflit d'assiduité pour un étudiant sa ligne deviendra
|
||||
rouge.
|
||||
<br>
|
||||
Dans ce cas il faut résoudre manuellement le conflit : cliquez sur un des boutons d'assiduités pour ouvrir
|
||||
le
|
||||
résolveur de conflit.
|
||||
<br>
|
||||
Correspondance des couleurs :
|
||||
</p>
|
||||
<ul>
|
||||
{% include "assiduites/widgets/legende_couleur.j2" %}
|
||||
</ul>
|
||||
|
||||
<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 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 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>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Ajout d'un conteneur pour le loader -->
|
||||
<div class="loader-container" id="loaderContainer">
|
||||
<div class="loader"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
const etudid = {{ sco.etud.id }};
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
setupDate(() => {
|
||||
if (updateDate()) {
|
||||
actualizeEtud(etudid);
|
||||
updateSelect();
|
||||
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
|
||||
onlyAbs();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
setupTimeLine(() => {
|
||||
if(document.querySelector('.etud_holder .placeholder') != null){
|
||||
generateAllEtudRow();
|
||||
}
|
||||
updateJustifyBtn();
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
updateDate();
|
||||
getSingleEtud(etudid);
|
||||
actualizeEtud(etudid);
|
||||
updateSelect()
|
||||
updateJustifyBtn();
|
||||
})
|
||||
|
||||
|
||||
function setTimeLineTimes(a, b) {
|
||||
setPeriodValues(a, b);
|
||||
updateJustifyBtn();
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
|
||||
<style>
|
||||
.justifie {
|
||||
background-color: rgb(104, 104, 252);
|
||||
color: whitesmoke;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
</div>
|
@ -1,4 +1,86 @@
|
||||
{% extends "sco_page.j2" %}
|
||||
|
||||
{% block title %}
|
||||
{{title}}
|
||||
{% endblock title %}
|
||||
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/purl.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/etud_info.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/groups_view.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/date_utils.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/js/assiduites.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
{% if readonly != "false" %}
|
||||
function getPeriodValues(){
|
||||
return [0, 23]
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
const readOnly = {{ readonly }};
|
||||
|
||||
|
||||
setupDate();
|
||||
updateDate();
|
||||
if (!readOnly){
|
||||
setupTimeLine(()=>{
|
||||
generateAllEtudRow();
|
||||
});
|
||||
}
|
||||
|
||||
window.forceModule = "{{ forcer_module }}"
|
||||
window.forceModule = window.forceModule == "True" ? true : false
|
||||
|
||||
const etudsDefDem = {{ defdem | safe }}
|
||||
|
||||
const select = document.getElementById("moduleimpl_select");
|
||||
|
||||
select?.addEventListener('change', (e) => {
|
||||
generateAllEtudRow();
|
||||
});
|
||||
|
||||
if (window.forceModule) {
|
||||
const btn = document.getElementById("validate_selectors");
|
||||
|
||||
|
||||
if (!readOnly && select.value == "") {
|
||||
document.getElementById('forcemodule').style.display = "block";
|
||||
}
|
||||
|
||||
select?.addEventListener('change', (e) => {
|
||||
if (e.target.value != "") {
|
||||
document.getElementById('forcemodule').style.display = "none";
|
||||
} else {
|
||||
document.getElementById('forcemodule').style.display = "block";
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock scripts %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap/css/bootstrap-theme.min.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/libjs/bootstrap-multiselect-1.1.2/bootstrap-multiselect.min.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/assiduites.css">
|
||||
<link rel="stylesheet" href="{{scu.STATIC_DIR}}/css/minitimeline.css">
|
||||
{% endblock styles %}
|
||||
|
||||
|
||||
{% block app_content %}
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
|
||||
{{ minitimeline|safe }}
|
||||
|
||||
<section id="content">
|
||||
|
||||
<div class="no-display">
|
||||
@ -20,7 +102,7 @@
|
||||
<div class="infos-button">Groupes : {{grp|safe}}</div>
|
||||
<div class="infos-button" style="margin-left: 24px;">Date : <span style="margin-left: 8px;"
|
||||
id="datestr"></span>
|
||||
<input type="text" class="datepicker" name="tl_date" id="tl_date" value="{{ date }}"
|
||||
<input type="text" name="tl_date" id="tl_date" value="{{ date }}"
|
||||
onchange="updateDate()">
|
||||
</div>
|
||||
</div>
|
||||
@ -47,7 +129,6 @@
|
||||
Faire la saisie
|
||||
</button>
|
||||
{% endif %}
|
||||
<p>Utilisez le bouton "Actualiser" si vous modifier la date ou le(s) groupe(s) sélectionné(s)</p>
|
||||
|
||||
|
||||
<div class="etud_holder">
|
||||
@ -79,57 +160,6 @@
|
||||
{% include "assiduites/widgets/prompt.j2" %}
|
||||
{% include "assiduites/widgets/conflict.j2" %}
|
||||
|
||||
<script>
|
||||
|
||||
{% if readonly != "false" %}
|
||||
function getPeriodValues(){
|
||||
return [0, 23]
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
|
||||
const nonWorkDays = [{{ nonworkdays| safe }}];
|
||||
|
||||
const readOnly = {{ readonly }};
|
||||
|
||||
|
||||
setupDate();
|
||||
updateDate();
|
||||
if (!readOnly){
|
||||
setupTimeLine(()=>{
|
||||
if(document.querySelector('.etud_holder .placeholder') != null){
|
||||
generateAllEtudRow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.forceModule = "{{ forcer_module }}"
|
||||
window.forceModule = window.forceModule == "True" ? true : false
|
||||
|
||||
const etudsDefDem = {{ defdem | safe }}
|
||||
|
||||
const select = document.getElementById("moduleimpl_select");
|
||||
|
||||
select?.addEventListener('change', (e) => {
|
||||
generateAllEtudRow();
|
||||
});
|
||||
|
||||
if (window.forceModule) {
|
||||
const btn = document.getElementById("validate_selectors");
|
||||
|
||||
|
||||
if (!readOnly && select.value == "") {
|
||||
document.getElementById('forcemodule').style.display = "block";
|
||||
}
|
||||
|
||||
select?.addEventListener('change', (e) => {
|
||||
if (e.target.value != "") {
|
||||
document.getElementById('forcemodule').style.display = "none";
|
||||
} else {
|
||||
document.getElementById('forcemodule').style.display = "block";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
</section>
|
||||
|
||||
{% endblock app_content %}
|
||||
|
@ -22,7 +22,7 @@
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-top: 32px;">
|
||||
<a href="" id="lien-retour">retour</a>
|
||||
<a class="stdlink" href="" id="lien-retour">retour</a>
|
||||
</div>
|
||||
<script>
|
||||
window.addEventListener('load', () => {
|
||||
|
@ -3,5 +3,6 @@
|
||||
<div class="assiduite-period">{{date_debut}}</div>
|
||||
<div class="assiduite-period">{{date_fin}}</div>
|
||||
<div class="assiduite-state">État: {{etat}}</div>
|
||||
<div class="assiduite-why">Motif: {{motif}}</div>
|
||||
<div class="assiduite-user_id">{{saisie}}</div>
|
||||
</div>
|
@ -6,8 +6,8 @@
|
||||
*/
|
||||
function getLeftPosition(start) {
|
||||
const startTime = new Date(start);
|
||||
const startMins = (startTime.getHours() - 8) * 60 + startTime.getMinutes();
|
||||
return (startMins / (18 * 60 - 8 * 60)) * 100 + "%";
|
||||
const startMins = (startTime.getHours() - t_start) * 60 + startTime.getMinutes();
|
||||
return (startMins / (t_end * 60 - t_start * 60)) * 100 + "%";
|
||||
}
|
||||
/**
|
||||
* Ajustement de l'espacement vertical entre les assiduités superposées
|
||||
@ -76,13 +76,7 @@
|
||||
|
||||
const duration = (endTime - startTime) / 1000 / 60;
|
||||
|
||||
const percent = (duration / (18 * 60 - 8 * 60)) * 100
|
||||
|
||||
if (percent > 100) {
|
||||
console.log(start, end);
|
||||
console.log(startTime, endTime)
|
||||
}
|
||||
|
||||
const percent = (duration / (t_end * 60 - t_start * 60)) * 100
|
||||
return percent + "%";
|
||||
}
|
||||
|
||||
@ -162,6 +156,13 @@
|
||||
|
||||
document.querySelector('#myModal .close').addEventListener('click', () => { this.close() })
|
||||
|
||||
// fermeture du modal en appuyant sur echap
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
this.close()
|
||||
}
|
||||
}, { once: true })
|
||||
|
||||
this.render()
|
||||
}
|
||||
|
||||
@ -246,12 +247,11 @@
|
||||
*/
|
||||
splitAssiduiteModal() {
|
||||
//Préparation du prompt
|
||||
const htmlPrompt = `<legend>Entrez l'heure de séparation (HH:mm) :</legend>
|
||||
<input type="time" id="promptTime" name="appt"
|
||||
min="08:00" max="18:00" required>`;
|
||||
const htmlPrompt = `<legend>Entrez l'heure de séparation</legend>
|
||||
<input type="text" id="promptTime" name="appt"required style="position: relative; z-index: 100000;">`;
|
||||
|
||||
const fieldSet = document.createElement("fieldset");
|
||||
fieldSet.classList.add("fieldsplit");
|
||||
fieldSet.classList.add("fieldsplit", "timepicker");
|
||||
fieldSet.innerHTML = htmlPrompt;
|
||||
|
||||
//Callback de division
|
||||
@ -309,11 +309,28 @@
|
||||
"L'heure de séparation doit être compris dans la période de l'assiduité sélectionnée."
|
||||
);
|
||||
|
||||
openAlertModal("Attention", att, "", "var(--color-warning))");
|
||||
openAlertModal("Attention", att, "", "var(--color-warning)");
|
||||
}
|
||||
};
|
||||
|
||||
openPromptModal("Séparation de l'assiduité sélectionnée", fieldSet, success, () => { }, "var(--color-present)");
|
||||
// Initialisation du timepicker
|
||||
const deb = this.selectedAssiduite.date_debut.substring(11,16);
|
||||
const fin = this.selectedAssiduite.date_fin.substring(11,16);
|
||||
setTimeout(()=>{
|
||||
$('#promptTime').timepicker({
|
||||
timeFormat: 'HH:mm',
|
||||
interval: 60 * tick_delay,
|
||||
minTime: deb,
|
||||
startTime: deb,
|
||||
maxTime: fin,
|
||||
dynamic: false,
|
||||
dropdown: true,
|
||||
scrollbar: false,
|
||||
|
||||
});
|
||||
}, 100
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -371,8 +388,7 @@
|
||||
assiduitesContainer.innerHTML = '<div class="assiduite-special"></div>';
|
||||
|
||||
// Ajout des labels d'heure sur la frise chronologique
|
||||
// TODO permettre la modification des bornes (8 et 18)
|
||||
for (let i = 8; i <= 18; i++) {
|
||||
for (let i = t_start; i <= t_end; i++) {
|
||||
const timeLabel = document.createElement("div");
|
||||
timeLabel.className = "time-label";
|
||||
timeLabel.textContent = i < 10 ? `0${i}:00` : `${i}:00`;
|
||||
@ -460,3 +476,8 @@
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.ui-timepicker-container {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
</style>
|
@ -270,6 +270,10 @@
|
||||
-webkit-box-sizing: border-box;
|
||||
border: 10px solid white;
|
||||
}
|
||||
|
||||
.mini-form {
|
||||
color: black;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
@ -73,11 +73,6 @@
|
||||
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
|
||||
updateJustifyBtn();
|
||||
}
|
||||
try {
|
||||
if (isCalendrier()) {
|
||||
window.location = `liste_assiduites_etud?etudid=${etudid}&assiduite_id=${assiduité.assiduite_id}`
|
||||
}
|
||||
} catch { }
|
||||
});
|
||||
//ajouter affichage assiduites on over
|
||||
setupAssiduiteBuble(block, assiduité);
|
||||
@ -138,12 +133,10 @@
|
||||
*/
|
||||
function setupAssiduiteBuble(el, assiduite) {
|
||||
if (!assiduite) return;
|
||||
el.addEventListener("mouseenter", (event) => {
|
||||
const bubble = document.querySelector(".assiduite-bubble");
|
||||
bubble.className = "assiduite-bubble";
|
||||
bubble.classList.add("is-active", assiduite.etat.toLowerCase());
|
||||
|
||||
bubble.innerHTML = "";
|
||||
const bubble = document.createElement('div');
|
||||
bubble.className = "assiduite-bubble";
|
||||
bubble.classList.add(assiduite.etat.toLowerCase());
|
||||
|
||||
const idDiv = document.createElement("div");
|
||||
idDiv.className = "assiduite-id";
|
||||
@ -164,6 +157,11 @@
|
||||
stateDiv.textContent = `État: ${assiduite.etat.capitalize()}`;
|
||||
bubble.appendChild(stateDiv);
|
||||
|
||||
const motifDiv = document.createElement("div");
|
||||
stateDiv.className = "assiduite-why";
|
||||
stateDiv.textContent = `Motif: ${assiduite.desc?.capitalize()}`;
|
||||
bubble.appendChild(motifDiv);
|
||||
|
||||
const userIdDiv = document.createElement("div");
|
||||
userIdDiv.className = "assiduite-user_id";
|
||||
userIdDiv.textContent = `saisie le ${formatDateModal(
|
||||
@ -176,13 +174,7 @@
|
||||
}
|
||||
bubble.appendChild(userIdDiv);
|
||||
|
||||
bubble.style.left = `${event.clientX - bubble.offsetWidth / 2}px`;
|
||||
bubble.style.top = `${event.clientY + 20}px`;
|
||||
});
|
||||
el.addEventListener("mouseout", () => {
|
||||
const bubble = document.querySelector(".assiduite-bubble");
|
||||
bubble.classList.remove("is-active");
|
||||
});
|
||||
el.appendChild(bubble);
|
||||
}
|
||||
|
||||
function setMiniTick(timelineDate, dayStart, dayDuration) {
|
||||
@ -199,126 +191,3 @@
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.assiduite-bubble {
|
||||
position: fixed;
|
||||
display: none;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
border: 3px solid #ccc;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.assiduite-bubble.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.assiduite-bubble::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #f9f9f9 transparent;
|
||||
}
|
||||
|
||||
.assiduite-bubble::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent transparent #ccc transparent;
|
||||
}
|
||||
|
||||
.assiduite-id,
|
||||
.assiduite-period,
|
||||
.assiduite-state,
|
||||
.assiduite-user_id {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.assiduite-bubble.absent {
|
||||
border-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.assiduite-bubble.present {
|
||||
border-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.assiduite-bubble.retard {
|
||||
border-color: var(--color-retard) !important;
|
||||
}
|
||||
|
||||
.mini-timeline {
|
||||
height: 7px;
|
||||
border: 1px solid black;
|
||||
position: relative;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.mini-timeline.single {
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.mini-timeline-block {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#page-assiduite-content .mini-timeline-block {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mini_tick {
|
||||
position: absolute;
|
||||
text-align: start;
|
||||
top: -40px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
|
||||
}
|
||||
|
||||
.mini_tick::after {
|
||||
display: block;
|
||||
content: "|";
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.mini-timeline-block.creneau {
|
||||
outline: 3px solid var(--color-primary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mini-timeline-block.absent {
|
||||
background-color: var(--color-absent) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.present {
|
||||
background-color: var(--color-present) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.retard {
|
||||
background-color: var(--color-retard) !important;
|
||||
}
|
||||
|
||||
.mini-timeline-block.justified {
|
||||
background-image: var(--motif-justi);
|
||||
}
|
||||
|
||||
.mini-timeline-block.invalid_justified {
|
||||
background-image: var(--motif-justi-invalide);
|
||||
}
|
||||
</style>
|
||||
|
@ -1,156 +0,0 @@
|
||||
<div>
|
||||
{% if label != false%}
|
||||
<label for="moduleimpl_select">
|
||||
Module
|
||||
</label>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% if moduleid %}
|
||||
<select id="{{moduleid}}" class="dynaSelect">
|
||||
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
|
||||
</select>
|
||||
{% else %}
|
||||
<select id="moduleimpl_select" class="dynaSelect">
|
||||
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
|
||||
</select>
|
||||
{% endif %}
|
||||
|
||||
<div id="saved" style="display: none;">
|
||||
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
function getEtudFormSemestres() {
|
||||
let semestre = {};
|
||||
sync_get(getUrl() + `/api/etudiant/etudid/${etudid}/formsemestres`, (data) => {
|
||||
semestre = data;
|
||||
});
|
||||
return semestre;
|
||||
}
|
||||
|
||||
function filterFormSemestres(semestres, dateIso) {
|
||||
const date = new Date(Date.removeUTC(dateIso));
|
||||
semestres = semestres.filter((fm) => {
|
||||
return date.isBetween(new Date(Date.removeUTC(fm.date_debut_iso)), new Date(Date.removeUTC(fm.date_fin_iso)), '[]');
|
||||
})
|
||||
|
||||
return semestres;
|
||||
}
|
||||
|
||||
function getFormSemestreProgramme(fm_id) {
|
||||
let semestre = {};
|
||||
sync_get(getUrl() + `/api/formsemestre/${fm_id}/programme`, (data) => {
|
||||
semestre = data;
|
||||
});
|
||||
return semestre;
|
||||
}
|
||||
|
||||
function getModulesImplByFormsemestre(semestres) {
|
||||
const map = new Map();
|
||||
|
||||
semestres.forEach((fm) => {
|
||||
const array = [];
|
||||
|
||||
const fm_p = getFormSemestreProgramme(fm.formsemestre_id);
|
||||
["ressources", "saes", "modules"].forEach((r) => {
|
||||
if (r in fm_p) {
|
||||
fm_p[r].forEach((o) => {
|
||||
array.push(getModuleInfos(o))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
map.set(fm.titre_num, array)
|
||||
})
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function getModuleInfos(obj) {
|
||||
return {
|
||||
moduleimpl_id: obj.moduleimpl_id,
|
||||
titre: obj.module.titre,
|
||||
code: obj.module.code,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function populateSelect(sems, selected, query) {
|
||||
const select = document.querySelector(query);
|
||||
select.innerHTML = document.getElementById('saved').innerHTML
|
||||
sems.forEach((mods, label) => {
|
||||
const optGrp = document.createElement('optgroup');
|
||||
optGrp.label = label
|
||||
|
||||
mods.forEach((obj) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = obj.moduleimpl_id;
|
||||
opt.textContent = `${obj.code} ${obj.titre}`
|
||||
if (obj.moduleimpl_id == selected) {
|
||||
opt.setAttribute('selected', 'true');
|
||||
}
|
||||
|
||||
optGrp.appendChild(opt);
|
||||
})
|
||||
select.appendChild(optGrp);
|
||||
})
|
||||
if (selected === "autre") {
|
||||
select.querySelector('option[value="autre"]').setAttribute('selected', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
function updateSelect(moduleimpl_id, query = "#moduleimpl_select", dateIso = null) {
|
||||
let sem = getEtudFormSemestres()
|
||||
if (!dateIso) {
|
||||
dateIso = getDate().format("YYYY-MM-DD")
|
||||
}
|
||||
|
||||
sem = filterFormSemestres(sem, dateIso)
|
||||
const mod = getModulesImplByFormsemestre(sem)
|
||||
populateSelect(mod, moduleimpl_id, query);
|
||||
}
|
||||
|
||||
function updateSelectedSelect(moduleimpl_id, query = "#moduleimpl_select") {
|
||||
const mod_id = moduleimpl_id != null ? moduleimpl_id : ""
|
||||
document.querySelector(query).value = `${mod_id}`.toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
{% if moduleid %}
|
||||
const moduleimpl_dynamic_selector_id = "{{moduleid}}"
|
||||
{% else %}
|
||||
const moduleimpl_dynamic_selector_id = "moduleimpl_select"
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
document.getElementById(moduleimpl_dynamic_selector_id).addEventListener('change', (el) => {
|
||||
const assi = getCurrentAssiduite(etudid);
|
||||
if (assi) {
|
||||
editAssiduite(assi.assiduite_id, assi.etat, [assi]);
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const conflicts = getAssiduitesConflict(etudid);
|
||||
if (conflicts.length > 0) {
|
||||
updateSelectedSelect(getCurrentAssiduiteModuleImplId());
|
||||
}
|
||||
} catch { }
|
||||
}, { once: true });
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#moduleimpl_select {
|
||||
width: 125px;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,20 @@
|
||||
<select name="moduleimpl_select" id="moduleimpl_select">
|
||||
|
||||
{% with moduleimpl_id=moduleimpl_id %}
|
||||
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
|
||||
{% endwith %}
|
||||
|
||||
{% for cat, mods in choices.items() %}
|
||||
<optgroup label="{{cat}}">
|
||||
{% for mod in mods %}
|
||||
{% if mod.moduleimpl_id == moduleimpl_id %}
|
||||
<option value="{{mod.moduleimpl_id}}" selected> {{mod.name}} </option>
|
||||
{% else %}
|
||||
<option value="{{mod.moduleimpl_id}}"> {{mod.name}} </option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</optgroup>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</select>
|
@ -1,10 +1,3 @@
|
||||
{% if scu.is_assiduites_module_forced(request.args.get('formsemestre_id', None))%}
|
||||
<option value="" disabled> Saisir Module</option>
|
||||
{% else %}
|
||||
<option value=""> Non spécifié </option>
|
||||
{% endif %}
|
||||
{% if moduleimpl_id == "autre" %}
|
||||
<option value="autre" selected> Tout module </option>
|
||||
{% else %}
|
||||
<option value="autre"> Tout module </option>
|
||||
{% endif %}
|
||||
<option value="autre" {{ 'selected' if moduleimpl_id == 'autre' else '' }}>Autre module (pas dans la liste)</option>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div>
|
||||
<div class="sco_box_title">{{ titre }}</div>
|
||||
<div id="options-tableau">
|
||||
<div class="options-tableau">
|
||||
{% if afficher_options != false %}
|
||||
<input type="checkbox" id="show_pres" name="show_pres"
|
||||
onclick="updateTableau()" {{'checked' if options.show_pres else ''}}>
|
||||
@ -18,32 +18,132 @@
|
||||
<br>
|
||||
{% endif %}
|
||||
<label for="nb_ligne_page">Nombre de lignes par page :</label>
|
||||
<input type="number" name="nb_ligne_page" id="nb_ligne_page"
|
||||
size="4" step="25" min="10" value="{{options.nb_ligne_page}}"
|
||||
onchange="updateTableau()"
|
||||
>
|
||||
|
||||
<label for="n_page">Page n°</label>
|
||||
<select name="n_page" id="n_page">
|
||||
{% for n in range(1,total_pages+1) %}
|
||||
<option value="{{n}}" {{'selected' if n == options.page else ''}}>{{n}}</option>
|
||||
<select name="nb_ligne_page" id="nb_ligne_page" onchange="updateTableau()">
|
||||
{% for i in [25,50,100,1000] %}
|
||||
{% if i == options.nb_ligne_page %}
|
||||
<option selected value="{{i}}">{{i}}</option>
|
||||
{% else %}
|
||||
<option value="{{i}}">{{i}}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<br>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="div-tableau">
|
||||
<div class="options-tableau">
|
||||
<!--Pagination basée sur : https://app.uxcel.com/courses/ui-components-best-practices/best-practices-005 -->
|
||||
<!-- Mettre les flèches -->
|
||||
{% if total_pages > 1 %}
|
||||
<ul class="pagination">
|
||||
<li class="">
|
||||
<a onclick="navigateToPage({{options.page - 1}})"><</a>
|
||||
</li>
|
||||
<!-- Toujours afficher la première page -->
|
||||
<li class="{% if options.page == 1 %}active{% endif %}">
|
||||
<a onclick="navigateToPage({{1}})">1</a>
|
||||
</li>
|
||||
|
||||
<!-- Afficher les ellipses si la page courante est supérieure à 2 -->
|
||||
<!-- et qu'il y a plus d'une page entre le 1 et la page courante-1 -->
|
||||
{% if options.page > 2 and (options.page - 1) - 1 > 1 %}
|
||||
<li class="disabled"><span>...</span></li>
|
||||
{% endif %}
|
||||
|
||||
<!-- Afficher la page précédente, la page courante, et la page suivante -->
|
||||
{% for i in range(options.page - 1, options.page + 2) %}
|
||||
{% if i > 1 and i < total_pages %}
|
||||
<li class="{% if options.page == i %}active{% endif %}">
|
||||
<a onclick="navigateToPage({{i}})">{{ i }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Afficher les ellipses si la page courante est inférieure à l'avant-dernière page -->
|
||||
<!-- et qu'il y a plus d'une page entre le total_pages et la page courante+1 -->
|
||||
{% if options.page < total_pages - 1 and total_pages - (options.page + 1 ) > 1 %}
|
||||
<li class="disabled"><span>...</span></li>
|
||||
{% endif %}
|
||||
|
||||
<!-- Toujours afficher la dernière page -->
|
||||
<li class="{% if options.page == total_pages %}active{% endif %}">
|
||||
<a onclick="navigateToPage({{total_pages}})">{{ total_pages }}</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a onclick="navigateToPage({{options.page + 1}})">></a>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<!-- Afficher un seul bouton si il n'y a qu'une seule page -->
|
||||
<ul class="pagination">
|
||||
<li class="active"><a onclick="navigateToPage({{1}})">1</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{table.html() | safe}}
|
||||
<div class="options-tableau">
|
||||
<!--Pagination basée sur : https://app.uxcel.com/courses/ui-components-best-practices/best-practices-005 -->
|
||||
<!-- Mettre les flèches -->
|
||||
{% if total_pages > 1 %}
|
||||
<ul class="pagination">
|
||||
<li class="">
|
||||
<a onclick="navigateToPage({{options.page - 1}})"><</a>
|
||||
</li>
|
||||
<!-- Toujours afficher la première page -->
|
||||
<li class="{% if options.page == 1 %}active{% endif %}">
|
||||
<a onclick="navigateToPage({{1}})">1</a>
|
||||
</li>
|
||||
|
||||
<!-- Afficher les ellipses si la page courante est supérieure à 2 -->
|
||||
<!-- et qu'il y a plus d'une page entre le 1 et la page courante-1 -->
|
||||
{% if options.page > 2 and (options.page - 1) - 1 > 1 %}
|
||||
<li class="disabled"><span>...</span></li>
|
||||
{% endif %}
|
||||
|
||||
<!-- Afficher la page précédente, la page courante, et la page suivante -->
|
||||
{% for i in range(options.page - 1, options.page + 2) %}
|
||||
{% if i > 1 and i < total_pages %}
|
||||
<li class="{% if options.page == i %}active{% endif %}">
|
||||
<a onclick="navigateToPage({{i}})">{{ i }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<!-- Afficher les ellipses si la page courante est inférieure à l'avant-dernière page -->
|
||||
<!-- et qu'il y a plus d'une page entre le total_pages et la page courante+1 -->
|
||||
{% if options.page < total_pages - 1 and total_pages - (options.page + 1 ) > 1 %}
|
||||
<li class="disabled"><span>...</span></li>
|
||||
{% endif %}
|
||||
|
||||
<!-- Toujours afficher la dernière page -->
|
||||
<li class="{% if options.page == total_pages %}active{% endif %}">
|
||||
<a onclick="navigateToPage({{total_pages}})">{{ total_pages }}</a>
|
||||
</li>
|
||||
<li class="">
|
||||
<a onclick="navigateToPage({{options.page + 1}})">></a>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<!-- Afficher un seul bouton si il n'y a qu'une seule page -->
|
||||
<ul class="pagination">
|
||||
<li class="active"><a onclick="navigateToPage({{1}})">1</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{table.html() | safe}}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
function updateTableau() {
|
||||
const url = new URL(location.href);
|
||||
const form = document.getElementById("options-tableau");
|
||||
const formValues = form.querySelectorAll("*[name]");
|
||||
|
||||
const formValues = document.querySelectorAll(".options-tableau *[name]");
|
||||
formValues.forEach((el) => {
|
||||
if (el.type == "checkbox") {
|
||||
url.searchParams.set(el.name, el.checked)
|
||||
@ -58,10 +158,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
const total_pages = {{total_pages}};
|
||||
|
||||
function navigateToPage(pageNumber){
|
||||
if(pageNumber > total_pages || pageNumber < 1) return;
|
||||
const url = new URL(location.href);
|
||||
url.searchParams.set("n_page", pageNumber)
|
||||
|
||||
if (!url.href.endsWith("#options-tableau")) {
|
||||
location.href = url.href + "#options-tableau";
|
||||
} else {
|
||||
location.href = url.href;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.addEventListener('load', ()=>{
|
||||
const table_columns = [...document.querySelectorAll('.external-sort')];
|
||||
table_columns.forEach((e)=>e.addEventListener('click', ()=>{
|
||||
|
||||
// récupération de l'ordre "ascending" / "descending"
|
||||
let order = e.ariaSort;
|
||||
// récupération de la colonne à ordonner
|
||||
// il faut avoir une classe `external-type:<NOM COL>`
|
||||
let order_col = e.className.split(" ").find((e)=>e.indexOf("external-type:") != -1);
|
||||
|
||||
//Création de la nouvelle url avec le tri
|
||||
const url = new URL(location.href);
|
||||
url.searchParams.set("order", order);
|
||||
url.searchParams.set("order_col", order_col.split(":")[1]);
|
||||
|
||||
location.href = url.href
|
||||
|
||||
}));
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.small-font {
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
.div-tableau{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.pagination li{
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,13 +1,36 @@
|
||||
<h2>Détails {{type}}</h2>
|
||||
<h2>Détails {{type}} concernant <span class="etudinfo"
|
||||
id="etudid-{{objet.etudid}}">{{etud.html_link_fiche()|safe}}</span></h2>
|
||||
|
||||
<style>
|
||||
.info-row {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
.info-etat {
|
||||
font-size: 110%;
|
||||
font-weight: bold;
|
||||
background-color: rgb(253, 234, 210);
|
||||
border: 1px solid grey;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
}
|
||||
.info-saisie {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="informations">
|
||||
<div class="info-row">
|
||||
<span class="info-label">Étudiant{{etud.e}} concerné{{etud.e}}:</span> <span class="etudinfo"
|
||||
id="etudid-{{objet.etudid}}">{{etud.html_link_fiche()|safe}}</span>
|
||||
|
||||
<div class="info-saisie">
|
||||
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Période :</span> {{objet.date_debut}} au {{objet.date_fin}}
|
||||
<span class="info-label">Période :</span> du <b>{{objet.date_debut}}</b> au <b>{{objet.date_fin}}</b>
|
||||
</div>
|
||||
|
||||
{% if type == "Assiduité" %}
|
||||
@ -23,27 +46,27 @@
|
||||
{% else %}
|
||||
<span class="info-label">État de l'assiduité :</span>
|
||||
{% endif %}
|
||||
<b>{{objet.etat}}</b>
|
||||
<span class="info-etat">{{objet.etat}}</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="info-row">
|
||||
{% if type == "Justificatif" %}
|
||||
<div class="info-label">Raison:</div>
|
||||
{% if objet.raison != None %}
|
||||
<div class="text">{{objet.raison}}</div>
|
||||
<span class="info-label">Raison:</span>
|
||||
{% if can_view_justif_detail %}
|
||||
<span class="text">{{objet.raison or " "}}</span>
|
||||
{% else %}
|
||||
<div class="text">/div>
|
||||
<span class="text unauthorized">(cachée)</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="info-label">Description:</div>
|
||||
<span class="info-label">Description:</span>
|
||||
{% if objet.description != None %}
|
||||
<div class="text">{{objet.description}}</div>
|
||||
<span class="text">{{objet.description}}</span>
|
||||
{% else %}
|
||||
<div class="text"></div>
|
||||
<span class="text"></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{# Affichage des justificatifs si assiduité justifiée #}
|
||||
@ -54,7 +77,8 @@
|
||||
<span class="text">Oui</span>
|
||||
<div>
|
||||
{% for justi in objet.justification.justificatifs %}
|
||||
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}"
|
||||
<a href="{{url_for('assiduites.tableau_assiduite_actions',
|
||||
type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}"
|
||||
target="_blank" rel="noopener noreferrer">Justificatif du {{justi.date_debut}} au {{justi.date_fin}}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -69,13 +93,15 @@
|
||||
<div class="info-row">
|
||||
<span class="info-label">Assiduités concernées: </span>
|
||||
{% if objet.justification.assiduites %}
|
||||
<div>
|
||||
<ul>
|
||||
{% for assi in objet.justification.assiduites %}
|
||||
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)}}"
|
||||
target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au
|
||||
<li><a href="{{url_for('assiduites.tableau_assiduite_actions',
|
||||
type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)
|
||||
}}" target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au
|
||||
{{assi.date_fin}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</ul>
|
||||
{% else %}
|
||||
<span class="text">Aucune</span>
|
||||
{% endif %}
|
||||
@ -92,7 +118,8 @@
|
||||
{% for filename in objet.justification.fichiers.filenames %}
|
||||
<li>
|
||||
<a
|
||||
href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
|
||||
href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,
|
||||
filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if not objet.justification.fichiers.filenames %}
|
||||
@ -103,8 +130,11 @@
|
||||
<span class="text">Aucun</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="info-row">
|
||||
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
|
||||
{% if current_user.has_permission(sco.Permission.AbsChange) %}
|
||||
<div><a class="stdlink" href="{{
|
||||
url_for('assiduites.edit_justificatif_etud', scodoc_dept=g.scodoc_dept, justif_id=obj_id)
|
||||
}}">modifier ce justificatif</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -1,5 +1,7 @@
|
||||
<h2>Modifier {{objet_name}} de {{ etud.html_link_fiche() | safe }}</h2>
|
||||
|
||||
{# XXX cette page ne semble plus utile ! remplacée par edit_justificatif_etud #}
|
||||
|
||||
<div>
|
||||
Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_debut}} au {{objet.date_fin}}
|
||||
</div>
|
||||
@ -39,8 +41,12 @@ Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_de
|
||||
<option value="modifie">Modifié</option>
|
||||
</select>
|
||||
|
||||
{% if current_user.has_permission(sco.Permission.AbsJustifView) %}
|
||||
<legend for="raison">Raison</legend>
|
||||
<textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea>
|
||||
{% else %}
|
||||
<div class="unauthorized">(raison non visible ni modifiable)</div>
|
||||
{% endif %}
|
||||
|
||||
<legend>Fichiers</legend>
|
||||
|
||||
|
@ -1,465 +0,0 @@
|
||||
<table id="assiduiteTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div>
|
||||
<span>Début</span>
|
||||
<a class="icon order" onclick="order('date_debut', assiduiteCallBack, this)"></a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div>
|
||||
<span>Fin</span>
|
||||
<a class="icon order" onclick="order('date_fin', assiduiteCallBack, this)"></a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div>
|
||||
<span>État</span>
|
||||
<a class="icon order" onclick="order('etat', assiduiteCallBack, this)"></a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div>
|
||||
<span>Module</span>
|
||||
<a class="icon order" onclick="order('moduleimpl_id', assiduiteCallBack, this)"></a>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div>
|
||||
<span>Justifiée</span>
|
||||
<a class="icon order" onclick="order('est_just', assiduiteCallBack, this)"></a>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tableBodyAssiduites">
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="paginationContainerAssiduites" class="pagination-container">
|
||||
</div>
|
||||
|
||||
<div style="display: none;" id="cache-module">
|
||||
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const paginationContainerAssiduites = document.getElementById("paginationContainerAssiduites");
|
||||
let currentPageAssiduites = 1;
|
||||
let orderAssiduites = true;
|
||||
let filterAssiduites = {
|
||||
columns: [
|
||||
"entry_date", "date_debut", "date_fin", "etat", "moduleimpl_id", "est_just"
|
||||
],
|
||||
filters: {}
|
||||
}
|
||||
const tableBodyAssiduites = document.getElementById("tableBodyAssiduites");
|
||||
|
||||
function assiduiteCallBack(assi) {
|
||||
assi = filterArray(assi, filterAssiduites.filters)
|
||||
renderTableAssiduites(currentPageAssiduites, assi);
|
||||
renderPaginationButtons(assi);
|
||||
|
||||
try { stats() } catch (_) { }
|
||||
}
|
||||
|
||||
|
||||
|
||||
function renderTableAssiduites(page, assiduités) {
|
||||
|
||||
generateTableHead(filterAssiduites.columns, true)
|
||||
|
||||
tableBodyAssiduites.innerHTML = "";
|
||||
const start = (page - 1) * itemsPerPage;
|
||||
const end = start + itemsPerPage;
|
||||
|
||||
assiduités.slice(start, end).forEach((assiduite) => {
|
||||
const row = document.createElement("tr");
|
||||
row.setAttribute('type', "assiduite");
|
||||
row.setAttribute('obj_id', assiduite.assiduite_id);
|
||||
|
||||
const etat = assiduite.etat.toLowerCase();
|
||||
row.classList.add(`l-${etat}`);
|
||||
filterAssiduites.columns.forEach((k) => {
|
||||
const td = document.createElement('td');
|
||||
if (k.indexOf('date') != -1) {
|
||||
td.textContent = new Date(Date.removeUTC(assiduite[k])).format(`DD/MM/Y HH:mm`)
|
||||
} else if (k.indexOf("module") != -1) {
|
||||
td.textContent = getModuleImpl(assiduite);
|
||||
} else if (k.indexOf('est_just') != -1) {
|
||||
td.textContent = assiduite[k] ? "Oui" : "Non"
|
||||
if (assiduite[k]) row.classList.add("est_just")
|
||||
} else if (k.indexOf('etudid') != -1) {
|
||||
const e = getEtudiant(assiduite.etudid);
|
||||
|
||||
td.innerHTML = `<a class="etudinfo" id="line-${assiduite.etudid}" href="bilan_etud?etudid=${assiduite.etudid}">${e.prenom.capitalize()} ${e.nom.toUpperCase()}</a>`;
|
||||
} else {
|
||||
td.textContent = assiduite[k].capitalize()
|
||||
}
|
||||
|
||||
row.appendChild(td)
|
||||
})
|
||||
|
||||
|
||||
row.addEventListener("contextmenu", openContext);
|
||||
|
||||
tableBodyAssiduites.appendChild(row);
|
||||
});
|
||||
updateActivePaginationButton();
|
||||
}
|
||||
|
||||
function detailAssiduites(assiduite_id) {
|
||||
const path = getUrl() + `/api/assiduite/${assiduite_id}`;
|
||||
async_get(
|
||||
path,
|
||||
(data) => {
|
||||
const user = getUser(data);
|
||||
const module = getModuleImpl(data);
|
||||
|
||||
const date_debut = new Date(Date.removeUTC(data.date_debut)).format("DD/MM/YYYY HH:mm");
|
||||
const date_fin = new Date(Date.removeUTC(data.date_fin)).format("DD/MM/YYYY HH:mm");
|
||||
const entry_date = new Date(Date.removeUTC(data.entry_date)).format("DD/MM/YYYY HH:mm");
|
||||
|
||||
const etat = data.etat.capitalize();
|
||||
const desc = data.desc == null ? "" : data.desc;
|
||||
const id = data.assiduite_id;
|
||||
const est_just = data.est_just ? "Oui" : "Non";
|
||||
|
||||
const html = `
|
||||
<div class="obj-detail">
|
||||
<div class="obj-dates">
|
||||
<div id="date_debut" class="obj-part">
|
||||
<span class="obj-title">Date de début</span>
|
||||
<span class="obj-content">${date_debut}</span>
|
||||
</div>
|
||||
<div id="date_fin" class="obj-part">
|
||||
<span class="obj-title">Date de fin</span>
|
||||
<span class="obj-content">${date_fin}</span>
|
||||
</div>
|
||||
<div id="entry_date" class="obj-part">
|
||||
<span class="obj-title">Date de saisie</span>
|
||||
<span class="obj-content">${entry_date}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="obj-mod">
|
||||
<div id="module" class="obj-part">
|
||||
<span class="obj-title">Module</span>
|
||||
<span class="obj-content">${module}</span>
|
||||
</div>
|
||||
<div id="etat" class="obj-part">
|
||||
<span class="obj-title">Etat</span>
|
||||
<span class="obj-content">${etat}</span>
|
||||
</div>
|
||||
<div id="user" class="obj-part">
|
||||
<span class="obj-title">par</span>
|
||||
<span class="obj-content">${user}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="obj-rest">
|
||||
<div id="est_just" class="obj-part">
|
||||
<span class="obj-title">Justifié</span>
|
||||
<span class="obj-content">${est_just}</span>
|
||||
</div>
|
||||
<div id="desc" class="obj-part">
|
||||
<span class="obj-title">Description</span>
|
||||
<p class="obj-content">${desc}</p>
|
||||
</div>
|
||||
<div id="id" class="obj-part" data-assiduite-id="${id}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = html;
|
||||
|
||||
openAlertModal("Détails", el.firstElementChild, null, "var(--color-information)")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function editionAssiduites(assiduite_id) {
|
||||
const path = getUrl() + `/api/assiduite/${assiduite_id}`;
|
||||
async_get(
|
||||
path,
|
||||
(data) => {
|
||||
let module = data.moduleimpl_id;
|
||||
if (
|
||||
module == null && data.hasOwnProperty("external_data") &&
|
||||
data.external_data != null &&
|
||||
data.external_data.hasOwnProperty('module')
|
||||
) {
|
||||
module = data.external_data.module.toLowerCase();
|
||||
}
|
||||
|
||||
const etat = data.etat;
|
||||
let desc = data.desc == null ? "" : data.desc;
|
||||
const html = `
|
||||
<div class="assi-edit">
|
||||
<div class="assi-edit-part">
|
||||
<legend>État de l'assiduité</legend>
|
||||
<select name="etat" id="etat">
|
||||
<option value="present">Présent</option>
|
||||
<option value="retard">En Retard</option>
|
||||
<option value="absent">Absent</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="assi-edit-part">
|
||||
<legend>Module</legend>
|
||||
<select name="module" id="module">
|
||||
</select>
|
||||
</div>
|
||||
<div class="assi-edit-part">
|
||||
<legend>Description</legend>
|
||||
<textarea name="desc" id="desc" cols="50" rows="10" maxlength="500"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`
|
||||
|
||||
const el = document.createElement('div')
|
||||
el.innerHTML = html;
|
||||
const assiEdit = el.firstElementChild;
|
||||
|
||||
assiEdit.querySelector('#etat').value = etat.toLowerCase();
|
||||
assiEdit.querySelector('#desc').value = desc != null ? desc : "";
|
||||
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');
|
||||
const etat = prompt.querySelector('#etat').value;
|
||||
const desc = prompt.querySelector('#desc').value;
|
||||
let module = prompt.querySelector('#moduleimpl_select').value;
|
||||
let edit = {
|
||||
"etat": etat,
|
||||
"desc": desc,
|
||||
"external_data": data.external_data
|
||||
}
|
||||
|
||||
edit = setModuleImplId(edit, module);
|
||||
|
||||
fullEditAssiduites(data.assiduite_id, edit, () => {
|
||||
loadAll();
|
||||
})
|
||||
|
||||
|
||||
}, () => { }, "var(--color-information)");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function fullEditAssiduites(assiduite_id, obj, call = () => { }) {
|
||||
const path = getUrl() + `/api/assiduite/${assiduite_id}/edit`;
|
||||
async_post(
|
||||
path,
|
||||
obj,
|
||||
call,
|
||||
(data, status) => {
|
||||
//error
|
||||
console.error(data, status);
|
||||
errorAlert();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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 Date(Date.removeUTC(time))
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
|
||||
}, () => { }, "var(--color-primary)");
|
||||
|
||||
}
|
||||
|
||||
function downloadAssi() {
|
||||
getAssi((d) => { toCSV(d, filterAssiduites) })
|
||||
}
|
||||
|
||||
function getAssi(action) {
|
||||
try { getAllAssiduitesFromEtud(etudid, action, true, true, assi_limit_annee) } catch (_) { }
|
||||
}
|
||||
|
||||
</script>
|
@ -89,8 +89,7 @@
|
||||
|
||||
}
|
||||
|
||||
function timelineMainEvent(event, callback) {
|
||||
const func_call = callback ? callback : () => { };
|
||||
function timelineMainEvent(event) {
|
||||
|
||||
const startX = (event.clientX || event.changedTouches[0].clientX);
|
||||
|
||||
@ -152,7 +151,6 @@
|
||||
updatePeriodTimeLabel();
|
||||
};
|
||||
const mouseUp = () => {
|
||||
generateAllEtudRow();
|
||||
snapHandlesToQuarters();
|
||||
timelineContainer.removeEventListener("mousemove", onMouseMove);
|
||||
func_call();
|
||||
@ -172,9 +170,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
let func_call = () => { };
|
||||
|
||||
function setupTimeLine(callback) {
|
||||
timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e, callback) });
|
||||
timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e, callback) });
|
||||
func_call = callback;
|
||||
timelineContainer.addEventListener("mousedown", (e) => { timelineMainEvent(e) });
|
||||
timelineContainer.addEventListener("touchstart", (e) => { timelineMainEvent(e) });
|
||||
}
|
||||
|
||||
function adjustPeriodPosition(newLeft, newWidth) {
|
||||
@ -230,8 +231,8 @@
|
||||
periodTimeLine.style.width = `${widthPercentage}%`;
|
||||
|
||||
snapHandlesToQuarters();
|
||||
generateAllEtudRow();
|
||||
updatePeriodTimeLabel()
|
||||
func_call();
|
||||
}
|
||||
|
||||
function snapHandlesToQuarters() {
|
||||
@ -270,7 +271,6 @@
|
||||
if (heure_deb != '' && heure_fin != '') {
|
||||
heure_deb = fromTime(heure_deb);
|
||||
heure_fin = fromTime(heure_fin);
|
||||
console.warn(heure_deb, heure_fin)
|
||||
setPeriodValues(heure_deb, heure_fin)
|
||||
}
|
||||
{% endif %}
|
||||
|
@ -1,3 +1,4 @@
|
||||
{# Base de toutes les pages ScoDoc #}
|
||||
{% block doc -%}
|
||||
<!DOCTYPE html>
|
||||
<html{% block html_attribs %}{% endblock html_attribs %}>
|
||||
@ -25,7 +26,10 @@
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{scu.STATIC_DIR}}/jQuery/jquery.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/bootstrap/js/bootstrap.js"></script>
|
||||
<script src="{{scu.STATIC_DIR}}/libjs/bootstrap/js/bootstrap.min.js"></script>
|
||||
<script>
|
||||
const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}";
|
||||
</script>
|
||||
{%- endblock scripts %}
|
||||
{%- endblock body %}
|
||||
</body>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user