forked from ScoDoc/ScoDoc
WIP: Affichage de l'emploi du temps du semestre.
This commit is contained in:
parent
e06292de99
commit
d1bc546d7b
@ -33,6 +33,7 @@ from app.models import (
|
|||||||
)
|
)
|
||||||
from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX
|
from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX
|
||||||
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
|
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
|
||||||
|
from app.scodoc import sco_edt_cal
|
||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc.sco_permissions import Permission
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc.sco_utils import ModuleType
|
from app.scodoc.sco_utils import ModuleType
|
||||||
@ -555,3 +556,18 @@ def save_groups_auto_assignment(formsemestre_id: int):
|
|||||||
formsemestre.groups_auto_assignment_data = request.data
|
formsemestre.groups_auto_assignment_data = request.data
|
||||||
db.session.add(formsemestre)
|
db.session.add(formsemestre)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/formsemestre/<int:formsemestre_id>/edt")
|
||||||
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edt")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
@as_json
|
||||||
|
def formsemestre_edt(formsemestre_id: int):
|
||||||
|
"""l'emploi du temps du semestre"""
|
||||||
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
||||||
|
if g.scodoc_dept:
|
||||||
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
||||||
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
||||||
|
return sco_edt_cal.formsemestre_edt_dict(formsemestre)
|
||||||
|
@ -259,6 +259,22 @@ class FormSemestre(db.Model):
|
|||||||
d["session_id"] = self.session_id()
|
d["session_id"] = self.session_id()
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def get_default_group(self) -> GroupDescr:
|
||||||
|
"""default ('tous') group.
|
||||||
|
Le groupe par défaut contient tous les étudiants et existe toujours.
|
||||||
|
C'est l'unique groupe de la partition sans nom.
|
||||||
|
"""
|
||||||
|
default_partition = self.partitions.filter_by(partition_name=None).first()
|
||||||
|
if default_partition:
|
||||||
|
return default_partition.groups.first()
|
||||||
|
raise ScoValueError("Le semestre n'a pas de groupe par défaut")
|
||||||
|
|
||||||
|
def get_edt_id(self) -> str:
|
||||||
|
"l'id pour l'emploi du temps: à défaut, le 1er code étape Apogée"
|
||||||
|
return (
|
||||||
|
self.edt_id or "" or (self.etapes[0].etape_apo if len(self.etapes) else "")
|
||||||
|
)
|
||||||
|
|
||||||
def get_infos_dict(self) -> dict:
|
def get_infos_dict(self) -> dict:
|
||||||
"""Un dict avec des informations sur le semestre
|
"""Un dict avec des informations sur le semestre
|
||||||
pour les bulletins et autres templates
|
pour les bulletins et autres templates
|
||||||
|
@ -231,8 +231,12 @@ class GroupDescr(db.Model):
|
|||||||
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
|
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_nom_with_part(self) -> str:
|
def get_nom_with_part(self, default="-") -> str:
|
||||||
"Nom avec partition: 'TD A'"
|
"""Nom avec partition: 'TD A'
|
||||||
|
Si groupe par défaut (tous), utilise default ou "-"
|
||||||
|
"""
|
||||||
|
if self.partition.partition_name is None:
|
||||||
|
return default
|
||||||
return f"{self.partition.partition_name or ''} {self.group_name or '-'}"
|
return f"{self.partition.partition_name or ''} {self.group_name or '-'}"
|
||||||
|
|
||||||
def to_dict(self, with_partition=True) -> dict:
|
def to_dict(self, with_partition=True) -> dict:
|
||||||
@ -243,10 +247,14 @@ class GroupDescr(db.Model):
|
|||||||
d["partition"] = self.partition.to_dict(with_groups=False)
|
d["partition"] = self.partition.to_dict(with_groups=False)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def get_edt_id(self) -> str:
|
||||||
|
"l'id pour l'emploi du temps: à défaut, le nom scodoc du groupe"
|
||||||
|
return self.edt_id or self.group_name or ""
|
||||||
|
|
||||||
def get_nb_inscrits(self) -> int:
|
def get_nb_inscrits(self) -> int:
|
||||||
"""Nombre inscrits à ce group et au formsemestre.
|
"""Nombre inscrits à ce group et au formsemestre.
|
||||||
C'est nécessaire car lors d'une désinscription, on conserve l'appartenance
|
C'est nécessaire car lors d'une désinscription, on conserve l'appartenance
|
||||||
aux groupes pour facilier une éventuelle ré-inscription.
|
aux groupes pour faciliter une éventuelle ré-inscription.
|
||||||
"""
|
"""
|
||||||
from app.models.formsemestre import FormSemestreInscription
|
from app.models.formsemestre import FormSemestreInscription
|
||||||
|
|
||||||
|
@ -45,6 +45,12 @@ class ModuleImpl(db.Model):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||||
|
|
||||||
|
def get_edt_id(self) -> str:
|
||||||
|
"l'id pour l'emploi du temps: actuellement celui du module"
|
||||||
|
return (
|
||||||
|
self.module.get_edt_id()
|
||||||
|
) # TODO à décliner pour autoriser des codes différents ?
|
||||||
|
|
||||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||||
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||||
|
@ -285,6 +285,14 @@ class Module(db.Model):
|
|||||||
return {x.strip() for x in self.code_apogee.split(",") if x}
|
return {x.strip() for x in self.code_apogee.split(",") if x}
|
||||||
return set()
|
return set()
|
||||||
|
|
||||||
|
def get_edt_id(self) -> str:
|
||||||
|
"l'id pour l'emploi du temps: à défaut, le 1er code Apogée"
|
||||||
|
return (
|
||||||
|
self.edt_id
|
||||||
|
or (self.code_apogee.split(",")[0] if self.code_apogee else "")
|
||||||
|
or ""
|
||||||
|
)
|
||||||
|
|
||||||
def get_parcours(self) -> list[ApcParcours]:
|
def get_parcours(self) -> list[ApcParcours]:
|
||||||
"""Les parcours utilisant ce module.
|
"""Les parcours utilisant ce module.
|
||||||
Si tous les parcours, liste vide (!).
|
Si tous les parcours, liste vide (!).
|
||||||
|
@ -29,175 +29,191 @@
|
|||||||
|
|
||||||
XXX usage uniquement experimental pour tests implémentations
|
XXX usage uniquement experimental pour tests implémentations
|
||||||
|
|
||||||
XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU).
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
import icalendar
|
import icalendar
|
||||||
|
|
||||||
import urllib
|
from flask import flash
|
||||||
|
|
||||||
import app.scodoc.sco_utils as scu
|
|
||||||
from app import log
|
from app import log
|
||||||
from app.scodoc import sco_formsemestre
|
from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig
|
||||||
from app.scodoc import sco_groups
|
import app.scodoc.sco_utils as scu
|
||||||
from app.scodoc import sco_preferences
|
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_get_ics_url(sem):
|
def formsemestre_load_calendar(
|
||||||
"""
|
formsemestre: FormSemestre,
|
||||||
edt_sem_ics_url est un template
|
) -> icalendar.cal.Calendar | None:
|
||||||
utilisé avec .format(sem=sem)
|
"""Load ics data, return calendar or None if not configured or not available"""
|
||||||
Par exemple:
|
edt_id = formsemestre.get_edt_id()
|
||||||
https://example.fr/agenda/{sem[etapes][0]}
|
if not edt_id:
|
||||||
"""
|
flash(
|
||||||
ics_url_tmpl = sco_preferences.get_preference(
|
"accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)"
|
||||||
"edt_sem_ics_url", sem["formsemestre_id"]
|
|
||||||
)
|
|
||||||
if not ics_url_tmpl:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
ics_url = ics_url_tmpl.format(sem=sem)
|
|
||||||
except:
|
|
||||||
log(
|
|
||||||
f"""Exception in formsemestre_get_ics_url(formsemestre_id={sem["formsemestre_id"]})
|
|
||||||
ics_url_tmpl='{ics_url_tmpl}'
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
log(traceback.format_exc())
|
|
||||||
return None
|
return None
|
||||||
return ics_url
|
edt_ics_path = ScoDocSiteConfig.get("edt_ics_path")
|
||||||
|
if not edt_ics_path.strip():
|
||||||
|
return None
|
||||||
|
ics_filename = edt_ics_path.format(edt_id=edt_id)
|
||||||
|
try:
|
||||||
|
with open(ics_filename, "rb") as file:
|
||||||
|
log(f"Loading edt from {ics_filename}")
|
||||||
|
calendar = icalendar.Calendar.from_ical(file.read())
|
||||||
|
except FileNotFoundError:
|
||||||
|
flash("erreur chargement du calendrier")
|
||||||
|
log(
|
||||||
|
f"formsemestre_load_calendar: ics not found for {formsemestre}\npath='{ics_filename}'"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return calendar
|
||||||
|
|
||||||
|
|
||||||
def formsemestre_load_ics(sem):
|
_COLOR_PALETTE = [
|
||||||
"""Load ics data, from our cache or, when necessary, from external provider"""
|
"#ff6961",
|
||||||
# TODO: cacher le résultat
|
"#ffb480",
|
||||||
ics_url = formsemestre_get_ics_url(sem)
|
"#f8f38d",
|
||||||
if not ics_url:
|
"#42d6a4",
|
||||||
ics_data = ""
|
"#08cad1",
|
||||||
else:
|
"#59adf6",
|
||||||
log(f"Loading edt from {ics_url}")
|
"#9d94ff",
|
||||||
# 5s TODO: add config parameter, eg for slow networks
|
"#c780e8",
|
||||||
f = urllib.request.urlopen(ics_url, timeout=5)
|
]
|
||||||
ics_data = f.read()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
cal = icalendar.Calendar.from_ical(ics_data)
|
|
||||||
return cal
|
|
||||||
|
|
||||||
|
|
||||||
def get_edt_transcodage_groups(formsemestre_id):
|
def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]:
|
||||||
"""-> { nom_groupe_edt : nom_groupe_scodoc }"""
|
"""EDT complet du semestre, comme une liste de dict serialisable en json.
|
||||||
# TODO: valider ces données au moment où on enregistre les préférences
|
Fonction appellée par l'API /formsemestre/<int:formsemestre_id>/edt
|
||||||
edt2sco = {}
|
TODO: spécifier intervalle de dates start et end
|
||||||
sco2edt = {}
|
TODO: cacher ?
|
||||||
msg = "" # message erreur, '' si ok
|
|
||||||
txt = sco_preferences.get_preference("edt_groups2scodoc", formsemestre_id)
|
|
||||||
if not txt:
|
|
||||||
return edt2sco, sco2edt, msg
|
|
||||||
|
|
||||||
line_num = 1
|
|
||||||
for line in txt.split("\n"):
|
|
||||||
fs = [s.strip() for s in line.split(";")]
|
|
||||||
if len(fs) == 1: # groupe 'tous'
|
|
||||||
edt2sco[fs[0]] = None
|
|
||||||
sco2edt[None] = fs[0]
|
|
||||||
elif len(fs) == 2:
|
|
||||||
edt2sco[fs[0]] = fs[1]
|
|
||||||
sco2edt[fs[1]] = fs[0]
|
|
||||||
else:
|
|
||||||
msg = f"ligne {line_num} invalide"
|
|
||||||
line_num += 1
|
|
||||||
|
|
||||||
log(f"sco2edt={pprint.pformat(sco2edt)}")
|
|
||||||
return edt2sco, sco2edt, msg
|
|
||||||
|
|
||||||
|
|
||||||
def group_edt_json(group_id, start="", end=""): # actuellement inutilisé
|
|
||||||
"""EDT complet du semestre, au format JSON
|
|
||||||
TODO: indiquer un groupe
|
|
||||||
TODO: utiliser start et end (2 dates au format ISO YYYY-MM-DD)
|
|
||||||
TODO: cacher
|
|
||||||
"""
|
"""
|
||||||
group = sco_groups.get_group(group_id)
|
# Correspondances id edt -> id scodoc pour groupes, modules et enseignants
|
||||||
sem = sco_formsemestre.get_formsemestre(group["formsemestre_id"])
|
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
||||||
edt2sco, sco2edt, msg = get_edt_transcodage_groups(group["formsemestre_id"])
|
group_colors = {
|
||||||
|
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
|
||||||
|
for i, group_name in enumerate(edt2group)
|
||||||
|
}
|
||||||
|
default_group = formsemestre.get_default_group()
|
||||||
|
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
|
||||||
|
|
||||||
edt_group_name = sco2edt.get(group["group_name"], group["group_name"])
|
# Chargement du calendier ics
|
||||||
log("group scodoc=%s : edt=%s" % (group["group_name"], edt_group_name))
|
calendar = formsemestre_load_calendar(formsemestre)
|
||||||
|
if not calendar:
|
||||||
cal = formsemestre_load_ics(sem)
|
return []
|
||||||
events = [e for e in cal.walk() if e.name == "VEVENT"]
|
# Génération des événements, avec titre et champs utiles pour l'affichage dans ScoDoc
|
||||||
J = []
|
events = [e for e in calendar.walk() if e.name == "VEVENT"]
|
||||||
for e in events:
|
events_dict = []
|
||||||
# if e['X-GROUP-ID'].strip() == edt_group_name:
|
for event in events:
|
||||||
if "DESCRIPTION" in e:
|
if "DESCRIPTION" in event:
|
||||||
|
# --- Group
|
||||||
|
edt_group = extract_event_group(event)
|
||||||
|
# si pas de groupe dans l'event, prend toute la promo ("tous")
|
||||||
|
group: GroupDescr = (
|
||||||
|
edt2group.get(edt_group, None) if edt_group else default_group
|
||||||
|
)
|
||||||
|
background_color = (
|
||||||
|
group_colors.get(edt_group, "rgb(214, 233, 248)")
|
||||||
|
if group
|
||||||
|
else "lightgrey"
|
||||||
|
)
|
||||||
|
group_disp = (
|
||||||
|
f"""<div class="group-name">{group.get_nom_with_part(default="promo")}</div>"""
|
||||||
|
if group
|
||||||
|
else f"""<div class="group-edt">{edt_group}
|
||||||
|
<span title="vérifier noms de groupe ou configuration extraction edt">
|
||||||
|
{scu.EMO_WARNING} non reconnu</span>
|
||||||
|
</div>"""
|
||||||
|
)
|
||||||
|
# --- ModuleImpl
|
||||||
|
edt_module = extract_event_module(event)
|
||||||
|
modimpl: ModuleImpl = edt2modimpl.get(edt_module, None)
|
||||||
|
mod_disp = (
|
||||||
|
f"""<div class="module-edt mod-name" title="{modimpl.module.abbrev or ""}">{modimpl.module.code}
|
||||||
|
</div>"""
|
||||||
|
if modimpl
|
||||||
|
else f"""<div class="module-edt mod-etd" title="vérifier code edt module ?">{scu.EMO_WARNING} {edt_module}</div>"""
|
||||||
|
)
|
||||||
d = {
|
d = {
|
||||||
"title": e.decoded("DESCRIPTION"), # + '/' + e['X-GROUP-ID'],
|
# Champs utilisés par tui.calendar
|
||||||
"start": e.decoded("dtstart").isoformat(),
|
"calendarId": "cal1",
|
||||||
"end": e.decoded("dtend").isoformat(),
|
"title": extract_event_title(event) + group_disp + mod_disp,
|
||||||
|
"start": event.decoded("dtstart").isoformat(),
|
||||||
|
"end": event.decoded("dtend").isoformat(),
|
||||||
|
"backgroundColor": background_color,
|
||||||
|
# Infos brutes pour usage API éventuel
|
||||||
|
"group_id": group.id if group else None,
|
||||||
|
"group_edt_id": edt_group,
|
||||||
|
"moduleimpl_id": modimpl.id if modimpl else None,
|
||||||
}
|
}
|
||||||
J.append(d)
|
events_dict.append(d)
|
||||||
|
|
||||||
return scu.sendJSON(J)
|
return events_dict
|
||||||
|
|
||||||
|
|
||||||
# def experimental_calendar(group_id=None, formsemestre_id=None): # inutilisé
|
def extract_event_title(event: icalendar.cal.Event) -> str:
|
||||||
# """experimental page"""
|
"""Extrait le titre à afficher dans nos calendriers (si on ne retrouve pas le module ScoDoc)
|
||||||
# return "\n".join(
|
En effet, le titre présent dans l'ics emploi du temps est souvent complexe et peu parlant.
|
||||||
# [
|
Par exemple, à l'USPN, Hyperplanning nous donne:
|
||||||
# html_sco_header.sco_header(
|
'Matière : VRETR113 - Mathematiques du sig (VRETR113\nEnseignant : 1234 - M. DUPONT PIERRE\nTD : TDB\nSalle : L112 (IUTV) - L112\n'
|
||||||
# javascripts=[
|
"""
|
||||||
# "libjs/purl.js",
|
# TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
|
||||||
# "libjs/moment.min.js",
|
if not event.has_key("DESCRIPTION"):
|
||||||
# "libjs/fullcalendar/fullcalendar.min.js",
|
return "-"
|
||||||
# ],
|
description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
|
||||||
# cssstyles=[
|
# ici on prend le nom du module
|
||||||
# # 'libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css',
|
m = re.search(r"Matière : \w+ - ([\w\.\s']+)", description)
|
||||||
# # 'libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css',
|
if m and len(m.groups()) > 0:
|
||||||
# # 'libjs/bootstrap-multiselect/bootstrap-multiselect.css'
|
return m.group(1)
|
||||||
# "libjs/fullcalendar/fullcalendar.css",
|
# fallback: full description
|
||||||
# # media='print' 'libjs/fullcalendar/fullcalendar.print.css'
|
return description
|
||||||
# ],
|
|
||||||
# ),
|
|
||||||
# """<style>
|
|
||||||
# #loading {
|
|
||||||
# display: none;
|
|
||||||
# position: absolute;
|
|
||||||
# top: 10px;
|
|
||||||
# right: 10px;
|
|
||||||
# }
|
|
||||||
# </style>
|
|
||||||
# """,
|
|
||||||
# """<form id="group_selector" method="get">
|
|
||||||
# <span style="font-weight: bold; font-size:120%">Emplois du temps du groupe</span>""",
|
|
||||||
# sco_groups_view.menu_group_choice(
|
|
||||||
# group_id=group_id, formsemestre_id=formsemestre_id
|
|
||||||
# ),
|
|
||||||
# """</form><div id="loading">loading...</div>
|
|
||||||
# <div id="calendar"></div>
|
|
||||||
# """,
|
|
||||||
# html_sco_header.sco_footer(),
|
|
||||||
# """<script>
|
|
||||||
# $(document).ready(function() {
|
|
||||||
|
|
||||||
# var group_id = $.url().param()['group_id'];
|
|
||||||
|
|
||||||
# $('#calendar').fullCalendar({
|
def extract_event_module(event: icalendar.cal.Event) -> str:
|
||||||
# events: {
|
"""Extrait le code module de l'emplois du temps.
|
||||||
# url: 'group_edt_json?group_id=' + group_id,
|
Chaine vide si ne le trouve pas.
|
||||||
# error: function() {
|
Par exemple, à l'USPN, Hyperplanning nous donne le code 'VRETR113' dans DESCRIPTION
|
||||||
# $('#script-warning').show();
|
'Matière : VRETR113 - Mathematiques du sig (VRETR113\nEnseignant : 1234 - M. DUPONT PIERRE\nTD : TDB\nSalle : L112 (IUTV) - L112\n'
|
||||||
# }
|
"""
|
||||||
# },
|
# TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
|
||||||
# timeFormat: 'HH:mm',
|
if not event.has_key("DESCRIPTION"):
|
||||||
# timezone: 'local', // heure locale du client
|
return "-"
|
||||||
# loading: function(bool) {
|
description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
|
||||||
# $('#loading').toggle(bool);
|
# extraction du code:
|
||||||
# }
|
m = re.search(r"Matière : ([A-Z][A-Z0-9]+)", description)
|
||||||
# });
|
if m and len(m.groups()) > 0:
|
||||||
# });
|
return m.group(1)
|
||||||
# </script>
|
return ""
|
||||||
# """,
|
|
||||||
# ]
|
|
||||||
# )
|
def extract_event_group(event: icalendar.cal.Event) -> str:
|
||||||
|
"""Extrait le nom du groupe (TD, ...). "" si pas de match."""
|
||||||
|
# TODO: fonction ajustée à l'USPN, devra être paramétrable d'une façon ou d'une autre: regexp ?
|
||||||
|
# Utilise ici le SUMMARY
|
||||||
|
# qui est de la forme
|
||||||
|
# SUMMARY;LANGUAGE=fr:TP2 GPR1 - VCYR303 - Services reseaux ava (VCYR303) - 1234 - M. VIENNET EMMANUEL - V2ROM - BUT2 RT pa. ROM - Groupe 1
|
||||||
|
if not event.has_key("SUMMARY"):
|
||||||
|
return "-"
|
||||||
|
summary = event.decoded("SUMMARY").decode("utf-8") # assume ics in utf8
|
||||||
|
# extraction du code:
|
||||||
|
m = re.search(r".*- ([\w\s]+)$", summary)
|
||||||
|
if m and len(m.groups()) > 0:
|
||||||
|
return m.group(1).strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def formsemestre_retreive_modimpls_from_edt_id(
|
||||||
|
formsemestre: FormSemestre,
|
||||||
|
) -> dict[str, ModuleImpl]:
|
||||||
|
"""Construit un dict donnant le moduleimpl de chaque edt_id"""
|
||||||
|
edt2modimpl = {modimpl.get_edt_id(): modimpl for modimpl in formsemestre.modimpls}
|
||||||
|
edt2modimpl.pop("", None)
|
||||||
|
return edt2modimpl
|
||||||
|
|
||||||
|
|
||||||
|
def formsemestre_retreive_groups_from_edt_id(
|
||||||
|
formsemestre: FormSemestre,
|
||||||
|
) -> dict[str, GroupDescr]:
|
||||||
|
"""Construit un dict donnant le groupe de chaque edt_id"""
|
||||||
|
edt2group = {}
|
||||||
|
for partition in formsemestre.partitions:
|
||||||
|
edt2group.update({g.get_edt_id(): g for g in partition.groups})
|
||||||
|
edt2group.pop("", None)
|
||||||
|
return edt2group
|
||||||
|
@ -86,11 +86,6 @@ def build_context_dict(formsemestre_id: int) -> dict:
|
|||||||
def formsemestre_custommenu_html(formsemestre_id):
|
def formsemestre_custommenu_html(formsemestre_id):
|
||||||
"HTML code for custom menu"
|
"HTML code for custom menu"
|
||||||
menu = []
|
menu = []
|
||||||
# Calendrier électronique ?
|
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
|
||||||
ics_url = sco_edt_cal.formsemestre_get_ics_url(sem)
|
|
||||||
if ics_url:
|
|
||||||
menu.append({"title": "Emploi du temps (ics)", "url": ics_url})
|
|
||||||
# Liens globaux (config. générale)
|
# Liens globaux (config. générale)
|
||||||
params = build_context_dict(formsemestre_id)
|
params = build_context_dict(formsemestre_id)
|
||||||
for link in ScoDocSiteConfig.get_perso_links():
|
for link in ScoDocSiteConfig.get_perso_links():
|
||||||
|
@ -257,6 +257,13 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
|
|||||||
"enabled": current_user.has_permission(Permission.EditFormSemestre),
|
"enabled": current_user.has_permission(Permission.EditFormSemestre),
|
||||||
"helpmsg": "",
|
"helpmsg": "",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Expérimental: emploi du temps",
|
||||||
|
"endpoint": "notes.formsemestre_edt",
|
||||||
|
"args": {"formsemestre_id": formsemestre_id},
|
||||||
|
"enabled": True,
|
||||||
|
"helpmsg": "",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
# debug :
|
# debug :
|
||||||
if current_app.config["DEBUG"]:
|
if current_app.config["DEBUG"]:
|
||||||
@ -796,10 +803,10 @@ def formsemestre_description(
|
|||||||
tab.html_before_table = f"""
|
tab.html_before_table = f"""
|
||||||
<form name="f" method="get" action="{request.base_url}">
|
<form name="f" method="get" action="{request.base_url}">
|
||||||
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
|
<input type="hidden" name="formsemestre_id" value="{formsemestre_id}"></input>
|
||||||
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()"
|
<input type="checkbox" name="with_evals" value="1" onchange="document.f.submit()"
|
||||||
{ "checked" if with_evals else "" }
|
{ "checked" if with_evals else "" }
|
||||||
>indiquer les évaluations</input>
|
>indiquer les évaluations</input>
|
||||||
<input type="checkbox" name="with_parcours" value="1" onchange="document.f.submit()"
|
<input type="checkbox" name="with_parcours" value="1" onchange="document.f.submit()"
|
||||||
{ "checked" if with_parcours else "" }
|
{ "checked" if with_parcours else "" }
|
||||||
>indiquer les parcours BUT</input>
|
>indiquer les parcours BUT</input>
|
||||||
"""
|
"""
|
||||||
@ -836,7 +843,7 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||||||
'Tous les étudiants'}
|
'Tous les étudiants'}
|
||||||
</div>
|
</div>
|
||||||
<div class="sem-groups-partition-titre">{
|
<div class="sem-groups-partition-titre">{
|
||||||
"Gestion de l'assiduité" if not partition_is_empty else ""
|
"Gestion de l'assiduité" if not partition_is_empty else ""
|
||||||
}</div>
|
}</div>
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
@ -925,8 +932,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||||||
if formsemestre.can_change_groups():
|
if formsemestre.can_change_groups():
|
||||||
H.append(
|
H.append(
|
||||||
f""" (<a href="{url_for("scolar.partition_editor",
|
f""" (<a href="{url_for("scolar.partition_editor",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre.id,
|
||||||
edit_partition=1)
|
edit_partition=1)
|
||||||
}" class="stdlink">créer</a>)"""
|
}" class="stdlink">créer</a>)"""
|
||||||
)
|
)
|
||||||
@ -937,8 +944,8 @@ def _make_listes_sem(formsemestre: FormSemestre) -> str:
|
|||||||
H.append(
|
H.append(
|
||||||
f"""<h4><a class="stdlink"
|
f"""<h4><a class="stdlink"
|
||||||
href="{url_for("scolar.partition_editor",
|
href="{url_for("scolar.partition_editor",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre.id,
|
||||||
edit_partition=1)
|
edit_partition=1)
|
||||||
}">Ajouter une partition</a></h4>"""
|
}">Ajouter une partition</a></h4>"""
|
||||||
)
|
)
|
||||||
@ -1310,13 +1317,13 @@ def formsemestre_tableau_modules(
|
|||||||
<td class="formsemestre_status_code""><a
|
<td class="formsemestre_status_code""><a
|
||||||
href="{moduleimpl_status_url}"
|
href="{moduleimpl_status_url}"
|
||||||
title="{mod_descr}" class="stdlink">{mod.code}</a></td>
|
title="{mod_descr}" class="stdlink">{mod.code}</a></td>
|
||||||
<td class="scotext"><a href="{moduleimpl_status_url}" title="{mod_descr}"
|
<td class="scotext"><a href="{moduleimpl_status_url}" title="{mod_descr}"
|
||||||
class="formsemestre_status_link">{mod.abbrev or mod.titre or ""}</a>
|
class="formsemestre_status_link">{mod.abbrev or mod.titre or ""}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="formsemestre_status_inscrits">{len(mod_inscrits)}</td>
|
<td class="formsemestre_status_inscrits">{len(mod_inscrits)}</td>
|
||||||
<td class="resp scotext">
|
<td class="resp scotext">
|
||||||
<a class="discretelink" href="{moduleimpl_status_url}" title="{mod_ens}">{
|
<a class="discretelink" href="{moduleimpl_status_url}" title="{mod_ens}">{
|
||||||
sco_users.user_info(modimpl["responsable_id"])["prenomnom"]
|
sco_users.user_info(modimpl["responsable_id"])["prenomnom"]
|
||||||
}</a>
|
}</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@ -1457,8 +1464,8 @@ def formsemestre_warning_etuds_sans_note(
|
|||||||
"notes.formsemestre_note_etuds_sans_notes",
|
"notes.formsemestre_note_etuds_sans_notes",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
formsemestre_id=formsemestre.id,
|
formsemestre_id=formsemestre.id,
|
||||||
)}">{"lui" if nb_sans_notes == 1 else "leur"}
|
)}">{"lui" if nb_sans_notes == 1 else "leur"}
|
||||||
<span title="pour ne pas bloquer les autres étudiants, il est souvent préférable
|
<span title="pour ne pas bloquer les autres étudiants, il est souvent préférable
|
||||||
que les nouveaux aient des notes provisoires">affecter des notes</a>.
|
que les nouveaux aient des notes provisoires">affecter des notes</a>.
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
@ -208,8 +208,10 @@ def get_partition_groups(partition): # OBSOLETE !
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_default_group(formsemestre_id, fix_if_missing=False):
|
def get_default_group(formsemestre_id, fix_if_missing=False) -> int:
|
||||||
"""Returns group_id for default ('tous') group"""
|
"""Returns group_id for default ('tous') group
|
||||||
|
XXX remplacé par formsemestre.get_default_group
|
||||||
|
"""
|
||||||
r = ndb.SimpleDictFetch(
|
r = ndb.SimpleDictFetch(
|
||||||
"""SELECT gd.id AS group_id
|
"""SELECT gd.id AS group_id
|
||||||
FROM group_descr gd, partition p
|
FROM group_descr gd, partition p
|
||||||
|
@ -96,11 +96,13 @@ def group_rename(group_id):
|
|||||||
"default": group.edt_id or "",
|
"default": group.edt_id or "",
|
||||||
"size": 12,
|
"size": 12,
|
||||||
"allow_null": True,
|
"allow_null": True,
|
||||||
"explanation": "optionnel : identifiant du groupe dans le logiciel d'emploi du temps",
|
"explanation": """optionnel : identifiant du groupe dans le logiciel
|
||||||
|
d'emploi du temps, pour le cas où les noms de gropupes ne seraient pas
|
||||||
|
les mêmes dans ScoDoc et dans l'emploi du temps.""",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
submitlabel="Renommer",
|
submitlabel="Enregistrer",
|
||||||
cancelbutton="Annuler",
|
cancelbutton="Annuler",
|
||||||
)
|
)
|
||||||
dest_url = url_for(
|
dest_url = url_for(
|
||||||
|
@ -2033,24 +2033,13 @@ class BasePreferences:
|
|||||||
"category": "edt",
|
"category": "edt",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
(
|
# Divers
|
||||||
"edt_groups2scodoc",
|
|
||||||
{
|
|
||||||
"input_type": "textarea",
|
|
||||||
"initvalue": "",
|
|
||||||
"title": "Noms Groupes",
|
|
||||||
"explanation": "Transcodage: nom de groupe EDT ; non de groupe ScoDoc (sur plusieurs lignes)",
|
|
||||||
"rows": 8,
|
|
||||||
"cols": 16,
|
|
||||||
"category": "edt",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
(
|
||||||
"ImputationDept",
|
"ImputationDept",
|
||||||
{
|
{
|
||||||
"title": "Département d'imputation",
|
"title": "Département d'imputation",
|
||||||
"initvalue": "",
|
"initvalue": "",
|
||||||
"explanation": "préfixe id de session (optionnel, remplace nom département)",
|
"explanation": "optionnel: préfixe id de formsemestre (par défaut, le nom du département). Pour usages API avancés.",
|
||||||
"size": 10,
|
"size": 10,
|
||||||
"category": "edt",
|
"category": "edt",
|
||||||
},
|
},
|
||||||
|
16
app/static/css/edt.css
Normal file
16
app/static/css/edt.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
.toastui-calendar-template-time {
|
||||||
|
padding: 4px;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal !important;
|
||||||
|
align-items: normal !important;
|
||||||
|
font-size: 12pt;
|
||||||
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.group-name {
|
||||||
|
color:rgb(25, 113, 25);
|
||||||
|
}
|
||||||
|
.group-edt {
|
||||||
|
color: red;
|
||||||
|
background-color: yellow;
|
||||||
|
}
|
6
app/static/libjs/tui.calendar/toastui-calendar.min.css
vendored
Normal file
6
app/static/libjs/tui.calendar/toastui-calendar.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
9
app/static/libjs/tui.calendar/toastui-calendar.min.js
vendored
Normal file
9
app/static/libjs/tui.calendar/toastui-calendar.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
96
app/templates/formsemestre/edt.j2
Normal file
96
app/templates/formsemestre/edt.j2
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{% extends "sco_page.j2" %}
|
||||||
|
{% import 'bootstrap/wtf.html' as wtf %}
|
||||||
|
|
||||||
|
{% block styles %}
|
||||||
|
{{super()}}
|
||||||
|
<link href="{{scu.STATIC_DIR}}/libjs/tui.calendar/toastui-calendar.min.css" rel="stylesheet" type="text/css" />
|
||||||
|
<link rel="stylesheet" href="{{ scu.STATIC_DIR }}/css/edt.css" type="text/css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
|
||||||
|
<div class="tab-content">
|
||||||
|
<h2>Expérimental: emploi du temps</h2>
|
||||||
|
|
||||||
|
<div id="calendar" style="height: 900px;"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock app_content %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="{{scu.STATIC_DIR}}/libjs/tui.calendar/toastui-calendar.min.js"></script>
|
||||||
|
<script>
|
||||||
|
let hm_formatter = new Intl.DateTimeFormat('default', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const Calendar = tui.Calendar;
|
||||||
|
const container = document.getElementById('calendar');
|
||||||
|
const options = {
|
||||||
|
defaultView: 'week',
|
||||||
|
calendars: [
|
||||||
|
{
|
||||||
|
id: 'cal1',
|
||||||
|
name: 'Personal',
|
||||||
|
backgroundColor: '#03bd9e',
|
||||||
|
borderColor: 'white',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isReadOnly: true,
|
||||||
|
// timezone: { zones: [ { timezoneName: 'Europe/Paris' } ] },
|
||||||
|
template: {
|
||||||
|
// ce template nous permet d'avoir du HTML dans le title de l'event
|
||||||
|
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>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
usageStatistics: false,
|
||||||
|
week: {
|
||||||
|
dayNames: [ "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"],
|
||||||
|
eventView: ['time'],
|
||||||
|
hourStart: 7, // TODO préférence
|
||||||
|
hourEnd:24, // TODO préférence
|
||||||
|
showNowIndicator: true,
|
||||||
|
startDayOfWeek: 1,
|
||||||
|
taskView: false,
|
||||||
|
useDetailPopup:false, // on va pouvoir placer les liens scodoc
|
||||||
|
workweek: true, // TODO voir samedi travaillé
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const calendar = new Calendar(container, options);
|
||||||
|
//let events = [
|
||||||
|
// {
|
||||||
|
// id: "12456",
|
||||||
|
// start:"2023-11-10T09:30",
|
||||||
|
// end:"2023-11-10T11:30",
|
||||||
|
// backgroundColor:"lightblue",
|
||||||
|
// color: "red", // couleur du texte
|
||||||
|
// location: "quelque part",
|
||||||
|
// title:'Essai <a href="">saisir</a>',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: "12457",
|
||||||
|
// start:"2023-11-10T09:30",
|
||||||
|
// end:"2023-11-10T11:50",
|
||||||
|
// backgroundColor:"lightgreen",
|
||||||
|
// color: "blue", // couleur du texte
|
||||||
|
// title:'TD groupe 2',
|
||||||
|
// },
|
||||||
|
//];
|
||||||
|
fetch(`${SCO_URL}/../api/formsemestre/{{formsemestre.id}}/edt`)
|
||||||
|
.then(r=>{return r.json()})
|
||||||
|
.then(events=>{
|
||||||
|
calendar.createEvents(events);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock scripts %}
|
@ -105,3 +105,16 @@ def formsemestre_change_formation(formsemestre_id: int):
|
|||||||
formsemestre=formsemestre,
|
formsemestre=formsemestre,
|
||||||
sco=ScoData(formsemestre=formsemestre),
|
sco=ScoData(formsemestre=formsemestre),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/formsemestre/edt/<int:formsemestre_id>")
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
def formsemestre_edt(formsemestre_id: int):
|
||||||
|
"""Expérimental: affiche emploi du temps du semestre"""
|
||||||
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
||||||
|
return render_template(
|
||||||
|
"formsemestre/edt.j2",
|
||||||
|
formsemestre=formsemestre,
|
||||||
|
sco=ScoData(formsemestre=formsemestre),
|
||||||
|
)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.6.51"
|
SCOVERSION = "9.6.52"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user