EDT: ajout des edt_id dans formsemestre, groupes, modules, users

This commit is contained in:
Emmanuel Viennet 2023-11-06 22:05:38 +01:00
parent 5824b7fb59
commit e71e4b27ec
15 changed files with 259 additions and 148 deletions

View File

@ -12,6 +12,7 @@ from typing import Optional
import cracklib # pylint: disable=import-error import cracklib # pylint: disable=import-error
import flask
from flask import current_app, g from flask import current_app, g
from flask_login import UserMixin, AnonymousUserMixin from flask_login import UserMixin, AnonymousUserMixin
@ -88,7 +89,8 @@ class User(UserMixin, db.Model):
""" """
cas_last_login = db.Column(db.DateTime, nullable=True) cas_last_login = db.Column(db.DateTime, nullable=True)
"""date du dernier login via CAS""" """date du dernier login via CAS"""
edt_id = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
password_hash = db.Column(db.String(128)) password_hash = db.Column(db.String(128))
password_scodoc7 = db.Column(db.String(42)) password_scodoc7 = db.Column(db.String(42))
last_seen = db.Column(db.DateTime, default=datetime.utcnow) last_seen = db.Column(db.DateTime, default=datetime.utcnow)
@ -172,7 +174,8 @@ class User(UserMixin, db.Model):
return False return False
# if CAS activated and forced, allow only super-user and users with cas_allow_scodoc_login # if CAS activated and forced, allow only super-user and users with cas_allow_scodoc_login
if ScoDocSiteConfig.is_cas_enabled() and ScoDocSiteConfig.get("cas_force"): cas_enabled = ScoDocSiteConfig.is_cas_enabled()
if cas_enabled and ScoDocSiteConfig.get("cas_force"):
if (not self.is_administrator()) and not self.cas_allow_scodoc_login: if (not self.is_administrator()) and not self.cas_allow_scodoc_login:
return False return False
@ -182,7 +185,18 @@ class User(UserMixin, db.Model):
return self._migrate_scodoc7_password(password) return self._migrate_scodoc7_password(password)
return False return False
return check_password_hash(self.password_hash, password) password_ok = 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

