forked from ScoDoc/ScoDoc
Compare commits
4 Commits
e11101b53b
...
c456133487
Author | SHA1 | Date | |
---|---|---|---|
c456133487 | |||
21207b2dad | |||
772d796bda | |||
64b1a8536c |
@ -394,6 +394,32 @@ def group_edit(group_id: int):
|
||||
return group.to_dict(with_partition=True)
|
||||
|
||||
|
||||
@bp.route("/group/<int:group_id>/set_edt_id/<string:edt_id>", methods=["POST"])
|
||||
@api_web_bp.route("/group/<int:group_id>/set_edt_id/<string:edt_id>", methods=["POST"])
|
||||
@login_required
|
||||
@scodoc
|
||||
@permission_required(Permission.ScoView)
|
||||
@as_json
|
||||
def group_set_edt_id(group_id: int, edt_id: str):
|
||||
"""Set edt_id for this group.
|
||||
Contrairement à /edit, peut-être changé pour toute partition
|
||||
ou formsemestre non verrouillé.
|
||||
"""
|
||||
query = GroupDescr.query.filter_by(id=group_id)
|
||||
if g.scodoc_dept:
|
||||
query = (
|
||||
query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id)
|
||||
)
|
||||
group: GroupDescr = query.first_or_404()
|
||||
if not group.partition.formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
log(f"group_set_edt_id( {group_id}, '{edt_id}' )")
|
||||
group.edt_id = edt_id
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
return group.to_dict(with_partition=True)
|
||||
|
||||
|
||||
@bp.route("/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"])
|
||||
@api_web_bp.route(
|
||||
"/formsemestre/<int:formsemestre_id>/partition/create", methods=["POST"]
|
||||
@ -494,6 +520,7 @@ def formsemestre_order_partitions(formsemestre_id: int):
|
||||
db.session.commit()
|
||||
app.set_sco_dept(formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id)
|
||||
log(f"formsemestre_order_partitions({partition_ids})")
|
||||
return [
|
||||
partition.to_dict()
|
||||
for partition in formsemestre.partitions.order_by(Partition.numero)
|
||||
|
@ -139,7 +139,7 @@ class ConfigAssiduitesForm(FlaskForm):
|
||||
)
|
||||
|
||||
edt_ics_title_field = StringField(
|
||||
label="Champs contenant le titre",
|
||||
label="Champ contenant le titre",
|
||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
||||
validators=[Optional(), check_ics_field],
|
||||
)
|
||||
@ -152,7 +152,7 @@ class ConfigAssiduitesForm(FlaskForm):
|
||||
validators=[Optional(), check_ics_regexp],
|
||||
)
|
||||
edt_ics_group_field = StringField(
|
||||
label="Champs contenant le groupe",
|
||||
label="Champ contenant le groupe",
|
||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
||||
validators=[Optional(), check_ics_field],
|
||||
)
|
||||
@ -165,7 +165,7 @@ class ConfigAssiduitesForm(FlaskForm):
|
||||
validators=[Optional(), check_ics_regexp],
|
||||
)
|
||||
edt_ics_mod_field = StringField(
|
||||
label="Champs contenant le module",
|
||||
label="Champ contenant le module",
|
||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
||||
validators=[Optional(), check_ics_field],
|
||||
)
|
||||
@ -177,6 +177,18 @@ class ConfigAssiduitesForm(FlaskForm):
|
||||
""",
|
||||
validators=[Optional(), check_ics_regexp],
|
||||
)
|
||||
|
||||
edt_ics_uid_field = StringField(
|
||||
label="Champ contenant l'enseignant",
|
||||
description="""champ de l'évènement calendrier: DESCRIPTION, SUMMARY, ...""",
|
||||
validators=[Optional(), check_ics_field],
|
||||
)
|
||||
edt_ics_uid_regexp = StringField(
|
||||
label="Extraction de l'enseignant",
|
||||
description=r"""expression régulière python dont le premier groupe doit
|
||||
correspondre à l'identifiant (edt_id) de l'enseignant associé à l'évènement.
|
||||
Exemple: <tt>Enseignant : ([0-9]+)</tt>
|
||||
""",
|
||||
validators=[Optional(), check_ics_regexp],
|
||||
)
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
||||
|
@ -242,10 +242,12 @@ class GroupDescr(ScoDocModel):
|
||||
|
||||
def to_dict(self, with_partition=True) -> dict:
|
||||
"""as a dict, with or without partition"""
|
||||
if with_partition:
|
||||
partition_dict = self.partition.to_dict(with_groups=False)
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
if with_partition:
|
||||
d["partition"] = self.partition.to_dict(with_groups=False)
|
||||
d["partition"] = partition_dict
|
||||
return d
|
||||
|
||||
def get_edt_ids(self) -> list[str]:
|
||||
|
@ -36,6 +36,7 @@ import icalendar
|
||||
|
||||
from flask import g, url_for
|
||||
from app import log
|
||||
from app.auth.models import User
|
||||
from app.models import FormSemestre, GroupDescr, ModuleImpl, ScoDocSiteConfig
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -56,8 +57,11 @@ def formsemestre_load_calendar(
|
||||
Raises ScoValueError if not configured or not available or invalid format.
|
||||
"""
|
||||
edt_ids = []
|
||||
if edt_id is None and formsemestre:
|
||||
edt_ids = formsemestre.get_edt_ids()
|
||||
if edt_id is None:
|
||||
if formsemestre:
|
||||
edt_ids = formsemestre.get_edt_ids()
|
||||
else:
|
||||
edt_ids = [edt_id]
|
||||
if not edt_ids:
|
||||
raise ScoValueError(
|
||||
"accès aux emplois du temps non configuré pour ce semestre (pas d'edt_id)"
|
||||
@ -132,8 +136,10 @@ def formsemestre_edt_dict(
|
||||
except ScoValueError as exc:
|
||||
return exc.args[0]
|
||||
# Génération des événements pour le calendrier html
|
||||
promo_icon = f"""<img height="24px" src="{scu.STATIC_DIR}/icons/promo.svg"
|
||||
promo_icon = f"""<img height="28px" 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"
|
||||
title="saisir absences" alt="saisir absences"/>"""
|
||||
events_cal = []
|
||||
for event in events_scodoc:
|
||||
group: GroupDescr | bool = event["group"]
|
||||
@ -191,19 +197,26 @@ def formsemestre_edt_dict(
|
||||
# --- Lien saisie abs
|
||||
link_abs = (
|
||||
f"""<div class="module-edt link-abs"><a class="stdlink" href="{
|
||||
url_abs}">absences</a>
|
||||
url_abs}">{abs_icon}</a>
|
||||
</div>"""
|
||||
if url_abs
|
||||
else ""
|
||||
)
|
||||
|
||||
ens_user_name = event["ens"].user_name if event["ens"] else None
|
||||
ens_nomprenom = event["ens"].get_nomprenom() if event["ens"] else None
|
||||
d = {
|
||||
# Champs utilisés par tui.calendar
|
||||
"calendarId": "cal1",
|
||||
"title": f"""{title} {group_disp} {link_abs}""",
|
||||
"title": f"""{title} {group_disp} {
|
||||
'('+ens_nomprenom+')' if ens_nomprenom else ''
|
||||
} {link_abs}""",
|
||||
"start": event["start"],
|
||||
"end": event["end"],
|
||||
"backgroundColor": event["group_bg_color"],
|
||||
# Infos brutes pour usage API éventuel
|
||||
"ens_edt": event["edt_ens"],
|
||||
"ens_user_name": ens_user_name,
|
||||
"group_id": group.id if group else None,
|
||||
"group_edt_id": event["edt_group"],
|
||||
"moduleimpl_id": modimpl.id if modimpl else None,
|
||||
@ -257,6 +270,16 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
raise ScoValueError(
|
||||
"expression d'extraction du module depuis l'emploi du temps invalide"
|
||||
) from exc
|
||||
edt_ics_uid_field = ScoDocSiteConfig.get("edt_ics_uid_field")
|
||||
edt_ics_uid_regexp = ScoDocSiteConfig.get("edt_ics_uid_regexp")
|
||||
try:
|
||||
edt_ics_uid_pattern = (
|
||||
re.compile(edt_ics_uid_regexp) if edt_ics_uid_regexp else None
|
||||
)
|
||||
except re.error as exc:
|
||||
raise ScoValueError(
|
||||
"expression d'extraction de l'enseignant depuis l'emploi du temps invalide"
|
||||
) from exc
|
||||
# --- Correspondances id edt -> id scodoc pour groupes, modules et enseignants
|
||||
edt2group = formsemestre_retreive_groups_from_edt_id(formsemestre)
|
||||
group_colors = {
|
||||
@ -266,6 +289,7 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
edt_groups_ids = set() # les ids de groupes 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"]
|
||||
events_sco = []
|
||||
@ -313,7 +337,19 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
else:
|
||||
modimpl = False
|
||||
edt_module = ""
|
||||
# --- TODO: enseignant
|
||||
# --- Enseignant
|
||||
if edt_ics_uid_pattern:
|
||||
edt_ens = extract_event_data(
|
||||
event, edt_ics_uid_field, edt_ics_uid_pattern
|
||||
)
|
||||
if edt_ens in edt2user:
|
||||
ens = edt2user[edt_ens]
|
||||
else:
|
||||
ens = User.query.filter_by(edt_id=edt_ens).first()
|
||||
edt2user[edt_ens] = ens
|
||||
else:
|
||||
ens = None
|
||||
edt_ens = ""
|
||||
#
|
||||
events_sco.append(
|
||||
{
|
||||
@ -324,6 +360,9 @@ def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[s
|
||||
"group_bg_color": group_bg_color, # associée au groupe
|
||||
"modimpl": modimpl, # False si extracteur non configuré
|
||||
"edt_module": edt_module, # id module edt non traduit
|
||||
# Enseignant
|
||||
"edt_ens": edt_ens, # id ens edt, non traduit
|
||||
"ens": ens,
|
||||
# heures pour saisie abs: en heure LOCALE DU SERVEUR
|
||||
"heure_deb": event.decoded("dtstart")
|
||||
.replace(tzinfo=timezone.utc)
|
||||
|
@ -633,3 +633,7 @@ h3 {
|
||||
#zoneGroupes .groupe[data-idgroupe=aucun]>div:nth-child(1) {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#zonePartitions button span.editing:not(:first-child) {
|
||||
margin-left: 8px;
|
||||
}
|
1
app/static/icons/absences.svg
Normal file
1
app/static/icons/absences.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.8 KiB |
@ -96,7 +96,7 @@ affectent notamment les comptages d'absences de tous les bulletins des
|
||||
<button id="test_load_ics" type="button" onclick="load_ics_sample()" disabled>
|
||||
Essayer de charger l'ics</button>
|
||||
<div id="raw-ics-sample-zone">
|
||||
<div>Voici un évènement chargé au milieu de ce calendrier.
|
||||
<div>Voici un évènement chargé, pris au hasard au milieu de ce calendrier.
|
||||
Utilisez cet exemple pour configurer les expressions d'extraction
|
||||
en bas de ce formulaire.
|
||||
</div>
|
||||
@ -121,7 +121,10 @@ affectent notamment les comptages d'absences de tous les bulletins des
|
||||
{{ wtf.form_field(form.edt_ics_mod_field) }}
|
||||
{{ wtf.form_field(form.edt_ics_mod_regexp) }}
|
||||
</div>
|
||||
|
||||
<div class="config-edt">
|
||||
{{ wtf.form_field(form.edt_ics_uid_field) }}
|
||||
{{ wtf.form_field(form.edt_ics_uid_regexp) }}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
{{ wtf.form_field(form.submit) }}
|
||||
|
@ -93,6 +93,6 @@
|
||||
<script src="{{scu.STATIC_DIR}}/js/scodoc.js"></script>
|
||||
<script>
|
||||
const SCO_URL = "{{ url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)[:-11] }}";
|
||||
const SCO_TIMEZONE = {{ scu.TIME_ZONE }}
|
||||
const SCO_TIMEZONE = "{{ scu.TIME_ZONE }}";
|
||||
</script>
|
||||
{% endblock %}
|
@ -4,6 +4,9 @@
|
||||
{% block styles %}
|
||||
{{super()}}
|
||||
<style>
|
||||
.form-titre {
|
||||
font-weight: bold;
|
||||
}
|
||||
span.mod-label {
|
||||
display: inline-block;
|
||||
min-width: 300px;
|
||||
@ -68,13 +71,17 @@ Pour les modifier, aller dans l'édition de la formation.
|
||||
<form id="mf" class="form form-horizontal" method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.hidden_tag() }}
|
||||
{{ wtf.form_errors(form, hiddens="only") }}
|
||||
<div class="form-group">
|
||||
<div class="form-group form-titre">
|
||||
<span class="mod-label">
|
||||
<label>Module</label>
|
||||
<span class="code-apo-module">Code Apo. Module</span>
|
||||
<span class="field-apo">Code(s) Apogée</span>
|
||||
<span class="field-edt">Code EDT</span>
|
||||
<span class="code-apo-module" title="codes dans la formation">Code Apo. Module</span>
|
||||
</span>
|
||||
<span class="field-apo"
|
||||
title="codes spécifiques à ce semestre (si différents de ceux de la formation)">
|
||||
Code(s) Apogée</span>
|
||||
<span class="field-edt"
|
||||
title="identifiant dans l'emploi du temps (si différent du code Apogée)">
|
||||
Code EDT</span>
|
||||
</div>
|
||||
{% for modimpl in formsemestre.modimpls_sorted %}
|
||||
{{ render_text_field(form["modimpl_apo_" ~ modimpl.id], form["modimpl_edt_" ~ modimpl.id], modimpl.module.get_codes_apogee()) }}
|
||||
|
@ -47,6 +47,23 @@ table#edt2group tbody tr.active-row {
|
||||
si vous voyez ici de nombreuses lignes, il est possible que l'expression régulière
|
||||
d'extraction soit incorrecte (voir configuration globale) ou bien que votre logiciel d'emploi du temps génère de nombreux évènements non associés à un groupe donné.
|
||||
</div>
|
||||
|
||||
{% if ScoDocSiteConfig.get("edt_ics_group_field") %}
|
||||
<div>Les groupes sont extrait du champs <b>{{ScoDocSiteConfig.get("edt_ics_group_field")}}</b>
|
||||
à l'aide de l'expression régulière: <tt>{{ScoDocSiteConfig.get("edt_ics_group_regexp")}}</tt>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="fontred">extraction non configuré</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.has_permission(sco.Permission.ScoSuperAdmin) %}
|
||||
<div>Pour changer ce réglage, <a href="{{url_for('scodoc.config_assiduites')}}"
|
||||
class="stdlink">voir la page de configuration</a>.</div>
|
||||
{% else %}
|
||||
<div class="fontred">au besoin, l'administrateur peut changer ces réglages dans
|
||||
le panneau de configuration générale.</div>
|
||||
{% endif %}
|
||||
|
||||
<div>Voici ce qui a été extrait de l'emploi du temps par l'expression régulière configurée:
|
||||
</div>
|
||||
<ul>
|
||||
|
@ -251,7 +251,7 @@
|
||||
div.classList.add("dt-button");
|
||||
div.dataset.idgroupe = groupe.id;
|
||||
div.dataset.idedt = groupe.edt_id || "";
|
||||
let title_EDT = groupe.edt_id || "";
|
||||
let title_EDT = `Identifiant EDT: ${groupe.edt_id || groupe.group_name}`;
|
||||
div.innerHTML = `
|
||||
<span class="editing move">||</span>
|
||||
<span>${groupe.group_name}</span>
|
||||
@ -259,7 +259,7 @@
|
||||
<span class="editing calendarEdit" title="${title_EDT}">📅</span>
|
||||
<span class="editing suppr">❌</span>`;
|
||||
|
||||
if (title_EDT) {
|
||||
if (groupe.edt_id) {
|
||||
div.querySelector(".calendarEdit").classList.add("actif");
|
||||
}
|
||||
|
||||
@ -801,7 +801,7 @@
|
||||
|
||||
let btnGroupe = document.querySelector(`#zonePartitions .groupes [data-idgroupe="${idGroupe}"]`);
|
||||
btnGroupe.dataset.idedt = id_EDT;
|
||||
btnGroupe.querySelector(".calendarEdit").title = id_EDT || "";
|
||||
btnGroupe.querySelector(".calendarEdit").title = `Identifiant EDT: ${id_EDT || this.parentElement.parentElement.querySelector("span").innerText}`;
|
||||
|
||||
if (id_EDT) {
|
||||
btnGroupe.querySelector(".calendarEdit").classList.add("actif");
|
||||
@ -815,7 +815,7 @@
|
||||
fetch(url, { method: "POST" })
|
||||
.then(r => { return r.json() })
|
||||
.then(r => {
|
||||
if (r.OK != true) {
|
||||
if (r.id != idGroupe) {
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (5).</h2>";
|
||||
}
|
||||
});
|
||||
@ -870,7 +870,7 @@
|
||||
.then(r => { return r.json() })
|
||||
.then(r => {
|
||||
if (r.OK != true) {
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (5).</h2>";
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (6).</h2>";
|
||||
}
|
||||
listeGroupesAutoaffectation();
|
||||
})
|
||||
|
@ -209,4 +209,6 @@ def formsemestre_edt_help_config(formsemestre_id: int):
|
||||
edt_groups_ids=edt_groups_ids,
|
||||
events_sco=events_sco,
|
||||
sco=ScoData(formsemestre=formsemestre),
|
||||
ScoDocSiteConfig=ScoDocSiteConfig,
|
||||
title="Aide configuration EDT",
|
||||
)
|
||||
|
@ -52,7 +52,7 @@ from PIL import Image as PILImage
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
|
||||
from app import db
|
||||
from app import db, log
|
||||
from app.auth.models import User, Role
|
||||
from app.auth.cas import set_cas_configuration
|
||||
from app.decorators import (
|
||||
@ -332,6 +332,8 @@ def config_assiduites():
|
||||
("edt_ics_group_regexp", "Expression extraction groupe"),
|
||||
("edt_ics_mod_field", "Champ contenant module"),
|
||||
("edt_ics_mod_regexp", "Expression extraction module"),
|
||||
("edt_ics_uid_field", "Champ contenant l'enseignant"),
|
||||
("edt_ics_uid_regexp", "Expression extraction de l'enseignant"),
|
||||
)
|
||||
|
||||
if form.validate_on_submit():
|
||||
@ -380,16 +382,20 @@ def config_assiduites():
|
||||
@admin_required
|
||||
def ics_raw_sample(edt_id: str):
|
||||
"Renvoie un extrait de l'ics brut, pour aider à configurer les extractions"
|
||||
log(f"ics_raw_sample/{edt_id}")
|
||||
try:
|
||||
raw_ics, _ = sco_edt_cal.formsemestre_load_calendar(edt_id=edt_id)
|
||||
except ScoValueError as exc:
|
||||
log(f"ics_raw_sample: formsemestre_load_calendar({edt_id}) failed")
|
||||
return exc.args[0]
|
||||
try:
|
||||
ics = raw_ics.decode(scu.SCO_ENCODING)
|
||||
except SyntaxError:
|
||||
log("ics_raw_sample: raw_ics.decode failed")
|
||||
return f"Erreur lors de la conversion vers {scu.SCO_ENCODING}"
|
||||
evs = ics.split("BEGIN:VEVENT")
|
||||
if len(evs) < 1:
|
||||
log("ics_raw_sample: empty calendar")
|
||||
return "pas d'évènements VEVENT détectés dans ce fichier"
|
||||
return "BEGIN:VEVENT" + evs[len(evs) // 2]
|
||||
|
||||
|
@ -120,6 +120,14 @@ def test_formsemestre_partition(api_headers):
|
||||
assert group["group_name"] == group_d["group_name"]
|
||||
assert group["edt_id"] == "GEDT2"
|
||||
|
||||
# Change edt_id via route dédiée:
|
||||
group_t = POST_JSON(
|
||||
f"/group/{group_r['id']}/set_edt_id/GEDT3",
|
||||
headers=headers,
|
||||
)
|
||||
assert group_t["id"] == group_r["id"]
|
||||
assert group_t["edt_id"] == "GEDT3"
|
||||
|
||||
# Place un étudiant dans le groupe
|
||||
etud = GET(f"/formsemestre/{formsemestre_id}/etudiants", headers=headers)[0]
|
||||
repl = POST_JSON(f"/group/{group['id']}/set_etudiant/{etud['id']}", headers=headers)
|
||||
|
Loading…
Reference in New Issue
Block a user