Merge branch 'main96' into sans_moment

This commit is contained in:
Iziram 2023-11-10 14:23:24 +01:00
commit 6ae9bcafb1
16 changed files with 192 additions and 104 deletions

View File

@ -39,6 +39,15 @@ def after_cas_login():
"scodoc_cas_login_date" "scodoc_cas_login_date"
] = datetime.datetime.now().isoformat() ] = datetime.datetime.now().isoformat()
user.cas_last_login = datetime.datetime.utcnow() user.cas_last_login = datetime.datetime.utcnow()
if flask.session.get("CAS_EDT_ID"):
# essaie de récupérer l'edt_id s'il est présent
# cet ID peut être renvoyé par le CAS et extrait par ScoDoc
# via l'expression `cas_edt_id_from_xml_regexp`
# voir flask_cas.routing
edt_id = flask.session.get("CAS_EDT_ID")
current_app.logger.info(f"""after_cas_login: storing edt_id for {
user.user_name}: '{edt_id}'""")
user.edt_id = edt_id
db.session.add(user) db.session.add(user)
db.session.commit() db.session.commit()
return flask.redirect(url_for("scodoc.index")) return flask.redirect(url_for("scodoc.index"))

View File

@ -185,18 +185,7 @@ class User(UserMixin, db.Model):
return self._migrate_scodoc7_password(password) return self._migrate_scodoc7_password(password)
return False return False
password_ok = check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
if password_ok and cas_enabled and flask.session.get("CAS_EDT_ID"):
# essaie de récupérer l'edt_id s'il est présent
# cet ID peut être renvoyé par le CAS et extrait par ScoDoc
# via l'expression `cas_edt_id_from_xml_regexp`
# voir flask_cas.routing
edt_id = flask.session.get("CAS_EDT_ID")
log(f"Storing edt_id for {self.user_name}: '{edt_id}'")
self.edt_id = edt_id
db.session.add(self)
db.session.commit()
return password_ok
def _migrate_scodoc7_password(self, password) -> bool: def _migrate_scodoc7_password(self, password) -> bool:
"""After migration, rehash password.""" """After migration, rehash password."""

View File