@ -4,8 +4,8 @@
""" """
import json import json
import urllib.parse
import re import re
import urllib.parse
from flask import flash from flask import flash
from app import current_app, db, log from app import current_app, db, log
@ -13,8 +13,6 @@ from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from datetime import time
from app.scodoc.codes_cursus import ( from app.scodoc.codes_cursus import (
ABAN, ABAN,
ABL, ABL,
@ -105,6 +103,7 @@ class ScoDocSiteConfig(db.Model):
"cas_validate_route": str, "cas_validate_route": str,
"cas_attribute_id": str, "cas_attribute_id": str,
"cas_uid_from_mail_regexp": str, "cas_uid_from_mail_regexp": str,
"cas_edt_id_from_xml_regexp": str,
# Assiduité # Assiduité
"morning_time": str, "morning_time": str,
"lunch_time": str, "lunch_time": str,
@ -174,7 +173,7 @@ class ScoDocSiteConfig(db.Model):
klass = bonus_spo.get_bonus_class_dict().get(class_name) klass = bonus_spo.get_bonus_class_dict().get(class_name)
if klass is None: if klass is None:
flash( flash(
f"""Fonction de calcul bonus sport inexistante: {class_name}. f"""Fonction de calcul bonus sport inexistante: {class_name}.
Changez ou contactez votre administrateur local.""" Changez ou contactez votre administrateur local."""
) )
return klass return klass

View File

@ -64,6 +64,8 @@ class FormSemestre(db.Model):
titre = db.Column(db.Text(), nullable=False) titre = db.Column(db.Text(), nullable=False)
date_debut = db.Column(db.Date(), nullable=False) date_debut = db.Column(db.Date(), nullable=False)
date_fin = db.Column(db.Date(), nullable=False) date_fin = db.Column(db.Date(), nullable=False)
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true") etat = db.Column(db.Boolean(), nullable=False, default=True, server_default="true")
"False si verrouillé" "False si verrouillé"
modalite = db.Column( modalite = db.Column(

View File

@ -180,7 +180,7 @@ class Partition(db.Model):
"Crée un groupe dans cette partition" "Crée un groupe dans cette partition"
if not self.formsemestre.can_change_groups(): if not self.formsemestre.can_change_groups():
raise AccessDenied( raise AccessDenied(
"""Vous n'avez pas le droit d'effectuer cette opération, """Vous n'avez pas le droit d'effectuer cette opération,
ou bien le semestre est verrouillé !""" ou bien le semestre est verrouillé !"""
) )
if group_name: if group_name:
@ -213,10 +213,12 @@ class GroupDescr(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
group_id = db.synonym("id") group_id = db.synonym("id")
partition_id = db.Column(db.Integer, db.ForeignKey("partition.id")) partition_id = db.Column(db.Integer, db.ForeignKey("partition.id"))
# "A", "C2", ... (NULL for 'all'):
group_name = db.Column(db.String(GROUPNAME_STR_LEN)) group_name = db.Column(db.String(GROUPNAME_STR_LEN))
# Numero = ordre de presentation """nom du groupe: "A", "C2", ... (NULL for 'all')"""
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
numero = db.Column(db.Integer, nullable=False, default=0) numero = db.Column(db.Integer, nullable=False, default=0)
"Numero = ordre de presentation"
etuds = db.relationship( etuds = db.relationship(
"Identite", "Identite",
@ -272,6 +274,40 @@ class GroupDescr(db.Model):
return False return False
return True return True
def set_name(
self, group_name: str, edt_id: str | bool = False, dest_url: str = None
):
"""Set group name, and optionally edt_id.
Check permission and invalidate caches. Commit session.
dest_url is used for error messages.
"""
if not self.partition.formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if self.group_name is None:
raise ValueError("can't set a name to default group")
if group_name:
group_name = group_name.strip()
if not group_name:
raise ScoValueError("nom de groupe vide !", dest_url=dest_url)
if group_name != self.group_name and not GroupDescr.check_name(
self.partition, group_name
):
raise ScoValueError(
"Le nom de groupe existe déjà dans la partition", dest_url=dest_url
)
self.group_name = group_name
if edt_id is not False:
if isinstance(edt_id, str):
edt_id = edt_id.strip() or None
self.edt_id = edt_id
db.session.add(self)
db.session.commit()
sco_cache.invalidate_formsemestre(
formsemestre_id=self.partition.formsemestre_id
)
def remove_etud(self, etud: "Identite"): def remove_etud(self, etud: "Identite"):
"Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)" "Enlève l'étudiant de ce groupe s'il en fait partie (ne fait rien sinon)"
if etud in self.etuds: if etud in self.etuds:

View File

@ -34,8 +34,10 @@ class Module(db.Model):
# note: en APC, le semestre qui fait autorité est celui de l'UE # note: en APC, le semestre qui fait autorité est celui de l'UE
semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1") semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1")
numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation numero = db.Column(db.Integer, nullable=False, default=0) # ordre de présentation
# id de l'element pedagogique Apogee correspondant:
code_apogee = db.Column(db.String(APO_CODE_STR_LEN)) code_apogee = db.Column(db.String(APO_CODE_STR_LEN))
"id de l'element pedagogique Apogee correspondant"
edt_id: str | None = db.Column(db.Text(), index=True, nullable=True)
"identifiant emplois du temps (unicité non imposée)"
# Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum) # Type: ModuleType.STANDARD, MALUS, RESSOURCE, SAE (enum)
module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0") module_type = db.Column(db.Integer, nullable=False, default=0, server_default="0")
# Relations: # Relations:

View File

@ -34,16 +34,13 @@ XXX incompatible avec les ics HyperPlanning Paris 13 (était pour GPU).
""" """
import icalendar import icalendar
import pprint
import traceback
import urllib import urllib
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc import html_sco_header
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_preferences from app.scodoc import sco_preferences

View File

@ -42,7 +42,7 @@ from app import cache, db, log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre, Identite, Scolog from app.models import FormSemestre, Identite, Scolog
from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.groups import GroupDescr, Partition from app.models.groups import GroupDescr, Partition
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -136,7 +136,7 @@ def get_partitions_list(formsemestre_id, with_default=True) -> list[dict]:
partitions = ndb.SimpleDictFetch( partitions = ndb.SimpleDictFetch(
"""SELECT p.id AS partition_id, p.* """SELECT p.id AS partition_id, p.*
FROM partition p FROM partition p
WHERE formsemestre_id=%(formsemestre_id)s WHERE formsemestre_id=%(formsemestre_id)s
ORDER BY numero""", ORDER BY numero""",
{"formsemestre_id": formsemestre_id}, {"formsemestre_id": formsemestre_id},
) )
@ -258,14 +258,14 @@ def get_group_members(group_id, etat=None):
Trié par nom_usuel (ou nom) puis prénom Trié par nom_usuel (ou nom) puis prénom
""" """
req = """SELECT i.id as etudid, i.*, a.*, gm.*, ins.etat req = """SELECT i.id as etudid, i.*, a.*, gm.*, ins.etat
FROM identite i, adresse a, group_membership gm, FROM identite i, adresse a, group_membership gm,
group_descr gd, partition p, notes_formsemestre_inscription ins group_descr gd, partition p, notes_formsemestre_inscription ins
WHERE i.id = gm.etudid WHERE i.id = gm.etudid
and a.etudid = i.id and a.etudid = i.id
and ins.etudid = i.id and ins.etudid = i.id
and ins.formsemestre_id = p.formsemestre_id and ins.formsemestre_id = p.formsemestre_id
and p.id = gd.partition_id and p.id = gd.partition_id
and gd.id = gm.group_id and gd.id = gm.group_id
and gm.group_id=%(group_id)s and gm.group_id=%(group_id)s
""" """
if etat is not None: if etat is not None:
@ -350,12 +350,12 @@ def get_etud_groups(etudid: int, formsemestre_id: int, exclude_default=False):
"""Infos sur groupes de l'etudiant dans ce semestre """Infos sur groupes de l'etudiant dans ce semestre
[ group + partition_name ] [ group + partition_name ]
""" """
req = """SELECT p.id AS partition_id, p.*, req = """SELECT p.id AS partition_id, p.*,
g.id AS group_id, g.numero as group_numero, g.group_name g.id AS group_id, g.numero as group_numero, g.group_name
FROM group_descr g, partition p, group_membership gm FROM group_descr g, partition p, group_membership gm
WHERE gm.etudid=%(etudid)s WHERE gm.etudid=%(etudid)s
and gm.group_id = g.id and gm.group_id = g.id
and g.partition_id = p.id and g.partition_id = p.id
and p.formsemestre_id = %(formsemestre_id)s and p.formsemestre_id = %(formsemestre_id)s
""" """
if exclude_default: if exclude_default:
@ -393,7 +393,7 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
p.id AS partition_id, p.id AS partition_id,
gd.group_name, gd.group_name,
gd.id AS group_id gd.id AS group_id
FROM FROM
notes_formsemestre_inscription i, notes_formsemestre_inscription i,
partition p, partition p,
group_descr gd, group_descr gd,
@ -967,8 +967,8 @@ def edit_partition_form(formsemestre_id=None):
for p in partitions: for p in partitions:
if p["partition_name"] is not None: if p["partition_name"] is not None:
H.append( H.append(
f"""<tr><td class="epnav"><a class="stdlink" f"""<tr><td class="epnav"><a class="stdlink"
href="{url_for("scolar.partition_delete", href="{url_for("scolar.partition_delete",
scodoc_dept=g.scodoc_dept, partition_id=p["partition_id"]) scodoc_dept=g.scodoc_dept, partition_id=p["partition_id"])
}">{suppricon}</a>&nbsp;</td><td class="epnav">""" }">{suppricon}</a>&nbsp;</td><td class="epnav">"""
) )
@ -1299,85 +1299,6 @@ def partition_set_name(partition_id, partition_name, redirect=1):
) )
def group_set_name(group: GroupDescr, group_name: str, redirect=True):
"""Set group name"""
if not group.partition.formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if group.group_name is None:
raise ValueError("can't set a name to default group")
destination = url_for(
"scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=group.partition_id,
)
if group_name:
group_name = group_name.strip()
if not group_name:
raise ScoValueError("nom de groupe vide !", dest_url=destination)
if not GroupDescr.check_name(group.partition, group_name):
raise ScoValueError(
"Le nom de groupe existe déjà dans la partition", dest_url=destination
)
redirect = int(redirect)
group.group_name = group_name
db.session.add(group)
db.session.commit()
sco_cache.invalidate_formsemestre(formsemestre_id=group.partition.formsemestre_id)
# redirect to partition edit page:
if redirect:
return flask.redirect(destination)
def group_rename(group_id):
"""Form to rename a group"""
group = GroupDescr.query.get_or_404(group_id)
formsemestre_id = group.partition.formsemestre_id
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = [f"<h2>Renommer un groupe de {group.partition.partition_name or '-'}</h2>"]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("group_id", {"default": group_id, "input_type": "hidden"}),
(
"group_name",
{
"title": "Nouveau nom",
"default": group.group_name,
"size": 12,
"allow_null": False,
"validator": lambda val, _: len(val) < GROUPNAME_STR_LEN,
},
),
),
submitlabel="Renommer",
cancelbutton="Annuler",
)
if tf[0] == 0:
return (
html_sco_header.sco_header()
+ "\n".join(H)
+ "\n"
+ tf[1]
+ html_sco_header.sco_footer()
)
elif tf[0] == -1:
return flask.redirect(
url_for(
"scolar.affect_groups",
scodoc_dept=g.scodoc_dept,
partition_id=group.partition_id,
)
)
else:
# form submission
return group_set_name(group, tf[2]["group_name"])
def groups_auto_repartition(partition: Partition): def groups_auto_repartition(partition: Partition):
"""Réparti les etudiants dans des groupes dans une partition, en respectant le niveau """Réparti les etudiants dans des groupes dans une partition, en respectant le niveau
et la mixité. et la mixité.
@ -1570,7 +1491,7 @@ def do_evaluation_listeetuds_groups(
return [] # no groups, so no students return [] # no groups, so no students
rg = ["gm.group_id = '%(group_id)s'" % g for g in groups] rg = ["gm.group_id = '%(group_id)s'" % g for g in groups]
rq = """and Isem.etudid = gm.etudid rq = """and Isem.etudid = gm.etudid
and gd.partition_id = p.id and gd.partition_id = p.id
and p.formsemestre_id = Isem.formsemestre_id and p.formsemestre_id = Isem.formsemestre_id
""" """
r = rq + " AND (" + " or ".join(rg) + " )" r = rq + " AND (" + " or ".join(rg) + " )"
@ -1583,9 +1504,9 @@ def do_evaluation_listeetuds_groups(
"SELECT distinct Im.etudid, Isem.etat FROM " "SELECT distinct Im.etudid, Isem.etat FROM "
+ ", ".join(fromtables) + ", ".join(fromtables)
+ """ WHERE Isem.etudid = Im.etudid + """ WHERE Isem.etudid = Im.etudid
and Im.moduleimpl_id = M.id and Im.moduleimpl_id = M.id
and Isem.formsemestre_id = M.formsemestre_id and Isem.formsemestre_id = M.formsemestre_id
and E.moduleimpl_id = M.id and E.moduleimpl_id = M.id
and E.id = %(evaluation_id)s and E.id = %(evaluation_id)s
""" """
) )
@ -1612,7 +1533,7 @@ def do_evaluation_listegroupes(evaluation_id, include_default=False):
cursor = cnx.cursor() cursor = cnx.cursor()
cursor.execute( cursor.execute(
"""SELECT DISTINCT gd.id AS group_id """SELECT DISTINCT gd.id AS group_id
FROM group_descr gd, group_membership gm, partition p, FROM group_descr gd, group_membership gm, partition p,
notes_moduleimpl m, notes_evaluation e notes_moduleimpl m, notes_evaluation e
WHERE gm.group_id = gd.id WHERE gm.group_id = gd.id
and gd.partition_id = p.id and gd.partition_id = p.id

View File

@ -27,11 +27,15 @@
"""Formulaires gestion des groupes """Formulaires gestion des groupes
""" """
from flask import render_template import flask
from flask import flash, g, render_template, request, url_for
from app.models import Partition from app.models import FormSemestre, GroupDescr, Partition
from app.models import GROUPNAME_STR_LEN
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_exceptions import AccessDenied
import app.scodoc.sco_utils as scu
from app.scodoc.TrivialFormulator import TrivialFormulator
def affect_groups(partition_id): def affect_groups(partition_id):
@ -59,3 +63,64 @@ def affect_groups(partition_id):
), ),
formsemestre_id=formsemestre.id, formsemestre_id=formsemestre.id,
) )
def group_rename(group_id):
"""Form to rename a group"""
group: GroupDescr = GroupDescr.query.get_or_404(group_id)
formsemestre_id = group.partition.formsemestre_id
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if not formsemestre.can_change_groups():
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
H = [f"<h2>Renommer un groupe de {group.partition.partition_name or '-'}</h2>"]
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
(
("group_id", {"default": group_id, "input_type": "hidden"}),
(
"group_name",
{
"title": "Nouveau nom",
"default": group.group_name,
"size": 12,
"allow_null": False,
"validator": lambda val, _: len(val) < GROUPNAME_STR_LEN,
"explanation": "doit être unique dans cette partition",
},
),
(
"edt_id",
{
"title": "Id EDT",
"default": group.edt_id or "",
"size": 12,
"allow_null": True,
"explanation": "optionnel : identifiant du groupe dans le logiciel d'emploi du temps",
},
),
),
submitlabel="Renommer",
cancelbutton="Annuler",
)
dest_url = url_for(
"scolar.partition_editor",
scodoc_dept=g.scodoc_dept,
formsemestre_id=group.partition.formsemestre_id,
edit_partition=1,
)
if tf[0] == 0:
return (
html_sco_header.sco_header()
+ "\n".join(H)
+ "\n"
+ tf[1]
+ html_sco_header.sco_footer()
)
elif tf[0] == -1:
return flask.redirect(dest_url)
else:
# form submission
group.set_name(tf[2]["group_name"], edt_id=tf[2]["edt_id"], dest_url=dest_url)
flash("groupe modifié")
return flask.redirect(dest_url)

