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
"""
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

View File

@ -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)

View File

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

View File

@ -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 `<strong>${start}</strong> <span>${event.title}</span>`;
return `<strong>${start}</strong> <span>${event.title}</span> <div class="raw-event">${event.raw}</div>`;
},
},
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

View File

@ -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,

View File

@ -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(