forked from ScoDoc/ScoDoc
Compare commits
11 Commits
9bb7a1add7
...
b38c79a100
Author | SHA1 | Date | |
---|---|---|---|
b38c79a100 | |||
2377918b54 | |||
532fb3e701 | |||
457a9ddf51 | |||
ea1a03a654 | |||
e41879a1e1 | |||
|
38ba20aef6 | ||
|
33c9a606b0 | ||
|
54906c1bde | ||
4d3cbf7e75 | |||
939371cff9 |
@ -18,6 +18,7 @@ from sqlalchemy import desc, func, or_
|
||||
from sqlalchemy.dialects.postgresql import VARCHAR
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.api import api_bp as bp, api_web_bp
|
||||
from app.api import tools
|
||||
from app.but import bulletin_but_court
|
||||
@ -28,10 +29,12 @@ from app.models import (
|
||||
FormSemestreInscription,
|
||||
FormSemestre,
|
||||
Identite,
|
||||
ScolarNews,
|
||||
)
|
||||
from app.scodoc import sco_bulletins
|
||||
from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_utils import json_error, suppress_accents
|
||||
|
||||
@ -475,3 +478,48 @@ def etudiant_groups(formsemestre_id: int, etudid: int = None):
|
||||
data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@bp.route("/etudiant/create", methods=["POST"], defaults={"force": False})
|
||||
@bp.route("/etudiant/create/force", methods=["POST"], defaults={"force": True})
|
||||
@scodoc
|
||||
@permission_required(Permission.EtudInscrit)
|
||||
@as_json
|
||||
def etudiant_create(force=False):
|
||||
"""Création d'un nouvel étudiant
|
||||
Si force, crée même si homonymie détectée.
|
||||
L'étudiant créé n'est pas inscrit à un semestre.
|
||||
Champs requis: nom, prenom (sauf si config sans prénom), dept (string:acronyme)
|
||||
"""
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
dept = args.get("dept", None)
|
||||
if not dept:
|
||||
return scu.json_error(400, "dept requis")
|
||||
dept_o = Departement.query.filter_by(acronym=dept).first()
|
||||
if not dept_o:
|
||||
return scu.json_error(400, "dept invalide")
|
||||
app.set_sco_dept(dept)
|
||||
args["dept_id"] = dept_o.id
|
||||
# vérifie que le département de création est bien autorisé
|
||||
if not current_user.has_permission(Permission.EtudInscrit, dept):
|
||||
return json_error(403, "departement non autorisé")
|
||||
nom = args.get("nom", None)
|
||||
prenom = args.get("prenom", None)
|
||||
ok, homonyms = sco_etud.check_nom_prenom_homonyms(nom=nom, prenom=prenom)
|
||||
if not ok:
|
||||
return scu.json_error(400, "nom ou prénom invalide")
|
||||
if len(homonyms) > 0 and not force:
|
||||
return scu.json_error(
|
||||
400, f"{len(homonyms)} homonymes détectés. Vous pouvez utiliser /force."
|
||||
)
|
||||
etud = Identite.create_etud(**args)
|
||||
# Poste une nouvelle dans le département concerné:
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text=f"Nouvel étudiant {etud.html_link_fiche()}",
|
||||
url=etud.url_fiche(),
|
||||
max_frequency=0,
|
||||
dept_id=dept_o.id,
|
||||
)
|
||||
db.session.commit()
|
||||
return etud.to_dict_short()
|
||||
|
@ -303,15 +303,19 @@ def group_create(partition_id: int): # partition-group-create
|
||||
return json_error(403, "partition non editable")
|
||||
if not partition.formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is None:
|
||||
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
|
||||
if not GroupDescr.check_name(partition, group_name):
|
||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||
group_name = group_name.strip()
|
||||
|
||||
group = GroupDescr(group_name=group_name, partition_id=partition_id)
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = args.get("group_name")
|
||||
if not isinstance(group_name, str):
|
||||
return json_error(API_CLIENT_ERROR, "missing group name or invalid data format")
|
||||
args["group_name"] = args["group_name"].strip()
|
||||
if not GroupDescr.check_name(partition, args["group_name"]):
|
||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||
args["partition_id"] = partition_id
|
||||
try:
|
||||
group = GroupDescr(**args)
|
||||
except TypeError:
|
||||
return json_error(API_CLIENT_ERROR, "invalid arguments")
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log(f"created group {group}")
|
||||
@ -369,16 +373,22 @@ def group_edit(group_id: int):
|
||||
return json_error(403, "partition non editable")
|
||||
if not group.partition.formsemestre.can_change_groups():
|
||||
return json_error(401, "opération non autorisée")
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
group_name = data.get("group_name")
|
||||
if group_name is not None:
|
||||
group_name = group_name.strip()
|
||||
if not GroupDescr.check_name(group.partition, group_name, existing=True):
|
||||
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
if "group_name" in args:
|
||||
if not isinstance(args["group_name"], str):
|
||||
return json_error(API_CLIENT_ERROR, "invalid data format for group_name")
|
||||
args["group_name"] = args["group_name"].strip() if args["group_name"] else ""
|
||||
if not GroupDescr.check_name(
|
||||
group.partition, args["group_name"], existing=True
|
||||
):
|
||||
return json_error(API_CLIENT_ERROR, "invalid group_name")
|
||||
group.group_name = group_name
|
||||
|
||||
group.from_dict(args)
|
||||
db.session.add(group)
|
||||
db.session.commit()
|
||||
log(f"modified {group}")
|
||||
|
||||
app.set_sco_dept(group.partition.formsemestre.departement.acronym)
|
||||
sco_cache.invalidate_formsemestre(group.partition.formsemestre_id)
|
||||
return group.to_dict(with_partition=True)
|
||||
|
@ -7,7 +7,7 @@
|
||||
"""
|
||||
ScoDoc 9 API : accès aux utilisateurs
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from flask import g, request
|
||||
from flask_json import as_json
|
||||
@ -85,6 +85,20 @@ def users_info_query():
|
||||
return [user.to_dict() for user in query]
|
||||
|
||||
|
||||
def _is_allowed_user_edit(args: dict) -> tuple[bool, str]:
|
||||
"Vrai si on peut"
|
||||
if "cas_id" in args and not current_user.has_permission(
|
||||
Permission.UsersChangeCASId
|
||||
):
|
||||
return False, "non autorise a changer cas_id"
|
||||
|
||||
if not current_user.is_administrator():
|
||||
for field in ("cas_allow_login", "cas_allow_scodoc_login"):
|
||||
if field in args:
|
||||
return False, f"non autorise a changer {field}"
|
||||
return True, ""
|
||||
|
||||
|
||||
@bp.route("/user/create", methods=["POST"])
|
||||
@api_web_bp.route("/user/create", methods=["POST"])
|
||||
@login_required
|
||||
@ -95,21 +109,22 @@ def user_create():
|
||||
"""Création d'un utilisateur
|
||||
The request content type should be "application/json":
|
||||
{
|
||||
"user_name": str,
|
||||
"active":bool (default True),
|
||||
"dept": str or null,
|
||||
"nom": str,
|
||||
"prenom": str,
|
||||
"active":bool (default True)
|
||||
"user_name": str,
|
||||
...
|
||||
}
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user_name = data.get("user_name")
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user_name = args.get("user_name")
|
||||
if not user_name:
|
||||
return json_error(404, "empty user_name")
|
||||
user = User.query.filter_by(user_name=user_name).first()
|
||||
if user:
|
||||
return json_error(404, f"user_create: user {user} already exists\n")
|
||||
dept = data.get("dept")
|
||||
dept = args.get("dept")
|
||||
if dept == "@all":
|
||||
dept = None
|
||||
allowed_depts = current_user.get_depts_with_permission(Permission.UsersAdmin)
|
||||
@ -119,10 +134,12 @@ def user_create():
|
||||
Departement.query.filter_by(acronym=dept).first() is None
|
||||
):
|
||||
return json_error(404, "user_create: departement inexistant")
|
||||
nom = data.get("nom")
|
||||
prenom = data.get("prenom")
|
||||
active = scu.to_bool(data.get("active", True))
|
||||
user = User(user_name=user_name, active=active, dept=dept, nom=nom, prenom=prenom)
|
||||
args["dept"] = dept
|
||||
ok, msg = _is_allowed_user_edit(args)
|
||||
if not ok:
|
||||
return json_error(403, f"user_create: {msg}")
|
||||
user = User(user_name=user_name)
|
||||
user.from_dict(args, new_user=True)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user.to_dict()
|
||||
@ -142,13 +159,14 @@ def user_edit(uid: int):
|
||||
"nom": str,
|
||||
"prenom": str,
|
||||
"active":bool
|
||||
...
|
||||
}
|
||||
"""
|
||||
data = request.get_json(force=True) # may raise 400 Bad Request
|
||||
args = request.get_json(force=True) # may raise 400 Bad Request
|
||||
user: User = User.query.get_or_404(uid)
|
||||
# L'utilisateur doit avoir le droit dans le département de départ et celui d'arrivée
|
||||
orig_dept = user.dept
|
||||
dest_dept = data.get("dept", False)
|
||||
dest_dept = args.get("dept", False)
|
||||
if dest_dept is not False:
|
||||
if dest_dept == "@all":
|
||||
dest_dept = None
|
||||
@ -164,10 +182,11 @@ def user_edit(uid: int):
|
||||
return json_error(404, "user_edit: departement inexistant")
|
||||
user.dept = dest_dept
|
||||
|
||||
user.nom = data.get("nom", user.nom)
|
||||
user.prenom = data.get("prenom", user.prenom)
|
||||
user.active = scu.to_bool(data.get("active", user.active))
|
||||
ok, msg = _is_allowed_user_edit(args)
|
||||
if not ok:
|
||||
return json_error(403, f"user_edit: {msg}")
|
||||
|
||||
user.from_dict(args)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
return user.to_dict()
|
||||
|
@ -12,7 +12,6 @@ from typing import Optional
|
||||
|
||||
import cracklib # pylint: disable=import-error
|
||||
|
||||
import flask
|
||||
from flask import current_app, g
|
||||
from flask_login import UserMixin, AnonymousUserMixin
|
||||
|
||||
@ -21,14 +20,13 @@ from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import jwt
|
||||
|
||||
from app import db, email, log, login
|
||||
from app.models import Departement
|
||||
from app.models import Departement, ScoDocModel
|
||||
from app.models import SHORT_STR_LEN, USERNAME_STR_LEN
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_etud # a deplacer dans scu
|
||||
|
||||
VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
|
||||
|
||||
@ -53,13 +51,14 @@ def is_valid_password(cleartxt) -> bool:
|
||||
def invalid_user_name(user_name: str) -> bool:
|
||||
"Check that user_name (aka login) is invalid"
|
||||
return (
|
||||
(len(user_name) < 2)
|
||||
not user_name
|
||||
or (len(user_name) < 2)
|
||||
or (len(user_name) >= USERNAME_STR_LEN)
|
||||
or not VALID_LOGIN_EXP.match(user_name)
|
||||
)
|
||||
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
class User(UserMixin, db.Model, ScoDocModel):
|
||||
"""ScoDoc users, handled by Flask / SQLAlchemy"""
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@ -116,12 +115,17 @@ class User(UserMixin, db.Model):
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"user_name:str is mandatory"
|
||||
self.roles = []
|
||||
self.user_roles = []
|
||||
# check login:
|
||||
if kwargs.get("user_name") and invalid_user_name(kwargs["user_name"]):
|
||||
if not "user_name" in kwargs:
|
||||
raise ValueError("missing user_name argument")
|
||||
if invalid_user_name(kwargs["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {kwargs['user_name']}")
|
||||
super(User, self).__init__(**kwargs)
|
||||
kwargs["nom"] = kwargs.get("nom", "") or ""
|
||||
kwargs["prenom"] = kwargs.get("prenom", "") or ""
|
||||
super().__init__(**kwargs)
|
||||
# Ajoute roles:
|
||||
if (
|
||||
not self.roles
|
||||
@ -251,12 +255,13 @@ class User(UserMixin, db.Model):
|
||||
"cas_last_login": self.cas_last_login.isoformat() + "Z"
|
||||
if self.cas_last_login
|
||||
else None,
|
||||
"edt_id": self.edt_id,
|
||||
"status_txt": "actif" if self.active else "fermé",
|
||||
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
|
||||
"nom": (self.nom or ""), # sco8
|
||||
"prenom": (self.prenom or ""), # sco8
|
||||
"nom": self.nom or "",
|
||||
"prenom": self.prenom or "",
|
||||
"roles_string": self.get_roles_string(), # eg "Ens_RT, Ens_Info"
|
||||
"user_name": self.user_name, # sco8
|
||||
"user_name": self.user_name,
|
||||
# Les champs calculés:
|
||||
"nom_fmt": self.get_nom_fmt(),
|
||||
"prenom_fmt": self.get_prenom_fmt(),
|
||||
@ -270,37 +275,50 @@ class User(UserMixin, db.Model):
|
||||
data["email_institutionnel"] = self.email_institutionnel or ""
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict. No other side effect.
|
||||
args: dict with args in application.
|
||||
returns: dict to store in model's db.
|
||||
Convert boolean values to bools.
|
||||
"""
|
||||
args_dict = args
|
||||
# Dates
|
||||
if "date_expiration" in args:
|
||||
date_expiration = args.get("date_expiration")
|
||||
if isinstance(date_expiration, str):
|
||||
args["date_expiration"] = (
|
||||
datetime.datetime.fromisoformat(date_expiration)
|
||||
if date_expiration
|
||||
else None
|
||||
)
|
||||
# booléens:
|
||||
for field in ("active", "cas_allow_login", "cas_allow_scodoc_login"):
|
||||
if field in args:
|
||||
args_dict[field] = scu.to_bool(args.get(field))
|
||||
|
||||
# chaines ne devant pas être NULLs
|
||||
for field in ("nom", "prenom"):
|
||||
if field in args:
|
||||
args[field] = args[field] or ""
|
||||
|
||||
return args_dict
|
||||
|
||||
def from_dict(self, data: dict, new_user=False):
|
||||
"""Set users' attributes from given dict values.
|
||||
Roles must be encoded as "roles_string", like "Ens_RT, Secr_CJ"
|
||||
- roles_string : roles, encoded like "Ens_RT, Secr_CJ"
|
||||
- date_expiration is a dateime object.
|
||||
Does not check permissions here.
|
||||
"""
|
||||
for field in [
|
||||
"nom",
|
||||
"prenom",
|
||||
"dept",
|
||||
"active",
|
||||
"email",
|
||||
"email_institutionnel",
|
||||
"date_expiration",
|
||||
"cas_id",
|
||||
]:
|
||||
if field in data:
|
||||
setattr(self, field, data[field] or None)
|
||||
# required boolean fields
|
||||
for field in [
|
||||
"cas_allow_login",
|
||||
"cas_allow_scodoc_login",
|
||||
]:
|
||||
setattr(self, field, scu.to_bool(data.get(field, False)))
|
||||
|
||||
if new_user:
|
||||
if "user_name" in data:
|
||||
# never change name of existing users
|
||||
if invalid_user_name(data["user_name"]):
|
||||
raise ValueError(f"invalid user_name: {data['user_name']}")
|
||||
self.user_name = data["user_name"]
|
||||
if "password" in data:
|
||||
self.set_password(data["password"])
|
||||
if invalid_user_name(self.user_name):
|
||||
raise ValueError(f"invalid user_name: {self.user_name}")
|
||||
|
||||
# Roles: roles_string is "Ens_RT, Secr_RT, ..."
|
||||
if "roles_string" in data:
|
||||
self.user_roles = []
|
||||
@ -309,6 +327,8 @@ class User(UserMixin, db.Model):
|
||||
role, dept = UserRole.role_dept_from_string(r_d)
|
||||
self.add_role(role, dept)
|
||||
|
||||
super().from_dict(data, excluded={"user_name", "roles_string", "roles"})
|
||||
|
||||
# Set cas_id using regexp if configured:
|
||||
exp = ScoDocSiteConfig.get("cas_uid_from_mail_regexp")
|
||||
if exp and self.email_institutionnel:
|
||||
@ -441,8 +461,8 @@ class User(UserMixin, db.Model):
|
||||
"""nomplogin est le nom en majuscules suivi du prénom et du login
|
||||
e.g. Dupont Pierre (dupont)
|
||||
"""
|
||||
nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper()
|
||||
return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})"
|
||||
nom = scu.format_nom(self.nom) if self.nom else self.user_name.upper()
|
||||
return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
|
||||
|
||||
@staticmethod
|
||||
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
|
||||
@ -460,29 +480,29 @@ class User(UserMixin, db.Model):
|
||||
def get_nom_fmt(self):
|
||||
"""Nom formaté: "Martin" """
|
||||
if self.nom:
|
||||
return sco_etud.format_nom(self.nom, uppercase=False)
|
||||
return scu.format_nom(self.nom, uppercase=False)
|
||||
else:
|
||||
return self.user_name
|
||||
|
||||
def get_prenom_fmt(self):
|
||||
"""Prénom formaté (minuscule capitalisées)"""
|
||||
return sco_etud.format_prenom(self.prenom)
|
||||
return scu.format_prenom(self.prenom)
|
||||
|
||||
def get_nomprenom(self):
|
||||
"""Nom capitalisé suivi de l'initiale du prénom:
|
||||
Viennet E.
|
||||
"""
|
||||
prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom))
|
||||
prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
|
||||
return (self.get_nom_fmt() + " " + prenom_abbrv).strip()
|
||||
|
||||
def get_prenomnom(self):
|
||||
"""L'initiale du prénom suivie du nom: "J.-C. Dupont" """
|
||||
prenom_abbrv = scu.abbrev_prenom(sco_etud.format_prenom(self.prenom))
|
||||
prenom_abbrv = scu.abbrev_prenom(scu.format_prenom(self.prenom))
|
||||
return (prenom_abbrv + " " + self.get_nom_fmt()).strip()
|
||||
|
||||
def get_nomcomplet(self):
|
||||
"Prénom et nom complets"
|
||||
return sco_etud.format_prenom(self.prenom) + " " + self.get_nom_fmt()
|
||||
return scu.format_prenom(self.prenom) + " " + self.get_nom_fmt()
|
||||
|
||||
# nomnoacc était le nom en minuscules sans accents (inutile)
|
||||
|
||||
|
@ -6,6 +6,7 @@ from flask import Blueprint
|
||||
from app.scodoc import sco_etud
|
||||
from app.auth.models import User
|
||||
from app.models import Departement
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
bp = Blueprint("entreprises", __name__)
|
||||
|
||||
@ -15,12 +16,12 @@ SIRET_PROVISOIRE_START = "xx"
|
||||
|
||||
@bp.app_template_filter()
|
||||
def format_prenom(s):
|
||||
return sco_etud.format_prenom(s)
|
||||
return scu.format_prenom(s)
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
def format_nom(s):
|
||||
return sco_etud.format_nom(s)
|
||||
return scu.format_nom(s)
|
||||
|
||||
|
||||
@bp.app_template_filter()
|
||||
|
@ -1580,8 +1580,8 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
|
||||
)
|
||||
)
|
||||
elif request.method == "GET":
|
||||
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} {
|
||||
sco_etud.format_prenom(etudiant.prenom)}"""
|
||||
form.etudiant.data = f"""{scu.format_nom(etudiant.nom)} {
|
||||
scu.format_prenom(etudiant.prenom)}"""
|
||||
form.etudid.data = etudiant.id
|
||||
form.type_offre.data = stage_apprentissage.type_offre
|
||||
form.date_debut.data = stage_apprentissage.date_debut
|
||||
@ -1699,7 +1699,7 @@ def json_etudiants():
|
||||
list = []
|
||||
for etudiant in etudiants:
|
||||
content = {}
|
||||
value = f"{sco_etud.format_nom(etudiant.nom)} {sco_etud.format_prenom(etudiant.prenom)}"
|
||||
value = f"{scu.format_nom(etudiant.nom)} {scu.format_prenom(etudiant.prenom)}"
|
||||
if etudiant.inscription_courante() is not None:
|
||||
content = {
|
||||
"id": f"{etudiant.id}",
|
||||
|
@ -53,8 +53,9 @@ class ScoDocModel:
|
||||
@classmethod
|
||||
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
|
||||
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded.
|
||||
By default, excluded == { 'id' }"""
|
||||
excluded = {"id"} if excluded is None else set()
|
||||
Add 'id' to excluded."""
|
||||
excluded = excluded or set()
|
||||
excluded.add("id") # always exclude id
|
||||
# Les attributs du modèle qui sont des variables: (élimine les __ et les alias comme adm_id)
|
||||
my_attributes = [
|
||||
a
|
||||
@ -70,7 +71,7 @@ class ScoDocModel:
|
||||
|
||||
@classmethod
|
||||
def convert_dict_fields(cls, args: dict) -> dict:
|
||||
"""Convert fields in the given dict. No side effect.
|
||||
"""Convert fields from the given dict to model's attributes values. No side effect.
|
||||
By default, do nothing, but is overloaded by some subclasses.
|
||||
args: dict with args in application.
|
||||
returns: dict to store in model's db.
|
||||
@ -78,9 +79,11 @@ class ScoDocModel:
|
||||
# virtual, by default, do nothing
|
||||
return args
|
||||
|
||||
def from_dict(self, args: dict):
|
||||
def from_dict(self, args: dict, excluded: set[str] = None):
|
||||
"Update object's fields given in dict. Add to session but don't commit."
|
||||
args_dict = self.convert_dict_fields(self.filter_model_attributes(args))
|
||||
args_dict = self.convert_dict_fields(
|
||||
self.filter_model_attributes(args, excluded=excluded)
|
||||
)
|
||||
for key, value in args_dict.items():
|
||||
if hasattr(self, key):
|
||||
setattr(self, key, value)
|
||||
@ -130,7 +133,6 @@ from app.models.notes import (
|
||||
NotesNotesLog,
|
||||
)
|
||||
from app.models.validations import (
|
||||
ScolarEvent,
|
||||
ScolarFormSemestreValidation,
|
||||
ScolarAutorisationInscription,
|
||||
)
|
||||
@ -149,3 +151,4 @@ from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
|
||||
from app.models.assiduites import Assiduite, Justificatif
|
||||
from app.models.scolar_event import ScolarEvent
|
||||
|
@ -15,7 +15,7 @@ from sqlalchemy import desc, text
|
||||
|
||||
from app import db, log
|
||||
from app import models
|
||||
|
||||
from app.models.scolar_event import ScolarEvent
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc.sco_bac import Baccalaureat
|
||||
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
|
||||
@ -170,9 +170,13 @@ class Identite(db.Model, models.ScoDocModel):
|
||||
|
||||
def html_link_fiche(self) -> str:
|
||||
"lien vers la fiche"
|
||||
return f"""<a class="stdlink" href="{
|
||||
url_for("scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id)
|
||||
}">{self.nomprenom}</a>"""
|
||||
return f"""<a class="stdlink" href="{self.url_fiche()}">{self.nomprenom}</a>"""
|
||||
|
||||
def url_fiche(self) -> str:
|
||||
"url de la fiche étudiant"
|
||||
return url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, etudid=None, code_nip=None) -> "Identite":
|
||||
@ -211,6 +215,10 @@ class Identite(db.Model, models.ScoDocModel):
|
||||
etud.admission = Admission()
|
||||
etud.adresses.append(Adresse(typeadresse="domicile"))
|
||||
db.session.flush()
|
||||
|
||||
event = ScolarEvent(etud=etud, event_type="CREATION")
|
||||
db.session.add(event)
|
||||
log(f"Identite.create {etud}")
|
||||
return etud
|
||||
|
||||
@property
|
||||
|
@ -133,7 +133,7 @@ class ScolarNews(db.Model):
|
||||
return query.order_by(cls.date.desc()).limit(n).all()
|
||||
|
||||
@classmethod
|
||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=600):
|
||||
def add(cls, typ, obj=None, text="", url=None, max_frequency=600, dept_id=None):
|
||||
"""Enregistre une nouvelle
|
||||
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
||||
à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
|
||||
@ -141,10 +141,11 @@ class ScolarNews(db.Model):
|
||||
même (obj, typ, user).
|
||||
La nouvelle enregistrée est aussi envoyée par mail.
|
||||
"""
|
||||
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
||||
if max_frequency:
|
||||
last_news = (
|
||||
cls.query.filter_by(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
dept_id=dept_id,
|
||||
authenticated_user=current_user.user_name,
|
||||
type=typ,
|
||||
object=obj,
|
||||
@ -163,7 +164,7 @@ class ScolarNews(db.Model):
|
||||
return
|
||||
|
||||
news = ScolarNews(
|
||||
dept_id=g.scodoc_dept_id,
|
||||
dept_id=dept_id,
|
||||
authenticated_user=current_user.user_name,
|
||||
type=typ,
|
||||
object=obj,
|
||||
|
@ -11,14 +11,14 @@ from operator import attrgetter
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app import db, log
|
||||
from app.models import Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||
from app.models import ScoDocModel, Scolog, GROUPNAME_STR_LEN, SHORT_STR_LEN
|
||||
from app.models.etudiants import Identite
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
||||
|
||||
|
||||
class Partition(db.Model):
|
||||
class Partition(db.Model, ScoDocModel):
|
||||
"""Partition: découpage d'une promotion en groupes"""
|
||||
|
||||
__table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),)
|
||||
@ -204,7 +204,7 @@ class Partition(db.Model):
|
||||
return group
|
||||
|
||||
|
||||
class GroupDescr(db.Model):
|
||||
class GroupDescr(db.Model, ScoDocModel):
|
||||
"""Description d'un groupe d'une partition"""
|
||||
|
||||
__tablename__ = "group_descr"
|
||||
|
48
app/models/scolar_event.py
Normal file
48
app/models/scolar_event.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""évènements scolaires dans la vie d'un étudiant(inscription, ...)
|
||||
"""
|
||||
from app import db
|
||||
from app.models import SHORT_STR_LEN
|
||||
|
||||
|
||||
class ScolarEvent(db.Model):
|
||||
"""Evenement dans le parcours scolaire d'un étudiant"""
|
||||
|
||||
__tablename__ = "scolar_events"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
event_id = db.synonym("id")
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
)
|
||||
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
|
||||
)
|
||||
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
|
||||
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
|
||||
# 'ECHEC_SEM'
|
||||
# 'UTIL_COMPENSATION'
|
||||
event_type = db.Column(db.String(SHORT_STR_LEN))
|
||||
# Semestre compensé par formsemestre_id:
|
||||
comp_formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
|
||||
formsemestre = db.relationship(
|
||||
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"
|
@ -1,6 +1,6 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""Notes, décisions de jury, évènements scolaires
|
||||
"""Notes, décisions de jury
|
||||
"""
|
||||
|
||||
from app import db
|
||||
@ -218,47 +218,3 @@ class ScolarAutorisationInscription(db.Model):
|
||||
msg=f"Passage vers S{autorisation.semestre_id}: effacé",
|
||||
)
|
||||
db.session.flush()
|
||||
|
||||
|
||||
class ScolarEvent(db.Model):
|
||||
"""Evenement dans le parcours scolaire d'un étudiant"""
|
||||
|
||||
__tablename__ = "scolar_events"
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
event_id = db.synonym("id")
|
||||
etudid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("identite.id", ondelete="CASCADE"),
|
||||
)
|
||||
event_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
|
||||
formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id", ondelete="SET NULL"),
|
||||
)
|
||||
ue_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_ue.id", ondelete="SET NULL"),
|
||||
)
|
||||
# 'CREATION', 'INSCRIPTION', 'DEMISSION',
|
||||
# 'AUT_RED', 'EXCLUS', 'VALID_UE', 'VALID_SEM'
|
||||
# 'ECHEC_SEM'
|
||||
# 'UTIL_COMPENSATION'
|
||||
event_type = db.Column(db.String(SHORT_STR_LEN))
|
||||
# Semestre compensé par formsemestre_id:
|
||||
comp_formsemestre_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("notes_formsemestre.id"),
|
||||
)
|
||||
etud = db.relationship("Identite", lazy="select", backref="events", uselist=False)
|
||||
formsemestre = db.relationship(
|
||||
"FormSemestre", lazy="select", uselist=False, foreign_keys=[formsemestre_id]
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"as a dict"
|
||||
d = dict(self.__dict__)
|
||||
d.pop("_sa_instance_state", None)
|
||||
return d
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.event_type}, {self.event_date.isoformat()}, {self.formsemestre})"
|
||||
|
@ -45,6 +45,12 @@ from app.models.etudiants import (
|
||||
pivot_year,
|
||||
)
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import (
|
||||
format_civilite,
|
||||
format_nom,
|
||||
format_nomprenom,
|
||||
format_prenom,
|
||||
)
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||
from app.scodoc import safehtml
|
||||
@ -102,60 +108,6 @@ def force_uppercase(s):
|
||||
return s.upper() if s else s
|
||||
|
||||
|
||||
def format_nomprenom(etud, reverse=False):
|
||||
"""Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
||||
Si reverse, "Dupont Pierre", sans civilité.
|
||||
|
||||
DEPRECATED: utiliser Identite.nomprenom
|
||||
"""
|
||||
nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
|
||||
prenom = format_prenom(etud["prenom"])
|
||||
civilite = format_civilite(etud["civilite"])
|
||||
if reverse:
|
||||
fs = [nom, prenom]
|
||||
else:
|
||||
fs = [civilite, prenom, nom]
|
||||
return " ".join([x for x in fs if x])
|
||||
|
||||
|
||||
def format_prenom(s):
|
||||
"""Formatte prenom etudiant pour affichage
|
||||
DEPRECATED: utiliser Identite.prenom_str
|
||||
"""
|
||||
if not s:
|
||||
return ""
|
||||
frags = s.split()
|
||||
r = []
|
||||
for frag in frags:
|
||||
fs = frag.split("-")
|
||||
r.append("-".join([x.lower().capitalize() for x in fs]))
|
||||
return " ".join(r)
|
||||
|
||||
|
||||
def format_nom(s, uppercase=True):
|
||||
if not s:
|
||||
return ""
|
||||
if uppercase:
|
||||
return s.upper()
|
||||
else:
|
||||
return format_prenom(s)
|
||||
|
||||
|
||||
def format_civilite(civilite):
|
||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
personne ne souhaitant pas d'affichage).
|
||||
Raises ScoValueError if conversion fails.
|
||||
"""
|
||||
try:
|
||||
return {
|
||||
"M": "M.",
|
||||
"F": "Mme",
|
||||
"X": "",
|
||||
}[civilite]
|
||||
except KeyError as exc:
|
||||
raise ScoValueError(f"valeur invalide pour la civilité: {civilite}") from exc
|
||||
|
||||
|
||||
def _format_etat_civil(etud: dict) -> str:
|
||||
"Mme Béatrice DUPONT, en utilisant les données d'état civil si indiquées."
|
||||
if etud["prenom_etat_civil"] or etud["civilite_etat_civil"]:
|
||||
@ -657,16 +609,6 @@ def create_etud(cnx, args: dict = None):
|
||||
db.session.commit()
|
||||
etudid = etud.id
|
||||
|
||||
# event
|
||||
scolar_events_create(
|
||||
cnx,
|
||||
args={
|
||||
"etudid": etudid,
|
||||
"event_date": time.strftime("%d/%m/%Y"),
|
||||
"formsemestre_id": None,
|
||||
"event_type": "CREATION",
|
||||
},
|
||||
)
|
||||
# log
|
||||
logdb(
|
||||
cnx,
|
||||
@ -674,16 +616,18 @@ def create_etud(cnx, args: dict = None):
|
||||
etudid=etudid,
|
||||
msg="creation initiale",
|
||||
)
|
||||
etud = etudident_list(cnx, {"etudid": etudid})[0]
|
||||
fill_etuds_info([etud])
|
||||
etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
|
||||
etud_dict = etudident_list(cnx, {"etudid": etudid})[0]
|
||||
fill_etuds_info([etud_dict])
|
||||
etud_dict["url"] = url_for(
|
||||
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||
)
|
||||
ScolarNews.add(
|
||||
typ=ScolarNews.NEWS_INSCR,
|
||||
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
|
||||
text=f"Nouvel étudiant {etud.html_link_fiche()}",
|
||||
url=etud["url"],
|
||||
max_frequency=0,
|
||||
)
|
||||
return etud
|
||||
return etud_dict
|
||||
|
||||
|
||||
# ---------- "EVENTS"
|
||||
|
@ -42,6 +42,7 @@ from app.scodoc import sco_groups
|
||||
from app.scodoc.sco_exceptions import ScoException
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def form_search_etud(
|
||||
@ -271,7 +272,7 @@ def search_etud_by_name(term: str) -> list:
|
||||
data = [
|
||||
{
|
||||
"label": "%s %s %s"
|
||||
% (x["code_nip"], x["nom"], sco_etud.format_prenom(x["prenom"])),
|
||||
% (x["code_nip"], x["nom"], scu.format_prenom(x["prenom"])),
|
||||
"value": x["code_nip"],
|
||||
}
|
||||
for x in r
|
||||
@ -290,7 +291,7 @@ def search_etud_by_name(term: str) -> list:
|
||||
|
||||
data = [
|
||||
{
|
||||
"label": "%s %s" % (x["nom"], sco_etud.format_prenom(x["prenom"])),
|
||||
"label": "%s %s" % (x["nom"], scu.format_prenom(x["prenom"])),
|
||||
"value": x["etudid"],
|
||||
}
|
||||
for x in r
|
||||
|
@ -39,7 +39,7 @@ from app.comp.res_compat import NotesTableCompat
|
||||
from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog
|
||||
from app.models.etudiants import Identite
|
||||
from app.models.groups import Partition, GroupDescr
|
||||
from app.models.validations import ScolarEvent
|
||||
from app.models.scolar_event import ScolarEvent
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc.scolog import logdb
|
||||
|
@ -53,6 +53,7 @@ from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_cursus
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_etud import etud_sort_key
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_xml
|
||||
from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
@ -573,8 +574,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
etudid=str(e["etudid"]),
|
||||
civilite=etud["civilite_str"] or "",
|
||||
sexe=etud["civilite_str"] or "", # compat
|
||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
||||
nom=scu.format_nom(etud["nom"] or ""),
|
||||
prenom=scu.format_prenom(etud["prenom"] or ""),
|
||||
origin=_comp_etud_origin(etud, formsemestre),
|
||||
)
|
||||
)
|
||||
@ -599,8 +600,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
"etud",
|
||||
etudid=str(etud["etudid"]),
|
||||
sexe=etud["civilite_str"] or "",
|
||||
nom=sco_etud.format_nom(etud["nom"] or ""),
|
||||
prenom=sco_etud.format_prenom(etud["prenom"] or ""),
|
||||
nom=scu.format_nom(etud["nom"] or ""),
|
||||
prenom=scu.format_prenom(etud["prenom"] or ""),
|
||||
origin=_comp_etud_origin(etud, formsemestre),
|
||||
)
|
||||
)
|
||||
|
@ -101,7 +101,8 @@ def group_rename(group_id):
|
||||
"allow_null": True,
|
||||
"explanation": """optionnel : identifiant du groupe dans le logiciel
|
||||
d'emploi du temps, pour le cas où les noms de groupes ne seraient pas
|
||||
les mêmes dans ScoDoc et dans l'emploi du temps (si plusieurs ids,
|
||||
les mêmes dans ScoDoc et dans l'emploi du temps (si plusieurs ids de
|
||||
groupes EDT doivent correspondre au même groupe ScoDoc,
|
||||
les séparer par des virgules).""",
|
||||
},
|
||||
),
|
||||
|
@ -254,7 +254,7 @@ def import_users(users, force="") -> tuple[bool, list[str], int]:
|
||||
if import_ok:
|
||||
for u in created.values():
|
||||
# Création de l'utilisateur (via SQLAlchemy)
|
||||
user = User()
|
||||
user = User(user_name=u["user_name"])
|
||||
user.from_dict(u, new_user=True)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
@ -244,8 +244,8 @@ def feuille_preparation_jury(formsemestre_id):
|
||||
[
|
||||
etud.id,
|
||||
etud.civilite_str,
|
||||
sco_etud.format_nom(etud.nom),
|
||||
sco_etud.format_prenom(etud.prenom),
|
||||
scu.format_nom(etud.nom),
|
||||
scu.format_prenom(etud.prenom),
|
||||
etud.date_naissance,
|
||||
etud.admission.bac if etud.admission else "",
|
||||
etud.admission.specialite if etud.admission else "",
|
||||
|
@ -1244,7 +1244,7 @@ def _form_saisie_notes(
|
||||
'<span class="%s">' % classdem
|
||||
+ e["civilite_str"]
|
||||
+ " "
|
||||
+ sco_etud.format_nomprenom(e, reverse=True)
|
||||
+ scu.format_nomprenom(e, reverse=True)
|
||||
+ "</span>"
|
||||
)
|
||||
|
||||
|
@ -164,9 +164,9 @@ def trombino_html(groups_infos):
|
||||
H.append("</span>")
|
||||
H.append(
|
||||
'<span class="trombi_legend"><span class="trombi_prenom">'
|
||||
+ sco_etud.format_prenom(t["prenom"])
|
||||
+ scu.format_prenom(t["prenom"])
|
||||
+ '</span><span class="trombi_nom">'
|
||||
+ sco_etud.format_nom(t["nom"])
|
||||
+ scu.format_nom(t["nom"])
|
||||
+ (" <i>(dem.)</i>" if t["etat"] == "D" else "")
|
||||
)
|
||||
H.append("</span></span></span>")
|
||||
@ -349,7 +349,7 @@ def _trombino_pdf(groups_infos):
|
||||
[img],
|
||||
[
|
||||
Paragraph(
|
||||
SU(sco_etud.format_nomprenom(t)),
|
||||
SU(scu.format_nomprenom(t)),
|
||||
style_sheet["Normal"],
|
||||
)
|
||||
],
|
||||
@ -428,7 +428,7 @@ def _listeappel_photos_pdf(groups_infos):
|
||||
t = groups_infos.members[i]
|
||||
img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
|
||||
txt = Paragraph(
|
||||
SU(sco_etud.format_nomprenom(t)),
|
||||
SU(scu.format_nomprenom(t)),
|
||||
style_sheet["Normal"],
|
||||
)
|
||||
if currow:
|
||||
|
@ -55,7 +55,7 @@ def trombino_doc(groups_infos):
|
||||
cell = table.rows[2 * li + 1].cells[co]
|
||||
cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
|
||||
cell_p, cell_f, cell_r = _paragraph_format_run(cell)
|
||||
cell_r.add_text(sco_etud.format_nomprenom(t))
|
||||
cell_r.add_text(scu.format_nomprenom(t))
|
||||
cell_f.space_after = Mm(8)
|
||||
|
||||
return scu.send_docx(document, filename)
|
||||
|
@ -196,9 +196,9 @@ def pdf_trombino_tours(
|
||||
Paragraph(
|
||||
SU(
|
||||
"<para align=center><font size=8>"
|
||||
+ sco_etud.format_prenom(m["prenom"])
|
||||
+ scu.format_prenom(m["prenom"])
|
||||
+ " "
|
||||
+ sco_etud.format_nom(m["nom"])
|
||||
+ scu.format_nom(m["nom"])
|
||||
+ text_group
|
||||
+ "</font></para>"
|
||||
),
|
||||
@ -413,11 +413,7 @@ def pdf_feuille_releve_absences(
|
||||
for m in members:
|
||||
currow = [
|
||||
Paragraph(
|
||||
SU(
|
||||
sco_etud.format_nom(m["nom"])
|
||||
+ " "
|
||||
+ sco_etud.format_prenom(m["prenom"])
|
||||
),
|
||||
SU(scu.format_nom(m["nom"]) + " " + scu.format_prenom(m["prenom"])),
|
||||
StyleSheet["Normal"],
|
||||
)
|
||||
]
|
||||
|
@ -204,7 +204,7 @@ def list_users(
|
||||
"cas_allow_scodoc_login",
|
||||
"cas_last_login",
|
||||
]
|
||||
columns_ids.append("email_institutionnel")
|
||||
columns_ids += ["email_institutionnel", "edt_id"]
|
||||
|
||||
title = "Utilisateurs définis dans ScoDoc"
|
||||
tab = GenTable(
|
||||
@ -227,6 +227,7 @@ def list_users(
|
||||
"cas_allow_login": "CAS autorisé",
|
||||
"cas_allow_scodoc_login": "Cnx sans CAS",
|
||||
"cas_last_login": "Dernier login CAS",
|
||||
"edt_id": "Identifiant emploi du temps",
|
||||
},
|
||||
caption=title,
|
||||
page_title="title",
|
||||
@ -431,15 +432,3 @@ def check_modif_user(
|
||||
)
|
||||
# Roles ?
|
||||
return True, ""
|
||||
|
||||
|
||||
def user_edit(user_name, vals):
|
||||
"""Edit the user specified by user_name
|
||||
(ported from Zope to SQLAlchemy, hence strange !)
|
||||
"""
|
||||
u: User = User.query.filter_by(user_name=user_name).first()
|
||||
if not u:
|
||||
raise ScoValueError("Invalid user_name")
|
||||
u.from_dict(vals)
|
||||
db.session.add(u)
|
||||
db.session.commit()
|
||||
|
@ -64,6 +64,7 @@ from config import Config
|
||||
from app import log, ScoDocJSONEncoder
|
||||
|
||||
from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
from app.scodoc import sco_xml
|
||||
import sco_version
|
||||
|
||||
@ -1139,6 +1140,61 @@ def abbrev_prenom(prenom):
|
||||
return abrv
|
||||
|
||||
|
||||
def format_civilite(civilite):
|
||||
"""returns 'M.' ou 'Mme' ou '' (pour le genre neutre,
|
||||
personne ne souhaitant pas d'affichage).
|
||||
Raises ScoValueError if conversion fails.
|
||||
"""
|
||||
try:
|
||||
return {
|
||||
"M": "M.",
|
||||
"F": "Mme",
|
||||
"X": "",
|
||||
}[civilite]
|
||||
except KeyError as exc:
|
||||
raise ScoValueError(f"valeur invalide pour la civilité: {civilite}") from exc
|
||||
|
||||
|
||||
def format_nomprenom(etud, reverse=False):
|
||||
"""Formatte civilité/nom/prenom pour affichages: "M. Pierre Dupont"
|
||||
Si reverse, "Dupont Pierre", sans civilité.
|
||||
|
||||
DEPRECATED: utiliser Identite.nomprenom
|
||||
"""
|
||||
nom = etud.get("nom_disp", "") or etud.get("nom_usuel", "") or etud["nom"]
|
||||
prenom = format_prenom(etud["prenom"])
|
||||
civilite = format_civilite(etud["civilite"])
|
||||
if reverse:
|
||||
fs = [nom, prenom]
|
||||
else:
|
||||
fs = [civilite, prenom, nom]
|
||||
return " ".join([x for x in fs if x])
|
||||
|
||||
|
||||
def format_nom(s, uppercase=True):
|
||||
"Formatte le nom"
|
||||
if not s:
|
||||
return ""
|
||||
if uppercase:
|
||||
return s.upper()
|
||||
else:
|
||||
return format_prenom(s)
|
||||
|
||||
|
||||
def format_prenom(s):
|
||||
"""Formatte prenom etudiant pour affichage
|
||||
DEPRECATED: utiliser Identite.prenom_str
|
||||
"""
|
||||
if not s:
|
||||
return ""
|
||||
frags = s.split()
|
||||
r = []
|
||||
for frag in frags:
|
||||
fs = frag.split("-")
|
||||
r.append("-".join([x.lower().capitalize() for x in fs]))
|
||||
return " ".join(r)
|
||||
|
||||
|
||||
#
|
||||
def timedate_human_repr():
|
||||
"representation du temps courant pour utilisateur"
|
||||
@ -1476,3 +1532,15 @@ def is_assiduites_module_forced(
|
||||
except (TypeError, ValueError):
|
||||
retour = sco_preferences.get_preference("forcer_module", dept_id=dept_id)
|
||||
return retour
|
||||
|
||||
|
||||
def get_assiduites_time_config(config_type: str) -> str:
|
||||
from app.models import ScoDocSiteConfig
|
||||
|
||||
match config_type:
|
||||
case "matin":
|
||||
return ScoDocSiteConfig.get("assi_morning_time", "08:00:00")
|
||||
case "aprem":
|
||||
return ScoDocSiteConfig.get("assi_afternoon_time", "18:00:00")
|
||||
case "pivot":
|
||||
return ScoDocSiteConfig.get("assi_lunch_time", "13:00:00")
|
||||
|
@ -1,15 +1,17 @@
|
||||
:root {
|
||||
--color-present: #6bdb83;
|
||||
--color-absent: #e62a11;
|
||||
--color-absent-clair: #F25D4A;
|
||||
--color-retard: #f0c865;
|
||||
--color-justi: #7059FF;
|
||||
--color-justi-clair: #6885E3;
|
||||
--color-justi-invalide: #a84476;
|
||||
--color-nonwork: #badfff;
|
||||
|
||||
--color-absent-justi: #e65ab7;
|
||||
--color-retard-justi: #ffef7a;
|
||||
|
||||
--color-error: #FF0000;
|
||||
--color-error: #e62a11;
|
||||
--color-warning: #eec660;
|
||||
--color-information: #658ef0;
|
||||
|
||||
@ -21,7 +23,7 @@
|
||||
|
||||
--color-defaut: #FFF;
|
||||
--color-defaut-dark: #444;
|
||||
|
||||
--color-default-text: #1F1F1F;
|
||||
|
||||
|
||||
--motif-justi: repeating-linear-gradient(135deg, transparent, transparent 4px, var(--color-justi) 4px, var(--color-justi) 8px);
|
||||
|
@ -4347,6 +4347,10 @@ table.dataTable td.group {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
#zonePartitions .edt_id {
|
||||
color: rgb(85, 255, 24);
|
||||
}
|
||||
|
||||
/* ------------- Nouveau tableau recap ------------ */
|
||||
div.table_recap {
|
||||
margin-top: 6px;
|
||||
@ -4856,7 +4860,3 @@ div.cas_etat_certif_ssl {
|
||||
font-style: italic;
|
||||
color: rgb(231, 0, 0);
|
||||
}
|
||||
|
||||
.edt_id {
|
||||
color: rgb(85, 255, 24);
|
||||
}
|
||||
|
@ -953,6 +953,89 @@ function createAssiduite(etat, etudid) {
|
||||
);
|
||||
return !with_errors;
|
||||
}
|
||||
/**
|
||||
* Création d'une assiduité pour un étudiant
|
||||
* @param {String} etat l'état de l'étudiant
|
||||
* @param {Number | String} etudid l'identifiant de l'étudiant
|
||||
*
|
||||
* TODO : Rendre asynchrone
|
||||
*/
|
||||
function createAssiduiteComplete(assiduite, etudid) {
|
||||
if (!hasModuleImpl(assiduite) && window.forceModule) {
|
||||
const html = `
|
||||
<h3>Aucun module n'a été spécifié</h3>
|
||||
`;
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
openAlertModal("Erreur Module", div);
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = getUrl() + `/api/assiduite/${etudid}/create`;
|
||||
|
||||
let with_errors = false;
|
||||
|
||||
sync_post(
|
||||
path,
|
||||
[assiduite],
|
||||
(data, status) => {
|
||||
//success
|
||||
if (data.success.length > 0) {
|
||||
let obj = data.success["0"].message.assiduite_id;
|
||||
}
|
||||
if (data.errors.length > 0) {
|
||||
console.error(data.errors["0"].message);
|
||||
if (data.errors["0"].message == "Module non renseigné") {
|
||||
const HTML = `
|
||||
<p>Attention, le module doit obligatoirement être renseigné.</p>
|
||||
<p>Cela vient de la configuration du semestre ou plus largement du département.</p>
|
||||
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||
`;
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.innerHTML = HTML;
|
||||
|
||||
openAlertModal("Sélection du module", content);
|
||||
}
|
||||
if (
|
||||
data.errors["0"].message == "L'étudiant n'est pas inscrit au module"
|
||||
) {
|
||||
const HTML = `
|
||||
<p>Attention, l'étudiant n'est pas inscrit à ce module.</p>
|
||||
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||
`;
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.innerHTML = HTML;
|
||||
|
||||
openAlertModal("Sélection du module", content);
|
||||
}
|
||||
if (
|
||||
data.errors["0"].message ==
|
||||
"Duplication: la période rentre en conflit avec une plage enregistrée"
|
||||
) {
|
||||
const HTML = `
|
||||
<p>L'assiduité n'a pas pu être enregistrée car une autre assiduité existe sur la période sélectionnée</p>
|
||||
<p>Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.</p>
|
||||
`;
|
||||
|
||||
const content = document.createElement("div");
|
||||
content.innerHTML = HTML;
|
||||
|
||||
openAlertModal("Période conflictuelle", content);
|
||||
}
|
||||
with_errors = true;
|
||||
}
|
||||
},
|
||||
(data, status) => {
|
||||
//error
|
||||
console.error(data, status);
|
||||
errorAlert();
|
||||
with_errors = true;
|
||||
}
|
||||
);
|
||||
return !with_errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suppression d'une assiduité
|
||||
|
234
app/templates/assiduites/pages/ajout_assiduites.j2
Normal file
234
app/templates/assiduites/pages/ajout_assiduites.j2
Normal file
@ -0,0 +1,234 @@
|
||||
{% include "assiduites/widgets/toast.j2" %}
|
||||
{% block pageContent %}
|
||||
<div class="pageContent">
|
||||
<h3>Ajouter une assiduité</h3>
|
||||
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||
{% if saisie_eval %}
|
||||
<div id="saisie_eval">
|
||||
<br>
|
||||
<h3>
|
||||
La saisie de l'assiduité a été préconfigurée en fonction de l'évaluation. <br>
|
||||
Une fois la saisie finie, cliquez sur le lien si dessous pour revenir sur la gestion de l'évaluation
|
||||
</h3>
|
||||
<a href="{{redirect_url}}">retourner sur la page de l'évaluation</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<section class="assi-form page">
|
||||
<fieldset>
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="assi_date_debut" required>Date de début</legend>
|
||||
<scodoc-datetime name="assi_date_debut" id="assi_date_debut"> </scodoc-datetime>
|
||||
<span>Journée entière</span> <input type="checkbox" name="assi_journee" id="assi_journee">
|
||||
</div>
|
||||
<div class="assi-label" id="date_fin">
|
||||
<legend for="assi_date_fin" required>Date de fin</legend>
|
||||
<scodoc-datetime name="assi_date_fin" id="assi_date_fin"></scodoc-datetime>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="assi_etat" required>Etat de l'assiduité</legend>
|
||||
<select name="assi_etat" id="assi_etat">
|
||||
<option value="absent" selected>Absent</option>
|
||||
<option value="retard">Retard</option>
|
||||
<option value="present">Présent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="assi_module" required>Module</legend>
|
||||
{% with moduleid="ajout_assiduite_module_impl",label=false %}
|
||||
{% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assi-row">
|
||||
<div class="assi-label">
|
||||
<legend for="assi_raison">Raison</legend>
|
||||
<textarea name="assi_raison" id="assi_raison" cols="50" rows="10" maxlength="500"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="assi-row">
|
||||
<button onclick="validerFormulaire(this)">Créer l'assiduité</button>
|
||||
<button onclick="effacerFormulaire()">Remettre à zero</button>
|
||||
</div>
|
||||
|
||||
|
||||
</fieldset>
|
||||
|
||||
</section>
|
||||
<section class="liste">
|
||||
<a class="icon filter" onclick="filterAssi()"></a>
|
||||
{% include "assiduites/widgets/tableau_assi.j2" %}
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.assi-row {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.assi-form fieldset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.pageContent {
|
||||
max-width: var(--sco-content-max-width);
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.assi-label {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
[required]::after {
|
||||
content: "*";
|
||||
color: var(--color-error);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
|
||||
|
||||
function validateFields() {
|
||||
const field = document.querySelector('.assi-form')
|
||||
const { deb, fin } = getDates()
|
||||
const date_debut = new Date(deb);
|
||||
const date_fin = new Date(fin);
|
||||
|
||||
if (deb == "" || fin == "" || !date_debut.isValid() || !date_fin.isValid()) {
|
||||
openAlertModal("Erreur détéctée", document.createTextNode("Il faut indiquer une date de début et une date de fin valide."), "", color = "crimson");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (date_fin.isBefore(date_debut)) {
|
||||
openAlertModal("Erreur détéctée", document.createTextNode("La date de fin doit se trouver après la date de début."), "", color = "crimson");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function fieldsToAssiduite() {
|
||||
const field = document.querySelector('.assi-form.page')
|
||||
|
||||
const { deb, fin } = getDates()
|
||||
|
||||
const etat = field.querySelector('#assi_etat').value;
|
||||
const raison = field.querySelector('#assi_raison').value;
|
||||
const module = field.querySelector("#ajout_assiduite_module_impl").value;
|
||||
|
||||
return {
|
||||
date_debut: new Date(deb).toFakeIso(),
|
||||
date_fin: new Date(fin).toFakeIso(),
|
||||
etat: etat,
|
||||
description: raison,
|
||||
moduleimpl_id: module,
|
||||
}
|
||||
}
|
||||
|
||||
function validerFormulaire(btn) {
|
||||
if (!validateFields()) return
|
||||
|
||||
const assiduite = fieldsToAssiduite();
|
||||
let assiduite_id = null;
|
||||
|
||||
createAssiduiteComplete(assiduite, etudid);
|
||||
loadAll();
|
||||
btn.disabled = true;
|
||||
setTimeout(() => {
|
||||
btn.disabled = false;
|
||||
}, 1000)
|
||||
|
||||
}
|
||||
|
||||
function effacerFormulaire() {
|
||||
const field = document.querySelector('.assi-form')
|
||||
|
||||
field.querySelector('#assi_date_debut').value = "";
|
||||
field.querySelector('#assi_date_fin').value = "";
|
||||
field.querySelector('#assi_etat').value = "attente";
|
||||
field.querySelector('#assi_raison').value = "";
|
||||
|
||||
}
|
||||
|
||||
function dayOnly() {
|
||||
const date_deb = document.getElementById("assi_date_debut");
|
||||
const date_fin = document.getElementById("assi_date_fin");
|
||||
|
||||
if (document.getElementById('assi_journee').checked) {
|
||||
date_deb.setAttribute("show", "date")
|
||||
date_fin.setAttribute("show", "date")
|
||||
document.querySelector(`legend[for="assi_date_fin"]`).removeAttribute("required")
|
||||
} else {
|
||||
date_deb.removeAttribute("show")
|
||||
date_fin.removeAttribute("show")
|
||||
document.querySelector(`legend[for="assi_date_fin"]`).setAttribute("required", "")
|
||||
}
|
||||
}
|
||||
|
||||
function getDates() {
|
||||
const date_deb = document.querySelector(".page #assi_date_debut")
|
||||
const date_fin = document.querySelector(".page #assi_date_fin")
|
||||
const journee = document.querySelector('.page #assi_journee').checked
|
||||
const deb = date_deb.valueAsObject.date + "T" + (journee ? assi_morning : date_deb.valueAsObject.time)
|
||||
let fin = "T" + (journee ? assi_evening : date_fin.valueAsObject.time)
|
||||
if (journee) {
|
||||
fin = (date_fin.valueAsObject.date || date_deb.valueAsObject.date) + fin
|
||||
} else {
|
||||
fin = date_fin.valueAsObject.date + fin
|
||||
}
|
||||
|
||||
return {
|
||||
"deb": deb,
|
||||
"fin": fin,
|
||||
}
|
||||
}
|
||||
|
||||
const etudid = {{ sco.etud.id }};
|
||||
|
||||
const assi_limit_annee = "{{ assi_limit_annee }}" == "True" ? true : false;
|
||||
const assi_morning = '{{assi_morning}}';
|
||||
const assi_evening = '{{assi_evening}}';
|
||||
|
||||
{% if saisie_eval %}
|
||||
const saisie_eval = true;
|
||||
const date_deb = "{{date_deb}}";
|
||||
const date_fin = "{{date_fin}}";
|
||||
const moduleimpl = {{ moduleimpl_id }};
|
||||
{% else %}
|
||||
const saisie_eval = false;
|
||||
{% endif %}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
loadAll();
|
||||
document.getElementById('assi_journee').addEventListener('click', () => { dayOnly() });
|
||||
dayOnly()
|
||||
|
||||
if (saisie_eval) {
|
||||
document.getElementById("assi_date_debut").value = Date.removeUTC(date_deb);
|
||||
document.getElementById("assi_date_fin").value = Date.removeUTC(date_fin);
|
||||
} else {
|
||||
const today = (new Date()).format("YYYY-MM-DD");
|
||||
document.getElementById("assi_date_debut").valueAsObject = { date: today, time: assi_morning }
|
||||
document.getElementById("assi_date_fin").valueAsObject = { time: assi_evening }
|
||||
}
|
||||
|
||||
|
||||
document.getElementById("assi_date_debut").addEventListener("blur", (event) => {
|
||||
updateSelect(null, "#ajout_assiduite_module_impl", event.target.valueAsObject.date)
|
||||
})
|
||||
|
||||
updateSelect(saisie_eval ? moduleimpl : "", "#ajout_assiduite_module_impl", document.getElementById("assi_date_debut").valueAsObject.date);
|
||||
|
||||
|
||||
});
|
||||
</script>
|
||||
{% endblock pageContent %}
|
@ -3,10 +3,7 @@
|
||||
<div class="pageContent">
|
||||
<h3>Justifier des absences ou retards</h3>
|
||||
{% include "assiduites/widgets/tableau_base.j2" %}
|
||||
<section class="liste">
|
||||
<a class="icon filter" onclick="filterJusti()"></a>
|
||||
{% include "assiduites/widgets/tableau_justi.j2" %}
|
||||
</section>
|
||||
|
||||
|
||||
<section class="justi-form page">
|
||||
|
||||
@ -60,6 +57,10 @@
|
||||
</fieldset>
|
||||
|
||||
</section>
|
||||
<section class="liste">
|
||||
<a class="icon filter" onclick="filterJusti()"></a>
|
||||
{% include "assiduites/widgets/tableau_justi.j2" %}
|
||||
</section>
|
||||
|
||||
<div class="legende">
|
||||
|
||||
@ -224,12 +225,12 @@
|
||||
if (document.getElementById('justi_journee').checked) {
|
||||
date_deb.setAttribute("show", "date")
|
||||
date_fin.setAttribute("show", "date")
|
||||
document.getElementById("date_fin").classList.add("hidden");
|
||||
document.querySelector(`legend[for="justi_date_fin"]`).removeAttribute("required")
|
||||
|
||||
} else {
|
||||
date_deb.removeAttribute("show")
|
||||
date_fin.removeAttribute("show")
|
||||
document.getElementById("date_fin").classList.remove("hidden");
|
||||
|
||||
document.querySelector(`legend[for="justi_date_fin"]`).setAttribute("required", "")
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,8 +239,12 @@
|
||||
const date_fin = document.querySelector(".page #justi_date_fin")
|
||||
const journee = document.querySelector('.page #justi_journee').checked
|
||||
const deb = date_deb.valueAsObject.date + "T" + (journee ? assi_morning : date_deb.valueAsObject.time)
|
||||
const fin = (journee ? date_deb.valueAsObject.date : date_fin.valueAsObject.date) + "T" + (journee ? assi_evening : date_fin.valueAsObject.time)
|
||||
|
||||
let fin = "T" + (journee ? assi_evening : date_fin.valueAsObject.time)
|
||||
if (journee) {
|
||||
fin = (date_fin.valueAsObject.date || date_deb.valueAsObject.date) + fin
|
||||
} else {
|
||||
fin = date_fin.valueAsObject.date + fin
|
||||
}
|
||||
return {
|
||||
"deb": deb,
|
||||
"fin": fin,
|
||||
|
@ -343,6 +343,9 @@
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
||||
const datePivot = "{{scu.get_assiduites_time_config("pivot")}}".split(":").map((el) => Number(el))
|
||||
|
||||
function getDaysBetweenDates(start, end) {
|
||||
let now = new Date(start);
|
||||
end = new Date(end);
|
||||
@ -476,7 +479,7 @@
|
||||
const matin = [new Date(date), new Date(date)]
|
||||
color = "sans_etat"
|
||||
matin[0].setHours(0, 0, 0, 0)
|
||||
matin[1].setHours(12, 59, 59)
|
||||
matin[1].setHours(...datePivot)
|
||||
|
||||
|
||||
|
||||
@ -515,7 +518,8 @@
|
||||
span_aprem.classList.add("color");
|
||||
const aprem = [new Date(date), new Date(date)]
|
||||
color = "sans_etat"
|
||||
aprem[0].setHours(13, 0, 0, 0)
|
||||
aprem[0].setHours(...datePivot)
|
||||
aprem[0].add(1, "seconds")
|
||||
aprem[1].setHours(23, 59, 59)
|
||||
|
||||
|
||||
|
@ -1,13 +1,24 @@
|
||||
<label for="moduleimpl_select">
|
||||
<div>
|
||||
{% if label != false%}
|
||||
<label for="moduleimpl_select">
|
||||
Module
|
||||
</label>
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% if moduleid %}
|
||||
<select id="{{moduleid}}" class="dynaSelect">
|
||||
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
|
||||
</select>
|
||||
{% else %}
|
||||
<select id="moduleimpl_select" class="dynaSelect">
|
||||
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
|
||||
</select>
|
||||
{% endif %}
|
||||
|
||||
<div id="saved" style="display: none;">
|
||||
{% include "assiduites/widgets/simplemoduleimpl_select.j2" %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
|
@ -88,6 +88,7 @@
|
||||
td.textContent = getModuleImpl(assiduite);
|
||||
} else if (k.indexOf('est_just') != -1) {
|
||||
td.textContent = assiduite[k] ? "Oui" : "Non"
|
||||
if (assiduite[k]) row.classList.add("est_just")
|
||||
} else if (k.indexOf('etudid') != -1) {
|
||||
const e = getEtudiant(assiduite.etudid);
|
||||
|
||||
|
@ -456,6 +456,7 @@
|
||||
td {
|
||||
border: 1px solid #dddddd;
|
||||
padding: 8px;
|
||||
color: var(--color-default-text);
|
||||
}
|
||||
|
||||
th {
|
||||
@ -498,17 +499,25 @@
|
||||
|
||||
.l-absent,
|
||||
.l-invalid {
|
||||
background-color: var(--color-absent);
|
||||
background-color: var(--color-absent-clair);
|
||||
}
|
||||
|
||||
.l-valid {
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-justi-clair);
|
||||
}
|
||||
|
||||
.l-retard {
|
||||
background-color: var(--color-retard);
|
||||
}
|
||||
|
||||
.l-absent.est_just {
|
||||
background-color: var(--color-absent-justi);
|
||||
}
|
||||
|
||||
.l-retard.est_just {
|
||||
background-color: var(--color-retard-justi);
|
||||
}
|
||||
|
||||
/* Ajoutez des styles pour le conteneur de pagination et les boutons */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
|
@ -9,15 +9,18 @@
|
||||
<div class="user_basics">
|
||||
<b>Login :</b> {{user.user_name}}<br>
|
||||
<b>CAS id:</b> {{user.cas_id or "(aucun)"}}
|
||||
{% if ScoDocSiteConfig.is_cas_enabled() %}
|
||||
(CAS {{'autorisé' if user.cas_allow_login else 'interdit'}} pour cet utilisateur)
|
||||
{% if user.cas_allow_scodoc_login %}
|
||||
(connexion sans CAS autorisée)
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<br>
|
||||
<b>Nom :</b> {{user.nom or ""}}<br>
|
||||
<b>Prénom :</b> {{user.prenom or ""}}<br>
|
||||
<b>Mail :</b> {{user.email}}<br>
|
||||
<b>Mail institutionnel:</b> {{user.email_institutionnel or ""}}<br>
|
||||
<b>Identifiant EDT:</b> {{user.edt_id or ""}}<br>
|
||||
<b>Rôles :</b> {{user.get_roles_string()}}<br>
|
||||
<b>Dept :</b> {{user.dept or ""}}<br>
|
||||
{% if user.passwd_temp or user.password_scodoc7 %}
|
||||
|
@ -617,7 +617,7 @@
|
||||
listeGroupesAutoaffectation();
|
||||
})
|
||||
.catch(error => {
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>";
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (1).</h2>";
|
||||
})
|
||||
}
|
||||
|
||||
@ -665,13 +665,13 @@
|
||||
document.querySelector(`#zoneGroupes .partition[data-idpartition="${idPartition}"]`).innerHTML += templateGroupe_zoneGroupes(r.id, name);
|
||||
|
||||
// Lancement de l'édition du nom
|
||||
divGroupe.querySelector(".modif").click();
|
||||
// divGroupe.querySelector(".modif").click();
|
||||
|
||||
listeGroupesAutoaffectation();
|
||||
})
|
||||
.catch(error => {
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>";
|
||||
})
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (4).</h2>";
|
||||
});
|
||||
}
|
||||
|
||||
/********************/
|
||||
@ -746,12 +746,12 @@
|
||||
.then(r => { return r.json() })
|
||||
.then(r => {
|
||||
if (!r) {
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>";
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (2).</h2>";
|
||||
}
|
||||
listeGroupesAutoaffectation();
|
||||
})
|
||||
.catch(error => {
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>";
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (3).</h2>";
|
||||
})
|
||||
}
|
||||
|
||||
@ -802,7 +802,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.</h2>";
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (5).</h2>";
|
||||
}
|
||||
listeGroupesAutoaffectation();
|
||||
})
|
||||
@ -916,12 +916,12 @@
|
||||
.then(r => { return r.json() })
|
||||
.then(r => {
|
||||
if (!r) {
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>";
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (6).</h2>";
|
||||
}
|
||||
listeGroupesAutoaffectation();
|
||||
})
|
||||
.catch(error => {
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données.</h2>";
|
||||
document.querySelector("main").innerHTML = "<h2>Une erreur s'est produite lors de la sauvegarde des données (7).</h2>";
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -293,7 +293,8 @@ def signal_assiduites_etud():
|
||||
"js/date_utils.js",
|
||||
"js/etud_info.js",
|
||||
],
|
||||
cssstyles=[
|
||||
cssstyles=CSSSTYLES
|
||||
+ [
|
||||
"css/assiduites.css",
|
||||
],
|
||||
)
|
||||
@ -318,27 +319,43 @@ def signal_assiduites_etud():
|
||||
header,
|
||||
_mini_timeline(),
|
||||
render_template(
|
||||
"assiduites/pages/signal_assiduites_etud.j2",
|
||||
"assiduites/pages/ajout_assiduites.j2",
|
||||
sco=ScoData(etud),
|
||||
date=_dateiso_to_datefr(date),
|
||||
morning=morning,
|
||||
lunch=lunch,
|
||||
timeline=_timeline(heures=",".join([f"'{s}'" for s in heures])),
|
||||
afternoon=afternoon,
|
||||
nonworkdays=_non_work_days(),
|
||||
forcer_module=sco_preferences.get_preference(
|
||||
"forcer_module", dept_id=g.scodoc_dept_id
|
||||
),
|
||||
diff=_differee(
|
||||
etudiants=[sco_etud.get_etud_info(etudid=etud.etudid, filled=True)[0]],
|
||||
moduleimpl_select=select,
|
||||
assi_limit_annee=sco_preferences.get_preference(
|
||||
"assi_limit_annee",
|
||||
dept_id=g.scodoc_dept_id,
|
||||
),
|
||||
assi_morning=ScoDocSiteConfig.get("assi_morning_time", "08:00"),
|
||||
assi_evening=ScoDocSiteConfig.get("assi_afternoon_time", "18:00"),
|
||||
|
||||
saisie_eval=saisie_eval,
|
||||
date_deb=date_deb,
|
||||
date_fin=date_fin,
|
||||
redirect_url=redirect_url,
|
||||
moduleimpl_id=moduleimpl_id,
|
||||
),
|
||||
# render_template(
|
||||
# "assiduites/pages/signal_assiduites_etud.j2",
|
||||
# sco=ScoData(etud),
|
||||
# date=_dateiso_to_datefr(date),
|
||||
# morning=morning,
|
||||
# lunch=lunch,
|
||||
# timeline=_timeline(heures=",".join([f"'{s}'" for s in heures])),
|
||||
# afternoon=afternoon,
|
||||
# nonworkdays=_non_work_days(),
|
||||
# forcer_module=sco_preferences.get_preference(
|
||||
# "forcer_module", dept_id=g.scodoc_dept_id
|
||||
# ),
|
||||
# diff=_differee(
|
||||
# etudiants=[sco_etud.get_etud_info(etudid=etud.etudid, filled=True)[0]],
|
||||
# moduleimpl_select=select,
|
||||
# ),
|
||||
# saisie_eval=saisie_eval,
|
||||
# date_deb=date_deb,
|
||||
# date_fin=date_fin,
|
||||
# redirect_url=redirect_url,
|
||||
# moduleimpl_id=moduleimpl_id,
|
||||
# ),
|
||||
).build()
|
||||
|
||||
|
||||
|
@ -450,6 +450,17 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
"readonly": edit_only_roles,
|
||||
},
|
||||
),
|
||||
(
|
||||
"edt_id",
|
||||
{
|
||||
"title": "Identifiant sur l'emploi du temps",
|
||||
"input_type": "text",
|
||||
"explanation": """id du compte utilisateur sur l'emploi du temps
|
||||
ou l'annuaire de l'établissement (par défaut, l'e-mail institutionnel )""",
|
||||
"size": 36,
|
||||
"allow_null": True,
|
||||
},
|
||||
),
|
||||
]
|
||||
if not edit: # options création utilisateur
|
||||
descr += [
|
||||
@ -690,10 +701,12 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
log(f"sco_users: editing {user_name} by {current_user.user_name}")
|
||||
log(f"sco_users: previous_values={initvalues}")
|
||||
log(f"sco_users: new_values={vals}")
|
||||
sco_users.user_edit(user_name, vals)
|
||||
flash(f"Utilisateur {user_name} modifié")
|
||||
else:
|
||||
sco_users.user_edit(user_name, {"roles_string": vals["roles_string"]})
|
||||
vals = {"roles_string": vals["roles_string"]}
|
||||
the_user.from_dict(vals)
|
||||
db.session.add(the_user)
|
||||
db.session.commit()
|
||||
flash(f"Utilisateur {user_name} modifié")
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"users.user_info_page",
|
||||
@ -749,7 +762,7 @@ def create_user_form(user_name=None, edit=0, all_roles=True):
|
||||
log(
|
||||
f"""sco_users: new_user {vals["user_name"]} by {current_user.user_name}"""
|
||||
)
|
||||
the_user = User()
|
||||
the_user = User(user_name=user_name)
|
||||
the_user.from_dict(vals, new_user=True)
|
||||
db.session.add(the_user)
|
||||
db.session.commit()
|
||||
@ -916,11 +929,12 @@ def user_info_page(user_name=None):
|
||||
|
||||
return render_template(
|
||||
"auth/user_info_page.j2",
|
||||
user=user,
|
||||
title=f"Utilisateur {user.user_name}",
|
||||
Permission=Permission,
|
||||
dept=dept,
|
||||
Permission=Permission,
|
||||
ScoDocSiteConfig=ScoDocSiteConfig,
|
||||
session_info=session_info,
|
||||
title=f"Utilisateur {user.user_name}",
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
[pytest]
|
||||
norecursedirs = .git app/static
|
||||
markers =
|
||||
slow: marks tests as slow (deselect with '-m "not slow"')
|
||||
apo
|
||||
|
@ -88,7 +88,7 @@ python-editor==1.0.4
|
||||
pytz==2023.3.post1
|
||||
PyYAML==6.0.1
|
||||
redis==5.0.1
|
||||
reportlab==4.0.5
|
||||
reportlab==4.0.7
|
||||
requests==2.31.0
|
||||
rq==1.15.1
|
||||
six==1.16.0
|
||||
|
@ -28,10 +28,11 @@ from tests.api.setup_test_api import (
|
||||
API_URL,
|
||||
API_USER_ADMIN,
|
||||
CHECK_CERTIFICATE,
|
||||
DEPT_ACRONYM,
|
||||
POST_JSON,
|
||||
api_headers,
|
||||
get_auth_headers,
|
||||
)
|
||||
from tests.api.setup_test_api import api_headers # pylint: disable=unused-import
|
||||
from tests.api.tools_test_api import (
|
||||
BULLETIN_ETUDIANT_FIELDS,
|
||||
BULLETIN_FIELDS,
|
||||
@ -923,3 +924,20 @@ def test_etudiant_groups(api_headers):
|
||||
group = groups[0]
|
||||
fields_ok = verify_fields(group, fields)
|
||||
assert fields_ok is True
|
||||
|
||||
|
||||
def test_etudiant_create(api_headers):
|
||||
"""/etudiant/create"""
|
||||
admin_header = get_auth_headers(API_USER_ADMIN, API_PASSWORD_ADMIN)
|
||||
args = {
|
||||
"prenom": "Carl Philipp Emanuel",
|
||||
"nom": "Bach",
|
||||
"dept": DEPT_ACRONYM,
|
||||
"civilite": "M",
|
||||
}
|
||||
etud = POST_JSON(
|
||||
"/etudiant/create",
|
||||
args,
|
||||
headers=admin_header,
|
||||
)
|
||||
assert etud["nom"] == args["nom"].upper()
|
||||
|
@ -90,6 +90,7 @@ def test_formsemestre_partition(api_headers):
|
||||
)
|
||||
assert isinstance(group_r, dict)
|
||||
assert group_r["group_name"] == group_d["group_name"]
|
||||
assert group_r["edt_id"] is None
|
||||
# --- Liste groupes de la partition
|
||||
partition = GET(f"/partition/{partition_r['id']}", headers=headers)
|
||||
assert isinstance(partition, dict)
|
||||
@ -99,6 +100,26 @@ def test_formsemestre_partition(api_headers):
|
||||
group = partition["groups"][str(group_r["id"])] # nb: str car clés json en string
|
||||
assert group["group_name"] == group_d["group_name"]
|
||||
|
||||
# --- Ajout d'un groupe avec edt_id
|
||||
group_d = {"group_name": "extra", "edt_id": "GEDT"}
|
||||
group_r = POST_JSON(
|
||||
f"/partition/{partition_r['id']}/group/create",
|
||||
group_d,
|
||||
headers=headers,
|
||||
)
|
||||
assert group_r["edt_id"] == "GEDT"
|
||||
# Edit edt_id
|
||||
group_r = POST_JSON(
|
||||
f"/group/{group_r['id']}/edit",
|
||||
{"edt_id": "GEDT2"},
|
||||
headers=headers,
|
||||
)
|
||||
assert group_r["edt_id"] == "GEDT2"
|
||||
partition = GET(f"/partition/{partition_r['id']}", headers=headers)
|
||||
group = partition["groups"][str(group_r["id"])] # nb: str car clés json en string
|
||||
assert group["group_name"] == group_d["group_name"]
|
||||
assert group["edt_id"] == "GEDT2"
|
||||
|
||||
# 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)
|
||||
|
@ -88,15 +88,17 @@ def test_edit_users(api_admin_headers):
|
||||
# Change le dept et rend inactif
|
||||
user = POST_JSON(
|
||||
f"/user/{user['id']}/edit",
|
||||
{"active": False, "dept": "TAPI"},
|
||||
{"active": False, "dept": "TAPI", "edt_id": "GGG"},
|
||||
headers=admin_h,
|
||||
)
|
||||
assert user["dept"] == "TAPI"
|
||||
assert user["active"] is False
|
||||
assert user["edt_id"] == "GGG"
|
||||
user = GET(f"/user/{user['id']}", headers=admin_h)
|
||||
assert user["nom"] == "Toto"
|
||||
assert user["dept"] == "TAPI"
|
||||
assert user["active"] is False
|
||||
assert user["edt_id"] == "GGG"
|
||||
|
||||
|
||||
def test_roles(api_admin_headers):
|
||||
|
@ -123,3 +123,30 @@ def test_create_delete(test_client):
|
||||
db.session.commit()
|
||||
ul = User.query.filter_by(prenom="Pierre").all()
|
||||
assert len(ul) == 1
|
||||
|
||||
|
||||
def test_edit(test_client):
|
||||
"test edition object utlisateur"
|
||||
args = {
|
||||
"prenom": "No Totoro",
|
||||
"edt_id": "totorito",
|
||||
"cas_allow_login": 1, # boolean
|
||||
"irrelevant": "..", # intentionnellement en dehors des attributs
|
||||
}
|
||||
u = User(user_name="Tonari")
|
||||
u.from_dict(args)
|
||||
db.session.add(u)
|
||||
db.session.commit()
|
||||
db.session.refresh(u)
|
||||
assert u.edt_id == "totorito"
|
||||
assert u.nom == ""
|
||||
assert u.cas_allow_login is True
|
||||
d = u.to_dict()
|
||||
assert d["nom"] == ""
|
||||
args["cas_allow_login"] = 0
|
||||
u.from_dict(args)
|
||||
db.session.commit()
|
||||
db.session.refresh(u)
|
||||
assert u.cas_allow_login is False
|
||||
db.session.delete(u)
|
||||
db.session.commit()
|
||||
|
Loading…
Reference in New Issue
Block a user