@ -28,12 +28,14 @@
""" """
Formulaire configuration Module Assiduités Formulaire configuration Module Assiduités
""" """
import datetime
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import SubmitField, DecimalField from wtforms import DecimalField, SubmitField, ValidationError
from wtforms.fields.simple import StringField from wtforms.fields.simple import StringField
from wtforms.validators import Optional
from wtforms.widgets import TimeInput from wtforms.widgets import TimeInput
import datetime
class TimeField(StringField): class TimeField(StringField):
@ -72,9 +74,28 @@ class TimeField(StringField):
else: else:
raise ValueError raise ValueError
self.data = datetime.time(hour, minutes, seconds) self.data = datetime.time(hour, minutes, seconds)
except ValueError: except ValueError as exc:
self.data = None self.data = None
raise ValueError(self.gettext("Not a valid time string")) raise ValueError(self.gettext("Not a valid time string")) from exc
def check_tick_time(form, field):
"""Le tick_time doit être entre 0 et 60 minutes"""
if field.data < 1 or field.data > 59:
raise ValidationError("Valeur de granularité invalide (entre 1 et 59)")
def check_ics_path(form, field):
"""Vérifie que le chemin est bien un chemin absolu
et qu'il contient edt_id
"""
data = field.data.strip()
if not data:
return
if not data.startswith("/"):
raise ValidationError("Le chemin vers les ics doit commencer par /")
if not "{edt_id}" in data:
raise ValidationError("Le chemin vers les ics doit utiliser {edt_id}")
class ConfigAssiduitesForm(FlaskForm): class ConfigAssiduitesForm(FlaskForm):
@ -84,7 +105,20 @@ class ConfigAssiduitesForm(FlaskForm):
lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)") lunch_time = TimeField("Heure de midi (date pivot entre Matin et Après Midi)")
afternoon_time = TimeField("Fin de la journée") afternoon_time = TimeField("Fin de la journée")
tick_time = DecimalField("Granularité de la Time Line (temps en minutes)", places=0) tick_time = DecimalField(
"Granularité de la timeline (temps en minutes)",
places=0,
validators=[check_tick_time],
)
edt_ics_path = StringField(
label="Chemin vers les ics",
description="""Chemin absolu unix sur le serveur vers le fichier ics donnant l'emploi
du temps d'un semestre. La balise <tt>{edt_id}</tt> sera remplacée par l'edt_id du
semestre (par défaut, son code étape Apogée).
Si ce champ n'est pas renseigné, les emplois du temps ne seront pas utilisés.""",
validators=[Optional(), check_ics_path],
)
submit = SubmitField("Valider") submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -38,11 +38,17 @@ from app.models import ScoDocSiteConfig
def check_cas_uid_from_mail_regexp(form, field): def check_cas_uid_from_mail_regexp(form, field):
"Vérifie la regexp fournie pur l'extraction du CAS id" "Vérifie la regexp fournie pour l'extraction du CAS id"
if not ScoDocSiteConfig.cas_uid_from_mail_regexp_is_valid(field.data): if not ScoDocSiteConfig.cas_uid_from_mail_regexp_is_valid(field.data):
raise ValidationError("expression régulière invalide") raise ValidationError("expression régulière invalide")
def check_cas_edt_id_from_xml_regexp(form, field):
"Vérifie la regexp fournie pour l'extraction du CAS id"
if not ScoDocSiteConfig.cas_edt_id_from_xml_regexp_is_valid(field.data):
raise ValidationError("expression régulière pour edt_id invalide")
class ConfigCASForm(FlaskForm): class ConfigCASForm(FlaskForm):
"Formulaire paramétrage CAS" "Formulaire paramétrage CAS"
cas_enable = BooleanField("Activer le CAS") cas_enable = BooleanField("Activer le CAS")
@ -58,18 +64,18 @@ class ConfigCASForm(FlaskForm):
description="""url complète. Commence en général par <tt>https://</tt>.""", description="""url complète. Commence en général par <tt>https://</tt>.""",
) )
cas_login_route = StringField( cas_login_route = StringField(
label="Route du login CAS", label="Optionnel: route du login CAS",
description="""ajouté à l'URL du serveur: exemple <tt>/cas</tt> description="""ajouté à l'URL du serveur: exemple <tt>/cas</tt>
(si commence par <tt>/</tt>, part de la racine)""", (si commence par <tt>/</tt>, part de la racine)""",
default="/cas", default="/cas",
) )
cas_logout_route = StringField( cas_logout_route = StringField(
label="Route du logout CAS", label="Optionnel: route du logout CAS",
description="""ajouté à l'URL du serveur: exemple <tt>/cas/logout</tt>""", description="""ajouté à l'URL du serveur: exemple <tt>/cas/logout</tt>""",
default="/cas/logout", default="/cas/logout",
) )
cas_validate_route = StringField( cas_validate_route = StringField(
label="Route de validation CAS", label="Optionnel: route de validation CAS",
description="""ajouté à l'URL du serveur: exemple <tt>/cas/serviceValidate</tt>""", description="""ajouté à l'URL du serveur: exemple <tt>/cas/serviceValidate</tt>""",
default="/cas/serviceValidate", default="/cas/serviceValidate",
) )
@ -81,7 +87,7 @@ class ConfigCASForm(FlaskForm):
) )
cas_uid_from_mail_regexp = StringField( cas_uid_from_mail_regexp = StringField(
label="Expression pour extraire l'identifiant utilisateur", label="Optionnel: expression pour extraire l'identifiant utilisateur",
description="""regexp python appliquée au mail institutionnel de l'utilisateur, description="""regexp python appliquée au mail institutionnel de l'utilisateur,
dont le premier groupe doit donner l'identifiant CAS. dont le premier groupe doit donner l'identifiant CAS.
Si non fournie, le super-admin devra saisir cet identifiant pour chaque compte. Si non fournie, le super-admin devra saisir cet identifiant pour chaque compte.
@ -92,6 +98,17 @@ class ConfigCASForm(FlaskForm):
validators=[Optional(), check_cas_uid_from_mail_regexp], validators=[Optional(), check_cas_uid_from_mail_regexp],
) )
cas_edt_id_from_xml_regexp = StringField(
label="Optionnel: expression pour extraire l'identifiant edt",
description="""regexp python appliquée à la réponse XML du serveur CAS pour
retrouver l'id de l'utilisateur sur le SI de l'institution, et notamment sur les
calendrier d'emploi du temps. Par exemple, si cet id est renvoyé dans le champ
<b>supannEmpId</b>, utiliser:
<tt>&lt;cas:supannEmpId&gt;(.*?)&lt;/cas:supannEmpId&gt;</tt>
""",
validators=[Optional(), check_cas_edt_id_from_xml_regexp],
)
cas_ssl_verify = BooleanField("Vérification du certificat SSL") cas_ssl_verify = BooleanField("Vérification du certificat SSL")
cas_ssl_certificate_file = FileField( cas_ssl_certificate_file = FileField(
label="Certificat (PEM)", label="Certificat (PEM)",

View File

@ -29,7 +29,7 @@ def PersonalizedLinksForm() -> _PersonalizedLinksForm:
F, F,
f"link_{idx}", f"link_{idx}",
StringField( StringField(
f"Titre", "Titre",
validators=[ validators=[
validators.Optional(), validators.Optional(),
validators.Length(min=1, max=80), validators.Length(min=1, max=80),
@ -42,7 +42,7 @@ def PersonalizedLinksForm() -> _PersonalizedLinksForm:
F, F,
f"link_url_{idx}", f"link_url_{idx}",
StringField( StringField(
f"URL", "URL",
description="adresse, incluant le http.", description="adresse, incluant le http.",
validators=[ validators=[
validators.Optional(), validators.Optional(),
@ -56,7 +56,7 @@ def PersonalizedLinksForm() -> _PersonalizedLinksForm:
F, F,
f"link_with_args_{idx}", f"link_with_args_{idx}",
BooleanField( BooleanField(
f"ajouter arguments", "ajouter arguments",
description="query string avec ids", description="query string avec ids",
), ),
) )

View File

@ -286,7 +286,7 @@ class ScoDocSiteConfig(db.Model):
@classmethod @classmethod
def set(cls, name: str, value: str) -> bool: def set(cls, name: str, value: str) -> bool:
"Set parameter, returns True if change. Commit session." "Set parameter, returns True if change. Commit session."
value_str = str(value or "") value_str = str(value or "").strip()
if (cls.get(name) or "") != value_str: if (cls.get(name) or "") != value_str:
cfg = ScoDocSiteConfig.query.filter_by(name=name).first() cfg = ScoDocSiteConfig.query.filter_by(name=name).first()
if cfg is None: if cfg is None:
@ -429,7 +429,17 @@ class ScoDocSiteConfig(db.Model):
return False return False
# and returns at least one group on a simple cannonical address # and returns at least one group on a simple cannonical address
match = pattern.search("emmanuel@exemple.fr") match = pattern.search("emmanuel@exemple.fr")
return len(match.groups()) > 0 return match is not None and len(match.groups()) > 0
@classmethod
def cas_edt_id_from_xml_regexp_is_valid(cls, exp: str) -> bool:
"True si l'expression régulière semble valide"
# check that it compiles
try:
_ = re.compile(exp)
except re.error:
return False
return True
@classmethod @classmethod
def assi_get_rounded_time(cls, label: str, default: str) -> float: def assi_get_rounded_time(cls, label: str, default: str) -> float:

View File

@ -42,9 +42,6 @@ class ModuleImpl(db.Model):
viewonly=True, viewonly=True,
) )
def __init__(self, **kwargs):
super(ModuleImpl, self).__init__(**kwargs)
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)}>"

View File

@ -74,7 +74,8 @@ _moduleEditor = ndb.EditableTable(
"semestre_id", "semestre_id",
"numero", "numero",
"code_apogee", "code_apogee",
"module_type" "module_type",
"edt_id",
#'ects' #'ects'
), ),
sortkey="numero, code, titre", sortkey="numero, code, titre",
@ -171,7 +172,7 @@ def do_module_delete(oid):
d'en créer une nouvelle version pour la modifier sans affecter d'en créer une nouvelle version pour la modifier sans affecter
les semestres déjà en place. les semestres déjà en place.
</p> </p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept, <a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a> formation_id=mod["formation_id"])}">reprendre</a>
""" """
raise ScoGenError(err_page) raise ScoGenError(err_page)
@ -645,7 +646,7 @@ def module_edit(
"title": "Code Apogée", "title": "Code Apogée",
"size": 25, "size": 25,
"explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP "explanation": """(optionnel) code élément pédagogique Apogée ou liste de codes ELP
séparés par des virgules (ce code est propre à chaque établissement, se rapprocher séparés par des virgules (ce code est propre à chaque établissement, se rapprocher
du référent Apogée). du référent Apogée).
""", """,
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN, "validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
@ -682,7 +683,7 @@ def module_edit(
] ]
+ ["-1"], + ["-1"],
"explanation": """Parcours dans lesquels est utilisé ce module.<br> "explanation": """Parcours dans lesquels est utilisé ce module.<br>
Attention: si le module ne doit pas avoir les mêmes coefficients suivant le parcours, Attention: si le module ne doit pas avoir les mêmes coefficients suivant le parcours,
il faut en créer plusieurs versions, car dans ScoDoc chaque module a ses coefficients.""", il faut en créer plusieurs versions, car dans ScoDoc chaque module a ses coefficients.""",
}, },
) )
@ -746,7 +747,7 @@ def module_edit(
"input_type": "separator", "input_type": "separator",
"title": f"""<span class="fontred">{scu.EMO_WARNING } "title": f"""<span class="fontred">{scu.EMO_WARNING }
Pas de parcours: Pas de parcours:
<a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation', <a class="stdlink" href="{ url_for('notes.refcomp_assoc_formation',
scodoc_dept=g.scodoc_dept, formation_id=formation.id) scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}">associer un référentiel de compétence</a> }">associer un référentiel de compétence</a>
</span>""", </span>""",

