diff --git a/app/api/users.py b/app/api/users.py index 0a9f73978..761ad0c51 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -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//edt") +@api_web_bp.route("/user//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 diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index d4b8a1aa8..b8cdf9ac9 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -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"""promotion""" abs_icon = f""" 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) diff --git a/app/static/css/edt.css b/app/static/css/edt.css index cfbf9ef73..4644be999 100644 --- a/app/static/css/edt.css +++ b/app/static/css/edt.css @@ -144,3 +144,7 @@ span.ens-non-reconnu { .btn:active { outline: none; } + +.raw-event { + display: none; +} \ No newline at end of file diff --git a/app/templates/formsemestre/edt.j2 b/app/templates/formsemestre/edt.j2 index 7a72890af..b0a77f738 100644 --- a/app/templates/formsemestre/edt.j2 +++ b/app/templates/formsemestre/edt.j2 @@ -126,7 +126,7 @@ document.addEventListener('DOMContentLoaded', function() { time: function(event) { const date_start = new Date(event.start); const start = hm_formatter.format(date_start); - return `${start} ${event.title}`; + return `${start} ${event.title}
${event.raw}
`; }, }, timezone: { @@ -249,10 +249,8 @@ document.addEventListener('DOMContentLoaded', function() { if ((iso_date_start > "{{ formsemestre.date_fin.isoformat() }}") || (iso_date_end < "{{ formsemestre.date_debut.isoformat() }}")) { cal_warning.style.display = 'inline-block'; - console.log("OUTSIDE"); } else { cal_warning.style.display = 'none'; - console.log("INSIDE"); } } // View menu diff --git a/app/views/assiduites.py b/app/views/assiduites.py index 9b064d86a..33f9277be 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -947,12 +947,13 @@ def choix_date() -> str: @permission_required(Permission.AbsChange) def signal_assiduites_group(): """ - signal_assiduites_group Saisie des assiduités des groupes pour le jour donnée + signal_assiduites_group Saisie des assiduités des groupes pour le jour donné Returns: str: l'html généré """ # Récupération des paramètres de l'url + # formsemestre_id est optionnel si modimpl est indiqué formsemestre_id: int = request.args.get("formsemestre_id", -1) moduleimpl_id: int = request.args.get("moduleimpl_id") date: str = request.args.get("jour", datetime.date.today().isoformat()) @@ -972,13 +973,20 @@ def signal_assiduites_group(): moduleimpl_id = int(moduleimpl_id) except (TypeError, ValueError): moduleimpl_id = None - + if moduleimpl_id >= 0 and moduleimpl_id is not None: + modimpl = ModuleImpl.get_modimpl(moduleimpl_id) + else: + modimpl = None # Vérification du formsemestre_id try: formsemestre_id = int(formsemestre_id) except (TypeError, ValueError): formsemestre_id = None + if (formsemestre_id < 0 or formsemestre_id is None) and modimpl: + # si le module est spécifié mais pas le semestre: + formsemestre_id = modimpl.formsemestre_id + # Gestion des groupes groups_infos = sco_groups_view.DisplayedGroupsInfos( group_ids, diff --git a/tools/edt/edt_ens.py b/tools/edt/edt_ens.py index 8206cc030..63d0acf3a 100644 --- a/tools/edt/edt_ens.py +++ b/tools/edt/edt_ens.py @@ -48,7 +48,7 @@ from app.scodoc.sco_exceptions import ScoValueError import sco_version -def _calendar_factory(): +def _calendar_factory() -> icalendar.Calendar: "Create a new calendar" cal = icalendar.Calendar() cal.add(