Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
6 changed files with 159 additions and 47 deletions
Showing only changes of commit 9104a8986e - Show all commits

View File

@ -7,7 +7,6 @@
""" """
ScoDoc 9 API : accès aux utilisateurs ScoDoc 9 API : accès aux utilisateurs
""" """
import datetime
from flask import g, request from flask import g, request
from flask_json import as_json 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 import db, log
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR 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 User, Role, UserRole
from app.auth.models import is_valid_password from app.auth.models import is_valid_password
from app.decorators import scodoc, permission_required 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_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import json_error
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -441,3 +441,63 @@ def role_delete(role_name: str):
db.session.delete(role) db.session.delete(role)
db.session.commit() db.session.commit()
return {"OK": True} 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

View File

@ -177,12 +177,26 @@ def formsemestre_edt_dict(
TODO: spécifier intervalle de dates start et end TODO: spécifier intervalle de dates start et end
""" """
t0 = time.time() t0 = time.time()
group_ids_set = set(group_ids) if group_ids else set()
try: try:
events_scodoc, _ = load_and_convert_ics(formsemestre) events_scodoc, _ = load_and_convert_ics(formsemestre)
except ScoValueError as exc: except ScoValueError as exc:
return exc.args[0] 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" promo_icon = f"""<img height="18px" src="{scu.STATIC_DIR}/icons/promo.svg"
title="promotion complète" alt="promotion"/>""" title="promotion complète" alt="promotion"/>"""
abs_icon = f"""<img height="28px" src="{scu.STATIC_DIR}/icons/absences.svg" abs_icon = f"""<img height="28px" src="{scu.STATIC_DIR}/icons/absences.svg"
@ -211,7 +225,6 @@ def formsemestre_edt_dict(
url_for( url_for(
"assiduites.signal_assiduites_group", "assiduites.signal_assiduites_group",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
group_ids=group.id, group_ids=group.id,
heure_deb=event["heure_deb"], heure_deb=event["heure_deb"],
heure_fin=event["heure_fin"], heure_fin=event["heure_fin"],
@ -271,17 +284,17 @@ def formsemestre_edt_dict(
"start": event["start"], "start": event["start"],
"end": event["end"], "end": event["end"],
"backgroundColor": event["group_bg_color"], "backgroundColor": event["group_bg_color"],
"raw": event["raw"],
# Infos brutes pour usage API éventuel # Infos brutes pour usage API éventuel
"edt_ens_ids": event["edt_ens_ids"], "edt_ens_ids": event["edt_ens_ids"],
"ens_user_names": ens_user_names, "ens_user_names": ens_user_names,
"group_id": group.id if group else None, "group_id": group.id if group else None,
"group_edt_id": event["edt_group"], "group_edt_id": event["edt_group"],
"moduleimpl_id": modimpl.id if modimpl else None, "moduleimpl_id": modimpl.id if modimpl else None,
"UID": event["UID"], # icalendar event UID
} }
events_cal.append(d) events_cal.append(d)
log(
f"formsemestre_edt_dict: loaded edt for {formsemestre} in {(time.time()-t0):g}s"
)
return events_cal 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]]: 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 Renvoie une liste d'évènements, et la liste des identifiants de groupes
trouvés (utilisée pour l'aide). 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é - False si extraction regexp non configuré
- "tous" (promo) si pas de correspondance trouvée. - "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 # --- Paramètres d'extraction
edt_ics_title_field = ScoDocSiteConfig.get("edt_ics_title_field") edt_ics_title_field = ScoDocSiteConfig.get("edt_ics_title_field")
edt_ics_title_regexp = ScoDocSiteConfig.get("edt_ics_title_regexp") 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_field = ScoDocSiteConfig.get("edt_ics_uid_field")
edt_ics_uid_pattern = get_ics_uid_pattern() edt_ics_uid_pattern = get_ics_uid_pattern()
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants # --- Groupes
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
group_colors = { group_colors = {
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1] group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
for i, group_name in enumerate(edt2group) for i, group_name in enumerate(edt2group)
} }
edt_groups_ids = set() # les ids de groupes normalisés tels que dans l'ics 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) edt2user: dict[str, User | None] = {} # construit au fur et à mesure (cache)
# --- # ---
events = [e for e in calendar.walk() if e.name == "VEVENT"] 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 remplacé par le nom du module scodoc quand il est trouvé
title = title_edt 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 # --- ModuleImpl
if edt_ics_mod_pattern: if edt_ics_mod_pattern:
edt_module = extract_event_edt_id( edt_module = extract_event_edt_id(
@ -405,6 +417,34 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
else: else:
modimpl = False modimpl = False
edt_module = "" 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 # --- Enseignants
users: list[User] = [] users: list[User] = []
if edt_ics_uid_pattern: 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(), "jour": event.decoded("dtstart").date().isoformat(),
"start": event.decoded("dtstart").isoformat(), "start": event.decoded("dtstart").isoformat(),
"end": event.decoded("dtend").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) return events_sco, sorted(edt_groups_ids)

View File

@ -144,3 +144,7 @@ span.ens-non-reconnu {
.btn:active { .btn:active {
outline: none; outline: none;
} }
.raw-event {
display: none;
}

View File

@ -126,7 +126,7 @@ document.addEventListener('DOMContentLoaded', function() {
time: function(event) { time: function(event) {
const date_start = new Date(event.start); const date_start = new Date(event.start);
const start = hm_formatter.format(date_start); const start = hm_formatter.format(date_start);
return `<strong>${start}</strong> <span>${event.title}</span>`; return `<strong>${start}</strong> <span>${event.title}</span> <div class="raw-event">${event.raw}</div>`;
}, },
}, },
timezone: { timezone: {
@ -249,10 +249,8 @@ document.addEventListener('DOMContentLoaded', function() {
if ((iso_date_start > "{{ formsemestre.date_fin.isoformat() }}") if ((iso_date_start > "{{ formsemestre.date_fin.isoformat() }}")
|| (iso_date_end < "{{ formsemestre.date_debut.isoformat() }}")) { || (iso_date_end < "{{ formsemestre.date_debut.isoformat() }}")) {
cal_warning.style.display = 'inline-block'; cal_warning.style.display = 'inline-block';
console.log("OUTSIDE");
} else { } else {
cal_warning.style.display = 'none'; cal_warning.style.display = 'none';
console.log("INSIDE");
} }
} }
// View menu // View menu

View File

@ -947,12 +947,13 @@ def choix_date() -> str:
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def signal_assiduites_group(): 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: Returns:
str: l'html généré str: l'html généré
""" """
# Récupération des paramètres de l'url # 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) formsemestre_id: int = request.args.get("formsemestre_id", -1)
moduleimpl_id: int = request.args.get("moduleimpl_id") moduleimpl_id: int = request.args.get("moduleimpl_id")
date: str = request.args.get("jour", datetime.date.today().isoformat()) date: str = request.args.get("jour", datetime.date.today().isoformat())
@ -972,13 +973,20 @@ def signal_assiduites_group():
moduleimpl_id = int(moduleimpl_id) moduleimpl_id = int(moduleimpl_id)
except (TypeError, ValueError): except (TypeError, ValueError):
moduleimpl_id = None 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 # Vérification du formsemestre_id
try: try:
formsemestre_id = int(formsemestre_id) formsemestre_id = int(formsemestre_id)
except (TypeError, ValueError): except (TypeError, ValueError):
formsemestre_id = None 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 # Gestion des groupes
groups_infos = sco_groups_view.DisplayedGroupsInfos( groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, group_ids,

View File

@ -48,7 +48,7 @@ from app.scodoc.sco_exceptions import ScoValueError
import sco_version import sco_version
def _calendar_factory(): def _calendar_factory() -> icalendar.Calendar:
"Create a new calendar" "Create a new calendar"
cal = icalendar.Calendar() cal = icalendar.Calendar()
cal.add( cal.add(