View File

@ -68,6 +68,7 @@ _formsemestreEditor = ndb.EditableTable(
"ens_can_edit_eval", "ens_can_edit_eval",
"elt_sem_apo", "elt_sem_apo",
"elt_annee_apo", "elt_annee_apo",
"edt_id",
), ),
filter_dept=True, filter_dept=True,
sortkey="date_debut", sortkey="date_debut",
@ -571,7 +572,7 @@ def view_formsemestre_by_etape(etape_apo=None, fmt="html"):
), ),
html_title=html_title, html_title=html_title,
html_next_section="""<form action="view_formsemestre_by_etape"> html_next_section="""<form action="view_formsemestre_by_etape">
Etape: <input name="etape_apo" type="text" size="8"></input> Etape: <input name="etape_apo" type="text" size="8"></input>
</form>""", </form>""",
) )
tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "") tab.base_url = "%s?etape_apo=%s" % (request.base_url, etape_apo or "")

View File

@ -40,6 +40,7 @@ from app.models import (
ModuleImpl, ModuleImpl,
Evaluation, Evaluation,
UniteEns, UniteEns,
ScoDocSiteConfig,
ScolarFormSemestreValidation, ScolarFormSemestreValidation,
ScolarAutorisationInscription, ScolarAutorisationInscription,
ApcValidationAnnee, ApcValidationAnnee,
@ -321,7 +322,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
"size": 40, "size": 40,
"title": "Nom de ce semestre", "title": "Nom de ce semestre",
"explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans "explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans
le titre: ils seront automatiquement ajoutés <input type="button" le titre: ils seront automatiquement ajoutés <input type="button"
value="remettre titre par défaut" onClick="document.tf.titre.value='{ value="remettre titre par défaut" onClick="document.tf.titre.value='{
_default_sem_title(formation)}';"/>""", _default_sem_title(formation)}';"/>""",
"allow_null": False, "allow_null": False,
@ -445,13 +446,25 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
}, },
) )
) )
if ScoDocSiteConfig.get("edt_ics_path"):
modform.append(
(
"edt_id",
{
"size": 32,
"title": "Identifiant EDT",
"explanation": "optionnel, identifiant sur le logiciel emploi du temps (par défaut, utilise la première étape Apogée).",
"allow_null": True,
},
)
)
if edit: if edit:
formtit = f""" formtit = f"""
<p><a class="stdlink" href="{url_for("notes.formsemestre_edit_uecoefs", <p><a class="stdlink" href="{url_for("notes.formsemestre_edit_uecoefs",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">Modifier les coefficients des UE capitalisées</a> }">Modifier les coefficients des UE capitalisées</a>
</p> </p>
<h3>Sélectionner les modules, leurs responsables et les étudiants <h3>Sélectionner les modules, leurs responsables et les étudiants
à inscrire:</h3> à inscrire:</h3>
""" """
else: else:
@ -510,7 +523,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{ {
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"title": "", "title": "",
"explanation": """Autoriser tous les enseignants associés "explanation": """Autoriser tous les enseignants associés
à un module à y créer des évaluations""", à un module à y créer des évaluations""",
}, },
), ),
@ -585,8 +598,8 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
{ {
"input_type": "separator", "input_type": "separator",
"title": f"""<span class="fontred">{scu.EMO_WARNING } "title": f"""<span class="fontred">{scu.EMO_WARNING }
Pas de parcours: Pas de parcours:
<a class="stdlink" href="{ url_for('notes.ue_table', <a class="stdlink" href="{ url_for('notes.ue_table',
scodoc_dept=g.scodoc_dept, formation_id=formation.id) scodoc_dept=g.scodoc_dept, formation_id=formation.id)
}">vérifier la formation</a> }">vérifier la formation</a>
</span>""", </span>""",
@ -784,7 +797,7 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
if tf[0] == 0 or msg: if tf[0] == 0 or msg:
return f"""<p>Formation <a class="discretelink" href="{ return f"""<p>Formation <a class="discretelink" href="{
url_for("notes.ue_table", scodoc_dept=g.scodoc_dept, url_for("notes.ue_table", scodoc_dept=g.scodoc_dept,
formation_id=formation.id) formation_id=formation.id)
}"><em>{formation.titre}</em> ({formation.acronyme}), version { }"><em>{formation.titre}</em> ({formation.acronyme}), version {
formation.version}, code {formation.formation_code}</a> formation.version}, code {formation.formation_code}</a>
@ -969,11 +982,11 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
</li> </li>
</ul></span> </ul></span>
</div> </div>
{"<p>Modification effectuée</p>" if ok {"<p>Modification effectuée</p>" if ok
else "<p>Modules non modifiés</p>" else "<p>Modules non modifiés</p>"
} }
<a class="stdlink" href="{ <a class="stdlink" href="{
url_for('notes.formsemestre_status', url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">retour au tableau de bord</a> }">retour au tableau de bord</a>
""" """
@ -1309,11 +1322,11 @@ def formsemestre_delete(formsemestre_id):
html_sco_header.html_sem_header("Suppression du semestre"), html_sco_header.html_sem_header("Suppression du semestre"),
"""<div class="ue_warning"><span>Attention !</span> """<div class="ue_warning"><span>Attention !</span>
<p class="help">A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement, <p class="help">A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
<b>un semestre ne doit jamais être supprimé</b> <b>un semestre ne doit jamais être supprimé</b>
(on perd la mémoire des notes et de tous les événements liés à ce semestre !). (on perd la mémoire des notes et de tous les événements liés à ce semestre !).
</p> </p>
<p class="help">Tous les modules de ce semestre seront supprimés. <p class="help">Tous les modules de ce semestre seront supprimés.
Ceci n'est possible que si : Ceci n'est possible que si :
</p> </p>
<ol> <ol>
@ -1497,24 +1510,24 @@ def do_formsemestre_delete(formsemestre_id):
req = "DELETE FROM sco_prefs WHERE formsemestre_id=%(formsemestre_id)s" req = "DELETE FROM sco_prefs WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id}) cursor.execute(req, {"formsemestre_id": formsemestre_id})
# --- Suppression des groupes et partitions # --- Suppression des groupes et partitions
req = """DELETE FROM group_membership req = """DELETE FROM group_membership
WHERE group_id IN WHERE group_id IN
(SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd (SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd
WHERE gm.group_id = gd.id AND gd.partition_id = p.id WHERE gm.group_id = gd.id AND gd.partition_id = p.id
AND p.formsemestre_id=%(formsemestre_id)s) AND p.formsemestre_id=%(formsemestre_id)s)
""" """
cursor.execute(req, {"formsemestre_id": formsemestre_id}) cursor.execute(req, {"formsemestre_id": formsemestre_id})
req = """DELETE FROM group_descr req = """DELETE FROM group_descr
WHERE id IN WHERE id IN
(SELECT gd.id FROM group_descr gd, partition p (SELECT gd.id FROM group_descr gd, partition p
WHERE gd.partition_id = p.id WHERE gd.partition_id = p.id
AND p.formsemestre_id=%(formsemestre_id)s) AND p.formsemestre_id=%(formsemestre_id)s)
""" """
cursor.execute(req, {"formsemestre_id": formsemestre_id}) cursor.execute(req, {"formsemestre_id": formsemestre_id})
req = "DELETE FROM partition WHERE formsemestre_id=%(formsemestre_id)s" req = "DELETE FROM partition WHERE formsemestre_id=%(formsemestre_id)s"
cursor.execute(req, {"formsemestre_id": formsemestre_id}) cursor.execute(req, {"formsemestre_id": formsemestre_id})
# --- Responsables # --- Responsables
req = """DELETE FROM notes_formsemestre_responsables req = """DELETE FROM notes_formsemestre_responsables
WHERE formsemestre_id=%(formsemestre_id)s""" WHERE formsemestre_id=%(formsemestre_id)s"""
cursor.execute(req, {"formsemestre_id": formsemestre_id}) cursor.execute(req, {"formsemestre_id": formsemestre_id})
# --- Etapes # --- Etapes
@ -1606,7 +1619,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
Indiquez "auto" (ou laisser vide) pour que ScoDoc calcule automatiquement le coefficient, Indiquez "auto" (ou laisser vide) pour que ScoDoc calcule automatiquement le coefficient,
ou bien entrez une valeur (nombre réel). ou bien entrez une valeur (nombre réel).
</p> </p>
<p class="help">Dans le doute, si le mode auto n'est pas applicable et que tous les étudiants sont inscrits aux mêmes modules de ce semestre, prenez comme coefficient la somme indiquée. <p class="help">Dans le doute, si le mode auto n'est pas applicable et que tous les étudiants sont inscrits aux mêmes modules de ce semestre, prenez comme coefficient la somme indiquée.
Sinon, référez vous au programme pédagogique. Les lignes en <font color="red">rouge</font> Sinon, référez vous au programme pédagogique. Les lignes en <font color="red">rouge</font>
sont à changer. sont à changer.
</p> </p>
@ -1734,7 +1747,7 @@ def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
return f"""{html_sco_header.html_sem_header("Coefficients des UE du semestre")} return f"""{html_sco_header.html_sem_header("Coefficients des UE du semestre")}
{" ".join(message)} {" ".join(message)}
<p><a class="stdlink" href="{url_for("notes.formsemestre_status", <p><a class="stdlink" href="{url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id)
}">Revenir au tableau de bord</a> }">Revenir au tableau de bord</a>
</p> </p>

View File

@ -79,7 +79,9 @@ partitionEditor = ndb.EditableTable(
) )
groupEditor = ndb.EditableTable( groupEditor = ndb.EditableTable(
"group_descr", "group_id", ("group_id", "partition_id", "group_name", "numero") "group_descr",
"group_id",
("group_id", "partition_id", "group_name", "numero", "edt_id"),
) )
group_list = groupEditor.list group_list = groupEditor.list