View File

@ -31,11 +31,8 @@
# Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code) # Re-ecriture en 2014 (re-organisation de l'interface, modernisation du code)
import collections
import datetime import datetime
import urllib
from urllib.parse import parse_qs from urllib.parse import parse_qs
import time
from flask import url_for, g, request from flask import url_for, g, request
@ -45,7 +42,6 @@ from app import db
from app.models import FormSemestre from app.models import FormSemestre
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cal
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups

View File

@ -302,6 +302,10 @@ body.editionActivated .filtres>div>div>div>div {
display: none; display: none;
} }
#zonePartitions span.editing a {
text-decoration: none;
}
.editionActivated #zonePartitions .filtres .config { .editionActivated #zonePartitions .filtres .config {
display: block; display: block;
} }
@ -598,4 +602,4 @@ h3 {
#zoneGroupes .groupe[data-idgroupe=aucun]>div:nth-child(1) { #zoneGroupes .groupe[data-idgroupe=aucun]>div:nth-child(1) {
color: red; color: red;
} }

View File

@ -1327,7 +1327,7 @@ table.gt_table tr.etuddem td a {
table.gt_table tr.etuddem td.etudinfo:first-child::after { table.gt_table tr.etuddem td.etudinfo:first-child::after {
color: red; color: red;
content: " (dém.)"; content: " (dém.)";
} }
td.etudabs, td.etudabs,
td.etudabs a.discretelink, td.etudabs a.discretelink,
@ -3921,9 +3921,9 @@ div#update_warning>div:nth-child(2) {
padding-left: 8ex; padding-left: 8ex;
} }
/* /*
Titres des tabs: Titres des tabs:
.nav-tabs li a { .nav-tabs li a {
font-variant: small-caps; font-variant: small-caps;
font-size: 13pt; font-size: 13pt;
} }
@ -4354,7 +4354,7 @@ button.unselect {
/* Non supproté par les navigateurs (en Fev. 2023) /* Non supproté par les navigateurs (en Fev. 2023)
.table_recap button:has(span a.clearreaload) { .table_recap button:has(span a.clearreaload) {
} }
*/ */
div.table_recap table.table_recap, div.table_recap table.table_recap,
@ -4833,4 +4833,8 @@ div.cas_etat_certif_ssl {
margin-bottom: 8px; margin-bottom: 8px;
font-style: italic; font-style: italic;
color: rgb(231, 0, 0); color: rgb(231, 0, 0);
} }
.edt_id {
color: rgb(85, 255, 24);
}

