diff --git a/app/api/partitions.py b/app/api/partitions.py index 516fc417..268e64c9 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -394,6 +394,32 @@ def group_edit(group_id: int): return group.to_dict(with_partition=True) +@bp.route("/group//set_edt_id/", methods=["POST"]) +@api_web_bp.route("/group//set_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//partition/create", methods=["POST"]) @api_web_bp.route( "/formsemestre//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) diff --git a/app/forms/main/config_assiduites.py b/app/forms/main/config_assiduites.py index d4adb057..bb1e8920 100644 --- a/app/forms/main/config_assiduites.py +++ b/app/forms/main/config_assiduites.py @@ -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: Enseignant : ([0-9]+) + """, + validators=[Optional(), check_ics_regexp], + ) submit = SubmitField("Valider") cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/models/groups.py b/app/models/groups.py index 4df0b5d8..d4905cda 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -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]: diff --git a/app/scodoc/sco_edt_cal.py b/app/scodoc/sco_edt_cal.py index cf390438..e0a94e7d 100644 --- a/app/scodoc/sco_edt_cal.py +++ b/app/scodoc/sco_edt_cal.py @@ -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"""promotion""" + abs_icon = f"""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"""""" 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) diff --git a/app/static/css/partition_editor.css b/app/static/css/partition_editor.css index 912f0d16..00bbb248 100644 --- a/app/static/css/partition_editor.css +++ b/app/static/css/partition_editor.css @@ -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; +} \ No newline at end of file diff --git a/app/static/icons/absences.svg b/app/static/icons/absences.svg new file mode 100644 index 00000000..9a665ebb --- /dev/null +++ b/app/static/icons/absences.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/templates/assiduites/pages/config_assiduites.j2 b/app/templates/assiduites/pages/config_assiduites.j2 index 7d2ae0a5..f1462cd7 100644 --- a/app/templates/assiduites/pages/config_assiduites.j2 +++ b/app/templates/assiduites/pages/config_assiduites.j2 @@ -96,7 +96,7 @@ affectent notamment les comptages d'absences de tous les bulletins des
-
Voici un évènement chargé au milieu de ce calendrier. +
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.
@@ -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) }}
- +
+ {{ wtf.form_field(form.edt_ics_uid_field) }} + {{ wtf.form_field(form.edt_ics_uid_regexp) }} +
{{ wtf.form_field(form.submit) }} diff --git a/app/templates/base.j2 b/app/templates/base.j2 index 717a6af0..eace4df9 100644 --- a/app/templates/base.j2 +++ b/app/templates/base.j2 @@ -93,6 +93,6 @@ {% endblock %} \ No newline at end of file diff --git a/app/templates/formsemestre/edit_modimpls_codes.j2 b/app/templates/formsemestre/edit_modimpls_codes.j2 index a663207d..b1be99b1 100644 --- a/app/templates/formsemestre/edit_modimpls_codes.j2 +++ b/app/templates/formsemestre/edit_modimpls_codes.j2 @@ -4,6 +4,9 @@ {% block styles %} {{super()}}