View File

@ -1408,7 +1408,7 @@ function editAssiduite(assiduite_id, etat, assi) {
} }
/** /**
* Récupération des assiduités conflictuelles avec la période de la time line * Récupération des assiduités conflictuelles avec la période de la timeline
* @param {String | Number} etudid identifiant de l'étudiant * @param {String | Number} etudid identifiant de l'étudiant
* @returns {Array[Assiduité]} un tableau d'assiduité * @returns {Array[Assiduité]} un tableau d'assiduité
*/ */
@ -1685,17 +1685,17 @@ function generateEtudRow(
const HTML = `<div class="etud_row ${conflit} ${defdem}" id="etud_row_${ const HTML = `<div class="etud_row ${conflit} ${defdem}" id="etud_row_${
etud.id etud.id
}"> }">
<div class="index">${index}</div> <div class="index">${index}</div>
<div class="name_field"> <div class="name_field">
<img class="pdp" src="${pdp_url}"> <img class="pdp" src="${pdp_url}">
<a class="name_set" href="BilanEtud?etudid=${etud.id}"> <a class="name_set" href="BilanEtud?etudid=${etud.id}">
<h4 class="nom">${etud.nom}</h4> <h4 class="nom">${etud.nom}</h4>
<h5 class="prenom">${etud.prenom}</h5> <h5 class="prenom">${etud.prenom}</h5>
</a> </a>
</div> </div>
<div class="assiduites_bar"> <div class="assiduites_bar">
<div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}"> <div id="prevDateAssi" class="${assiduite.prevAssiduites?.etat?.toLowerCase()}">
@ -1704,12 +1704,12 @@ function generateEtudRow(
<fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${ <fieldset class="btns_field single" etudid="${etud.id}" assiduite_id="${
assiduite.id assiduite.id
}" type="${assiduite.type}"> }" type="${assiduite.type}">
${assi} ${assi}
</fieldset> </fieldset>
</div>`; </div>`;
return HTML; return HTML;

View File

@ -13,26 +13,36 @@ affectent notamment les comptages d'absences de tous les bulletins des
</div> </div>
<div class="row"> <form class="form form-horizontal" method="post" enctype="multipart/form-data" role="form">
<div class="col-md-8"> <div class="row">
<div class="col-md-8">
{{ form.hidden_tag() }}
{{ wtf.form_errors(form, hiddens="only") }}
<form class="form form-horizontal" method="post" enctype="multipart/form-data" role="form"> {{ wtf.form_field(form.morning_time) }}
{{ form.hidden_tag() }} {{ wtf.form_field(form.lunch_time) }}
{{ wtf.form_errors(form, hiddens="only") }} {{ wtf.form_field(form.afternoon_time) }}
{{ wtf.form_field(form.tick_time) }}
{{ wtf.form_field(form.morning_time) }} </div>
{{ wtf.form_field(form.lunch_time) }}
{{ wtf.form_field(form.afternoon_time) }}
{{ wtf.form_field(form.tick_time) }}
<div class="form-group">
{{ wtf.form_field(form.submit) }}
{{ wtf.form_field(form.cancel) }}
</div>
</form>
</div> </div>
</div> <div class="row">
<h1>Emplois du temps</h1>
<div class="help">ScoDoc peut récupérer les emplois du temps de chaque session.</div>
<div class="col-md-8">
<div class="config-edt">
{{ wtf.form_field(form.edt_ics_path) }}
</div>
<div class="form-group">
{{ wtf.form_field(form.submit) }}
{{ wtf.form_field(form.cancel) }}
</div>
</div>
</div>
</form>
{% endblock %}
{% endblock %}

View File

@ -24,6 +24,7 @@
{{ wtf.form_field(form.cas_validate_route) }} {{ wtf.form_field(form.cas_validate_route) }}
{{ wtf.form_field(form.cas_attribute_id) }} {{ wtf.form_field(form.cas_attribute_id) }}
{{ wtf.form_field(form.cas_uid_from_mail_regexp) }} {{ wtf.form_field(form.cas_uid_from_mail_regexp) }}
{{ wtf.form_field(form.cas_edt_id_from_xml_regexp) }}
<div class="cas_settings"> <div class="cas_settings">
{{ wtf.form_field(form.cas_ssl_verify) }} {{ wtf.form_field(form.cas_ssl_verify) }}
{{ wtf.form_field(form.cas_ssl_certificate_file) }} {{ wtf.form_field(form.cas_ssl_certificate_file) }}
@ -40,8 +41,8 @@
{{ wtf.form_field(form.cancel) }} {{ wtf.form_field(form.cancel) }}
</div> </div>
<div class="form-group" style="margin-top:16px;"> <div class="form-group" style="margin-top:16px;">
<em>Note: si le CAS est forcé, le super-admin et les utilisateurs autorisés <em>Note: si le CAS est forcé, le super-admin et les utilisateurs autorisés
à "se connecter via ScoDoc" pourront toujours se à "se connecter via ScoDoc" pourront toujours se
connecter via l'adresse spéciale</em> connecter via l'adresse spéciale</em>
<tt style="color: blue;">{{url_for("auth.login_scodoc", _external=True)}}</tt> <tt style="color: blue;">{{url_for("auth.login_scodoc", _external=True)}}</tt>
</div> </div>
@ -50,6 +51,6 @@
</div> </div>
{% endblock %}
{% endblock %}

View File

@ -73,8 +73,8 @@ Heure: <b><tt>{{ time.strftime("%d/%m/%Y %H:%M") }}</tt></b>
</p> </p>
</section> </section>
<section> <section>
<h2>Assiduité</h2> <h2>Assiduité et emplois du temps</h2>
<p><a class="stdlink" href="{{url_for('scodoc.config_assiduites')}}">Configuration du suivi de l'assiduité</a> <p><a class="stdlink" href="{{url_for('scodoc.config_assiduites')}}">Configuration du suivi de l'assiduité et accès aux emplois du temps</a>
</p> </p>
</section> </section>

View File

@ -273,6 +273,10 @@ def config_cas():
"cas_uid_from_mail_regexp", form.data["cas_uid_from_mail_regexp"] "cas_uid_from_mail_regexp", form.data["cas_uid_from_mail_regexp"]
): ):
flash("Expression extraction identifiant CAS enregistrée") flash("Expression extraction identifiant CAS enregistrée")
if ScoDocSiteConfig.set(
"cas_edt_id_from_xml_regexp", form.data["cas_edt_id_from_xml_regexp"]
):
flash("Expression extraction identifiant edt enregistrée")
if ScoDocSiteConfig.set("cas_ssl_verify", form.data["cas_ssl_verify"]): if ScoDocSiteConfig.set("cas_ssl_verify", form.data["cas_ssl_verify"]):
flash("Vérification SSL modifiée") flash("Vérification SSL modifiée")
if form.cas_ssl_certificate_file.data: if form.cas_ssl_certificate_file.data:
@ -300,6 +304,9 @@ def config_cas():
form.cas_uid_from_mail_regexp.data = ScoDocSiteConfig.get( form.cas_uid_from_mail_regexp.data = ScoDocSiteConfig.get(
"cas_uid_from_mail_regexp" "cas_uid_from_mail_regexp"
) )
form.cas_edt_id_from_xml_regexp.data = ScoDocSiteConfig.get(
"cas_edt_id_from_xml_regexp"
)
form.cas_ssl_verify.data = ScoDocSiteConfig.get("cas_ssl_verify") form.cas_ssl_verify.data = ScoDocSiteConfig.get("cas_ssl_verify")
return render_template( return render_template(
"config_cas.j2", "config_cas.j2",
@ -316,6 +323,7 @@ def config_assiduites():
form = ConfigAssiduitesForm() form = ConfigAssiduitesForm()
if request.method == "POST" and form.cancel.data: # cancel button if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.index")) return redirect(url_for("scodoc.index"))
if form.validate_on_submit(): if form.validate_on_submit():
if ScoDocSiteConfig.set("assi_morning_time", form.data["morning_time"]): if ScoDocSiteConfig.set("assi_morning_time", form.data["morning_time"]):
flash("Heure du début de la journée enregistrée") flash("Heure du début de la journée enregistrée")
@ -323,18 +331,13 @@ def config_assiduites():
flash("Heure de midi enregistrée") flash("Heure de midi enregistrée")
if ScoDocSiteConfig.set("assi_afternoon_time", form.data["afternoon_time"]): if ScoDocSiteConfig.set("assi_afternoon_time", form.data["afternoon_time"]):
flash("Heure de fin de la journée enregistrée") flash("Heure de fin de la journée enregistrée")
if ( if ScoDocSiteConfig.set("assi_tick_time", float(form.data["tick_time"])):
form.data["tick_time"] > 0
and form.data["tick_time"] < 60
and ScoDocSiteConfig.set("assi_tick_time", float(form.data["tick_time"]))
):
flash("Granularité de la timeline enregistrée") flash("Granularité de la timeline enregistrée")
else: if ScoDocSiteConfig.set("edt_ics_path", form.data["edt_ics_path"]):
flash("Erreur : Granularité invalide ou identique") flash("Chemin vers les calendriers ics enregistré")
return redirect(url_for("scodoc.configuration")) return redirect(url_for("scodoc.configuration"))
elif request.method == "GET": if request.method == "GET":
form.morning_time.data = ScoDocSiteConfig.get( form.morning_time.data = ScoDocSiteConfig.get(
"assi_morning_time", datetime.time(8, 0, 0) "assi_morning_time", datetime.time(8, 0, 0)
) )
@ -349,12 +352,13 @@ def config_assiduites():
except ValueError: except ValueError:
form.tick_time.data = 15.0 form.tick_time.data = 15.0
ScoDocSiteConfig.set("assi_tick_time", 15.0) ScoDocSiteConfig.set("assi_tick_time", 15.0)
form.edt_ics_path.data = ScoDocSiteConfig.get("edt_ics_path")
return render_template( return render_template(
"assiduites/pages/config_assiduites.j2", "assiduites/pages/config_assiduites.j2",
form=form, form=form,
title="Configuration du module Assiduité", title="Configuration du module Assiduité",
) )
@bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"]) @bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"])