View File

@ -8,7 +8,8 @@
<h2>Filtres</h2> <h2>Filtres</h2>
<div> <div>
<label class="edition"> <label class="edition">
<input type="checkbox" autocomplete="off" id="inputModif"> <input type="checkbox" autocomplete="off" id="inputModif"
{% if edit_partition %}checked{% endif %}>
Modifier les partitions et groupes Modifier les partitions et groupes
</label> </label>
<div class="filtres"></div> <div class="filtres"></div>
@ -212,7 +213,7 @@
</button> </button>
<div class="editing ajoutGroupe">+</div> <div class="editing ajoutGroupe">+</div>
</div> </div>
<!-- Config --> <!-- Config -->
<div class=config> <div class=config>
Configuration Configuration
@ -246,15 +247,15 @@
let div = document.createElement("button"); let div = document.createElement("button");
div.classList.add("dt-button"); div.classList.add("dt-button");
div.dataset.idgroupe = groupe.id; div.dataset.idgroupe = groupe.id;
let edt_id_str = groupe.edt_id ? `<tt class="edt_id" title="id edt">[${groupe.edt_id}]</tt>` : "";
div.innerHTML = ` div.innerHTML = `
<span class="editing move">||</span> <span class="editing move">||</span>
<span>${groupe.group_name}</span> <span>${groupe.group_name} ${edt_id_str}</span>
<span class="editing modif">✏️</span> <span class="editing"><a href="/ScoDoc/{{formsemestre.departement.acronym}}/Scolarite/group_rename?group_id=${groupe.id}">✏️</a></span>
<span class="editing suppr">❌</span>`; <span class="editing suppr">❌</span>`;
div.addEventListener("click", filtre); div.addEventListener("click", filtre);
div.querySelector(".move").addEventListener("mousedown", moveStart); div.querySelector(".move").addEventListener("mousedown", moveStart);
div.querySelector(".modif").addEventListener("click", editText);
div.querySelector(".suppr").addEventListener("click", suppr); div.querySelector(".suppr").addEventListener("click", suppr);
return div; return div;
@ -945,4 +946,4 @@
} }
</script> </script>

