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.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.sco_permissions import Permission
|
||||
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
|
||||
db.session.add(formsemestre)
|
||||
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()
|
||||
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:
|
||||
"""Un dict avec des informations sur le semestre
|
||||
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)'}">"""
|
||||
)
|
||||
|
||||
def get_nom_with_part(self) -> str:
|
||||
"Nom avec partition: 'TD A'"
|
||||
def get_nom_with_part(self, default="-") -> str:
|
||||
"""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 '-'}"
|
||||
|
||||
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)
|
||||
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:
|
||||
"""Nombre inscrits à ce group et au formsemestre.
|
||||
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
|
||||
|
||||
|
@ -45,6 +45,12 @@ class ModuleImpl(db.Model):
|
||||
def __repr__(self):
|
||||
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:
|
||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||
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 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]:
|
||||
"""Les parcours utilisant ce module.
|
||||
Si tous les parcours, liste vide (!).
|
||||
|
@ -29,175 +29,191 @@
|
||||
|
||||
XXX usage uniquement experimental pour tests implémentations
|
||||
|
||||
XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU).
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import icalendar
|
||||
|
||||
import urllib
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from flask import flash
|
||||
from app import log
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc import sco_preferences
|
||||
from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
|
||||
def formsemestre_get_ics_url(sem):
|
||||
"""
|
||||
edt_sem_ics_url est un template
|
||||
utilisé avec .format(sem=sem)
|
||||
Par exemple:
|
||||
https://example.fr/agenda/{sem[etapes][0]}
|
||||
"""
|
||||
ics_url_tmpl = sco_preferences.get_preference(
|
||||
"edt_sem_ics_url", sem["formsemestre_id"]
|
||||
def formsemestre_load_calendar(
|
||||
formsemestre: FormSemestre,
|
||||
) -> icalendar.cal.Calendar | None:
|
||||
"""Load ics data, return calendar or None if not configured or not available"""
|
||||
edt_id = formsemestre.get_edt_id()
|
||||
if not edt_id:
|
||||
flash(
|
||||
"accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)"
|
||||
)
|
||||
if not ics_url_tmpl:
|
||||
return None
|
||||
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:
|
||||
ics_url = ics_url_tmpl.format(sem=sem)
|
||||
except:
|
||||
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"""Exception in formsemestre_get_ics_url(formsemestre_id={sem["formsemestre_id"]})
|
||||
ics_url_tmpl='{ics_url_tmpl}'
|
||||
"""
|
||||
f"formsemestre_load_calendar: ics not found for {formsemestre}\npath='{ics_filename}'"
|
||||
)
|
||||
log(traceback.format_exc())
|
||||
return None
|
||||
return ics_url
|
||||
|
||||
return calendar
|
||||
|
||||
|
||||
def formsemestre_load_ics(sem):
|
||||
"""Load ics data, from our cache or, when necessary, from external provider"""
|
||||
# TODO: cacher le résultat
|
||||
ics_url = formsemestre_get_ics_url(sem)
|
||||
if not ics_url:
|
||||
ics_data = ""
|
||||
else:
|
||||
log(f"Loading edt from {ics_url}")
|
||||
# 5s TODO: add config parameter, eg for slow networks
|
||||
f = urllib.request.urlopen(ics_url, timeout=5)
|
||||
ics_data = f.read()
|
||||
f.close()
|
||||
|
||||
cal = icalendar.Calendar.from_ical(ics_data)
|
||||
return cal
|
||||
_COLOR_PALETTE = [
|
||||
"#ff6961",
|
||||
"#ffb480",
|
||||
"#f8f38d",
|
||||
"#42d6a4",
|
||||
"#08cad1",
|
||||
"#59adf6",
|
||||
"#9d94ff",
|
||||
"#c780e8",
|
||||
]
|
||||
|
||||
|
||||
def get_edt_transcodage_groups(formsemestre_id):
|
||||
"""-> { nom_groupe_edt : nom_groupe_scodoc }"""
|
||||
# TODO: valider ces données au moment où on enregistre les préférences
|
||||
edt2sco = {}
|
||||
sco2edt = {}
|
||||
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
|
||||
def formsemestre_edt_dict(formsemestre: FormSemestre) -> list[dict]:
|
||||
"""EDT complet du semestre, comme une liste de dict serialisable en json.
|
||||
Fonction appellée par l'API /formsemestre/<int:formsemestre_id>/edt
|
||||
TODO: spécifier intervalle de dates start et end
|
||||
TODO: cacher ?
|
||||
"""
|
||||
group = sco_groups.get_group(group_id)
|
||||
sem = sco_formsemestre.get_formsemestre(group["formsemestre_id"])
|
||||
edt2sco, sco2edt, msg = get_edt_transcodage_groups(group["formsemestre_id"])
|
||||
|
||||
edt_group_name = sco2edt.get(group["group_name"], group["group_name"])
|
||||
log("group scodoc=%s : edt=%s" % (group["group_name"], edt_group_name))
|
||||
|
||||
cal = formsemestre_load_ics(sem)
|
||||
events = [e for e in cal.walk() if e.name == "VEVENT"]
|
||||
J = []
|
||||
for e in events:
|
||||
# if e['X-GROUP-ID'].strip() == edt_group_name:
|
||||
if "DESCRIPTION" in e:
|
||||
d = {
|
||||
"title": e.decoded("DESCRIPTION"), # + '/' + e['X-GROUP-ID'],
|
||||
"start": e.decoded("dtstart").isoformat(),
|
||||
"end": e.decoded("dtend").isoformat(),
|
||||
# Correspondances id edt -> id scodoc pour groupes, modules et enseignants
|
||||
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
||||
group_colors = {
|
||||
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
|
||||
for i, group_name in enumerate(edt2group)
|
||||
}
|
||||
J.append(d)
|
||||
default_group = formsemestre.get_default_group()
|
||||
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
|
||||
|
||||
return scu.sendJSON(J)
|
||||
# Chargement du calendier ics
|
||||
calendar = formsemestre_load_calendar(formsemestre)
|
||||
if not calendar:
|
||||
return []
|
||||
# Génération des événements, avec titre et champs utiles pour l'affichage dans ScoDoc
|
||||
events = [e for e in calendar.walk() if e.name == "VEVENT"]
|
||||
events_dict = []
|
||||
for event in events:
|
||||
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 = {
|
||||
# Champs utilisés par tui.calendar
|
||||
"calendarId": "cal1",
|
||||
"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,
|
||||
}
|
||||
events_dict.append(d)
|
||||
|
||||
return events_dict
|
||||
|
||||
|
||||
# def experimental_calendar(group_id=None, formsemestre_id=None): # inutilisé
|
||||
# """experimental page"""
|
||||
# return "\n".join(
|
||||
# [
|
||||
# html_sco_header.sco_header(
|
||||
# javascripts=[
|
||||
# "libjs/purl.js",
|
||||
# "libjs/moment.min.js",
|
||||
# "libjs/fullcalendar/fullcalendar.min.js",
|
||||
# ],
|
||||
# cssstyles=[
|
||||
# # 'libjs/bootstrap-3.1.1-dist/css/bootstrap.min.css',
|
||||
# # 'libjs/bootstrap-3.1.1-dist/css/bootstrap-theme.min.css',
|
||||
# # 'libjs/bootstrap-multiselect/bootstrap-multiselect.css'
|
||||
# "libjs/fullcalendar/fullcalendar.css",
|
||||
# # media='print' 'libjs/fullcalendar/fullcalendar.print.css'
|
||||
# ],
|
||||
# ),
|
||||
# """<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() {
|
||||
def extract_event_title(event: icalendar.cal.Event) -> str:
|
||||
"""Extrait le titre à afficher dans nos calendriers (si on ne retrouve pas le module ScoDoc)
|
||||
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:
|
||||
'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 ?
|
||||
if not event.has_key("DESCRIPTION"):
|
||||
return "-"
|
||||
description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
|
||||
# ici on prend le nom du module
|
||||
m = re.search(r"Matière : \w+ - ([\w\.\s']+)", description)
|
||||
if m and len(m.groups()) > 0:
|
||||
return m.group(1)
|
||||
# fallback: full description
|
||||
return description
|
||||
|
||||
# var group_id = $.url().param()['group_id'];
|
||||
|
||||
# $('#calendar').fullCalendar({
|
||||
# events: {
|
||||
# url: 'group_edt_json?group_id=' + group_id,
|
||||
# error: function() {
|
||||
# $('#script-warning').show();
|
||||
# }
|
||||
# },
|
||||
# timeFormat: 'HH:mm',
|
||||
# timezone: 'local', // heure locale du client
|
||||
# loading: function(bool) {
|
||||
# $('#loading').toggle(bool);
|
||||
# }
|
||||
# });
|
||||
# });
|
||||
# </script>
|
||||
# """,
|
||||
# ]
|
||||
# )
|
||||
def extract_event_module(event: icalendar.cal.Event) -> str:
|
||||
"""Extrait le code module de l'emplois du temps.
|
||||
Chaine vide si ne le trouve pas.
|
||||
Par exemple, à l'USPN, Hyperplanning nous donne le code 'VRETR113' dans DESCRIPTION
|
||||
'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 ?
|
||||
if not event.has_key("DESCRIPTION"):
|
||||
return "-"
|
||||
description = event.decoded("DESCRIPTION").decode("utf-8") # assume ics in utf8
|
||||
# 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)
|
||||
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):
|
||||
"HTML code for custom 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)
|
||||
params = build_context_dict(formsemestre_id)
|
||||
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),
|
||||
"helpmsg": "",
|
||||
},
|
||||
{
|
||||
"title": "Expérimental: emploi du temps",
|
||||
"endpoint": "notes.formsemestre_edt",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
"enabled": True,
|
||||
"helpmsg": "",
|
||||
},
|
||||
]
|
||||
# debug :
|
||||
if current_app.config["DEBUG"]:
|
||||
|
@ -208,8 +208,10 @@ def get_partition_groups(partition): # OBSOLETE !
|
||||
)
|
||||
|
||||
|
||||
def get_default_group(formsemestre_id, fix_if_missing=False):
|
||||
"""Returns group_id for default ('tous') group"""
|
||||
def get_default_group(formsemestre_id, fix_if_missing=False) -> int:
|
||||
"""Returns group_id for default ('tous') group
|
||||
XXX remplacé par formsemestre.get_default_group
|
||||
"""
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT gd.id AS group_id
|
||||
FROM group_descr gd, partition p
|
||||
|
@ -96,11 +96,13 @@ def group_rename(group_id):
|
||||
"default": group.edt_id or "",
|
||||
"size": 12,
|
||||
"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",
|
||||
)
|
||||
dest_url = url_for(
|
||||
|
@ -2033,24 +2033,13 @@ class BasePreferences:
|
||||
"category": "edt",
|
||||
},
|
||||
),
|
||||
(
|
||||
"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",
|
||||
},
|
||||
),
|
||||
# Divers
|
||||
(
|
||||
"ImputationDept",
|
||||
{
|
||||
"title": "Département d'imputation",
|
||||
"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,
|
||||
"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,
|
||||
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 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.6.51"
|
||||
SCOVERSION = "9.6.52"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user