WIP: Affichage de l'emploi du temps du semestre.

This commit is contained in:
Emmanuel Viennet 2023-11-11 18:13:18 +01:00
parent e06292de99
commit d1bc546d7b
17 changed files with 393 additions and 188 deletions

View File

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

View File

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

View File

@ -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 -inscription. aux groupes pour faciliter une éventuelle -inscription.
""" """
from app.models.formsemestre import FormSemestreInscription from app.models.formsemestre import FormSemestreInscription

View File

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

View File

@ -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 (!).

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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 %}

View File

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

View File

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