View File

@ -842,7 +842,7 @@ sco_publish("/setGroups", sco_groups.setGroups, Permission.ScoView, methods=["PO
sco_publish( sco_publish(
"/group_rename", "/group_rename",
sco_groups.group_rename, sco_groups_edit.group_rename,
Permission.ScoView, Permission.ScoView,
methods=["GET", "POST"], methods=["GET", "POST"],
) )

View File

@ -1,19 +1,21 @@
"""
Routes for CAS authentication
Modified for ScoDoc
"""
import re
import ssl import ssl
from urllib.error import URLError
from urllib.request import urlopen
import flask import flask
from xmltodict import parse
from flask import current_app from flask import current_app
from xmltodict import parse
from .cas_urls import create_cas_login_url from .cas_urls import create_cas_login_url
from .cas_urls import create_cas_logout_url from .cas_urls import create_cas_logout_url
from .cas_urls import create_cas_validate_url from .cas_urls import create_cas_validate_url
try:
from urllib import urlopen # python 2
except ImportError:
from urllib.request import urlopen # python 3
from urllib.error import URLError
blueprint = flask.Blueprint("cas", __name__) blueprint = flask.Blueprint("cas", __name__)
@ -53,7 +55,6 @@ def login():
flask.session[cas_token_session_key] = flask.request.args["ticket"] flask.session[cas_token_session_key] = flask.request.args["ticket"]
if cas_token_session_key in flask.session: if cas_token_session_key in flask.session:
if validate(flask.session[cas_token_session_key]): if validate(flask.session[cas_token_session_key]):
if "CAS_AFTER_LOGIN_SESSION_URL" in flask.session: if "CAS_AFTER_LOGIN_SESSION_URL" in flask.session:
redirect_url = flask.session.pop("CAS_AFTER_LOGIN_SESSION_URL") redirect_url = flask.session.pop("CAS_AFTER_LOGIN_SESSION_URL")
@ -64,7 +65,7 @@ def login():
else: else:
flask.session.pop(cas_token_session_key, None) flask.session.pop(cas_token_session_key, None)
current_app.logger.debug("Redirecting to: {redirect_url}") current_app.logger.debug(f"cas.login: redirecting to {redirect_url}")
return flask.redirect(redirect_url) return flask.redirect(redirect_url)
@ -84,6 +85,7 @@ def logout():
flask.session.pop(cas_username_session_key, None) flask.session.pop(cas_username_session_key, None)
flask.session.pop(cas_attributes_session_key, None) flask.session.pop(cas_attributes_session_key, None)
flask.session.pop(cas_token_session_key, None) # added by EV flask.session.pop(cas_token_session_key, None) # added by EV
flask.session.pop("CAS_EDT_ID", None) # added by EV
cas_after_logout = current_app.config["CAS_AFTER_LOGOUT"] cas_after_logout = current_app.config["CAS_AFTER_LOGOUT"]
if cas_after_logout is not None: if cas_after_logout is not None:
@ -102,7 +104,7 @@ def logout():
else: else:
redirect_url = create_cas_logout_url(current_app.config["CAS_SERVER"], None) redirect_url = create_cas_logout_url(current_app.config["CAS_SERVER"], None)
current_app.logger.debug(f"Redirecting to: {redirect_url}") current_app.logger.debug(f"cas.logout: redirecting to {redirect_url}")
return flask.redirect(redirect_url) return flask.redirect(redirect_url)
@ -114,11 +116,12 @@ def validate(ticket):
key `CAS_USERNAME_SESSION_KEY` while the validated attributes dictionary key `CAS_USERNAME_SESSION_KEY` while the validated attributes dictionary
is saved under the key 'CAS_ATTRIBUTES_SESSION_KEY'. is saved under the key 'CAS_ATTRIBUTES_SESSION_KEY'.
""" """
from app.models.config import ScoDocSiteConfig
cas_username_session_key = current_app.config["CAS_USERNAME_SESSION_KEY"] cas_username_session_key = current_app.config["CAS_USERNAME_SESSION_KEY"]
cas_attributes_session_key = current_app.config["CAS_ATTRIBUTES_SESSION_KEY"] cas_attributes_session_key = current_app.config["CAS_ATTRIBUTES_SESSION_KEY"]
cas_error_callback = current_app.config.get("CAS_ERROR_CALLBACK") cas_error_callback = current_app.config.get("CAS_ERROR_CALLBACK")
current_app.logger.debug("validating token {0}".format(ticket)) current_app.logger.debug(f"validating token {ticket}")
cas_validate_url = create_cas_validate_url( cas_validate_url = create_cas_validate_url(
current_app.config["CAS_SERVER"], current_app.config["CAS_SERVER"],
@ -182,7 +185,7 @@ def validate(ticket):
attributes = xml_from_dict.get("cas:attributes", {}) attributes = xml_from_dict.get("cas:attributes", {})
if attributes and "cas:memberOf" in attributes: if attributes and "cas:memberOf" in attributes:
if isinstance(attributes["cas:memberOf"], basestring): if isinstance(attributes["cas:memberOf"], str):
attributes["cas:memberOf"] = ( attributes["cas:memberOf"] = (
attributes["cas:memberOf"].lstrip("[").rstrip("]").split(",") attributes["cas:memberOf"].lstrip("[").rstrip("]").split(",")
) )
@ -190,6 +193,15 @@ def validate(ticket):
attributes["cas:memberOf"][group_number] = ( attributes["cas:memberOf"][group_number] = (
attributes["cas:memberOf"][group_number].lstrip(" ").rstrip(" ") attributes["cas:memberOf"][group_number].lstrip(" ").rstrip(" ")
) )
# Extract auxiliary informations (utilisé pour edt_id)
exp = ScoDocSiteConfig.get("cas_edt_id_from_xml_regexp")
if exp:
m = re.search(exp, xmldump)
if m and len(m.groups()) > 0:
cas_edt_id = m.group(1)
if cas_edt_id:
flask.session["CAS_EDT_ID"] = cas_edt_id
flask.session[cas_username_session_key] = username flask.session[cas_username_session_key] = username
flask.session[cas_attributes_session_key] = attributes flask.session[cas_attributes_session_key] = attributes
else: else:

View File

@ -0,0 +1,58 @@
"""edt_id
Revision ID: 6fb956addd69
Revises: fd805feb7ba8
Create Date: 2023-11-06 12:14:42.808476
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "6fb956addd69"
down_revision = "fd805feb7ba8"
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table("group_descr", schema=None) as batch_op:
batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
batch_op.create_index(
batch_op.f("ix_group_descr_edt_id"), ["edt_id"], unique=False
)
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
batch_op.create_index(
batch_op.f("ix_notes_formsemestre_edt_id"), ["edt_id"], unique=False
)
with op.batch_alter_table("notes_modules", schema=None) as batch_op:
batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
batch_op.create_index(
batch_op.f("ix_notes_modules_edt_id"), ["edt_id"], unique=False
)
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.add_column(sa.Column("edt_id", sa.Text(), nullable=True))
batch_op.create_index(batch_op.f("ix_user_edt_id"), ["edt_id"], unique=False)
def downgrade():
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_user_edt_id"))
batch_op.drop_column("edt_id")
with op.batch_alter_table("notes_modules", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_notes_modules_edt_id"))
batch_op.drop_column("edt_id")
with op.batch_alter_table("notes_formsemestre", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_notes_formsemestre_edt_id"))
batch_op.drop_column("edt_id")
with op.batch_alter_table("group_descr", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_group_descr_edt_id"))
batch_op.drop_column("edt_id")