diff --git a/app/api/etudiants.py b/app/api/etudiants.py index 264271cda..03dc217d4 100755 --- a/app/api/etudiants.py +++ b/app/api/etudiants.py @@ -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() diff --git a/app/api/partitions.py b/app/api/partitions.py index 64ed136cc..516fc4177 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -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 - db.session.add(group) - db.session.commit() - log(f"modified {group}") + + 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) diff --git a/app/api/users.py b/app/api/users.py index 4fe895107..3dffd2773 100644 --- a/app/api/users.py +++ b/app/api/users.py @@ -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() diff --git a/app/auth/models.py b/app/auth/models.py index dcf762a2d..759c5bd73 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -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) diff --git a/app/entreprises/__init__.py b/app/entreprises/__init__.py index 121b7e3cb..1d41e77d0 100644 --- a/app/entreprises/__init__.py +++ b/app/entreprises/__init__.py @@ -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() diff --git a/app/entreprises/routes.py b/app/entreprises/routes.py index 60d567abf..529706e99 100644 --- a/app/entreprises/routes.py +++ b/app/entreprises/routes.py @@ -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}", diff --git a/app/models/__init__.py b/app/models/__init__.py index c4e04bd6b..b4eea1636 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -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 diff --git a/app/models/etudiants.py b/app/models/etudiants.py index e20a7b6e7..192d7ff0c 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -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"""{self.nomprenom}""" + return f"""{self.nomprenom}""" + + 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 diff --git a/app/models/events.py b/app/models/events.py index 74583b8a4..06dbe558d 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -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, diff --git a/app/models/groups.py b/app/models/groups.py index 72a54acf6..8b7ed5690 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -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" diff --git a/app/models/scolar_event.py b/app/models/scolar_event.py new file mode 100644 index 000000000..4294efb12 --- /dev/null +++ b/app/models/scolar_event.py @@ -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})" diff --git a/app/models/validations.py b/app/models/validations.py index 14e7a5b7a..17dd12a6b 100644 --- a/app/models/validations.py +++ b/app/models/validations.py @@ -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})" diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index a28728578..9bd09e899 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -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 %(nomprenom)s' % etud, + text=f"Nouvel étudiant {etud.html_link_fiche()}", url=etud["url"], max_frequency=0, ) - return etud + return etud_dict # ---------- "EVENTS" diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py index 3b9cd1305..eade74930 100644 --- a/app/scodoc/sco_find_etud.py +++ b/app/scodoc/sco_find_etud.py @@ -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 diff --git a/app/scodoc/sco_formsemestre_inscriptions.py b/app/scodoc/sco_formsemestre_inscriptions.py index 7837c299f..887a0625e 100644 --- a/app/scodoc/sco_formsemestre_inscriptions.py +++ b/app/scodoc/sco_formsemestre_inscriptions.py @@ -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 @@ -222,10 +222,10 @@ def do_formsemestre_desinscription(etudid, formsemestre_id): cnx = ndb.GetDBConnexion() cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor.execute( - """SELECT Im.id AS moduleimpl_inscription_id + """SELECT Im.id AS moduleimpl_inscription_id FROM notes_moduleimpl_inscription Im, notes_moduleimpl M WHERE Im.etudid=%(etudid)s - and Im.moduleimpl_id = M.id + and Im.moduleimpl_id = M.id and M.formsemestre_id = %(formsemestre_id)s """, {"etudid": etudid, "formsemestre_id": formsemestre_id}, @@ -253,7 +253,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id): nbinscrits = len(inscrits) if nbinscrits == 0: log( - f"""do_formsemestre_desinscription: + f"""do_formsemestre_desinscription: suppression du semestre extérieur {formsemestre}""" ) flash("Semestre exterieur supprimé") @@ -436,7 +436,7 @@ def formsemestre_inscription_with_modules( if inscr is not None: H.append( f""" -

{etud.nomprenom} est déjà inscrit +

{etud.nomprenom} est déjà inscrit dans le semestre {formsemestre.titre_mois()}

") H.append( f"""

Continuer quand même l'inscription

""" @@ -644,7 +644,7 @@ function chkbx_select(field_id, state) { """

Voici la liste des modules du semestre choisi.

- Les modules cochés sont ceux dans lesquels l'étudiant est inscrit. + Les modules cochés sont ceux dans lesquels l'étudiant est inscrit. Vous pouvez l'inscrire ou le désincrire d'un ou plusieurs modules.

Attention: cette méthode ne devrait être utilisée que pour les modules diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index a37e7934c..61b2e571a 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -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), ) ) diff --git a/app/scodoc/sco_groups_edit.py b/app/scodoc/sco_groups_edit.py index 6c6cfd422..bb0429eff 100644 --- a/app/scodoc/sco_groups_edit.py +++ b/app/scodoc/sco_groups_edit.py @@ -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).""", }, ), diff --git a/app/scodoc/sco_import_users.py b/app/scodoc/sco_import_users.py index af0cef3f0..1f06f0284 100644 --- a/app/scodoc/sco_import_users.py +++ b/app/scodoc/sco_import_users.py @@ -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() diff --git a/app/scodoc/sco_prepajury.py b/app/scodoc/sco_prepajury.py index bef329232..ba74b0aa9 100644 --- a/app/scodoc/sco_prepajury.py +++ b/app/scodoc/sco_prepajury.py @@ -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 "", diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py index e67ebea56..87bc1097b 100644 --- a/app/scodoc/sco_saisie_notes.py +++ b/app/scodoc/sco_saisie_notes.py @@ -1244,7 +1244,7 @@ def _form_saisie_notes( '' % classdem + e["civilite_str"] + " " - + sco_etud.format_nomprenom(e, reverse=True) + + scu.format_nomprenom(e, reverse=True) + "" ) diff --git a/app/scodoc/sco_trombino.py b/app/scodoc/sco_trombino.py index 396df98ec..d5fbdebb1 100644 --- a/app/scodoc/sco_trombino.py +++ b/app/scodoc/sco_trombino.py @@ -129,7 +129,7 @@ def trombino_html(groups_infos): H = [ f""" - """ ] @@ -164,9 +164,9 @@ def trombino_html(groups_infos): H.append("") H.append( '' - + sco_etud.format_prenom(t["prenom"]) + + scu.format_prenom(t["prenom"]) + '' - + sco_etud.format_nom(t["nom"]) + + scu.format_nom(t["nom"]) + (" (dem.)" if t["etat"] == "D" else "") ) H.append("") @@ -175,10 +175,10 @@ def trombino_html(groups_infos): H.append("") H.append( f"""
- Version PDF    - Version doc
""" ) @@ -202,9 +202,9 @@ def check_local_photos_availability(groups_infos, fmt=""): return ( False, scu.confirm_dialog( - f"""

Attention: {nb_missing} photos ne sont pas disponibles + f"""

Attention: {nb_missing} photos ne sont pas disponibles et ne peuvent pas être exportées.

-

Vous pouvez Vous pouvez exporter seulement les photos existantes""", dest_url="trombino", @@ -263,11 +263,11 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False): if not dialog_confirmed: return scu.confirm_dialog( f"""

Copier les photos du portail vers ScoDoc ?

-

Les photos du groupe {groups_infos.groups_titles} présentes +

Les photos du groupe {groups_infos.groups_titles} présentes dans ScoDoc seront remplacées par celles du portail (si elles existent).

-

(les photos sont normalement automatiquement copiées - lors de leur première utilisation, l'usage de cette fonction +

(les photos sont normalement automatiquement copiées + lors de leur première utilisation, l'usage de cette fonction n'est nécessaire que si les photos du portail ont été modifiées)

""", @@ -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: diff --git a/app/scodoc/sco_trombino_doc.py b/app/scodoc/sco_trombino_doc.py index b40da5771..7cdbad974 100644 --- a/app/scodoc/sco_trombino_doc.py +++ b/app/scodoc/sco_trombino_doc.py @@ -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) diff --git a/app/scodoc/sco_trombino_tours.py b/app/scodoc/sco_trombino_tours.py index 821c0e9ee..a70965363 100644 --- a/app/scodoc/sco_trombino_tours.py +++ b/app/scodoc/sco_trombino_tours.py @@ -196,9 +196,9 @@ def pdf_trombino_tours( Paragraph( SU( "" - + sco_etud.format_prenom(m["prenom"]) + + scu.format_prenom(m["prenom"]) + " " - + sco_etud.format_nom(m["nom"]) + + scu.format_nom(m["nom"]) + text_group + "" ), @@ -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"], ) ] diff --git a/app/scodoc/sco_users.py b/app/scodoc/sco_users.py index a102700a5..158378cec 100644 --- a/app/scodoc/sco_users.py +++ b/app/scodoc/sco_users.py @@ -102,7 +102,7 @@ def index_html( {menu_roles} - + """ ) @@ -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() diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 1523c8e82..35fb343ea 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -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") diff --git a/app/static/css/assiduites.css b/app/static/css/assiduites.css index fe429ecfc..ed5daa259 100644 --- a/app/static/css/assiduites.css +++ b/app/static/css/assiduites.css @@ -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); diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css index 0f9072c81..f96efa8b4 100644 --- a/app/static/css/scodoc.css +++ b/app/static/css/scodoc.css @@ -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); -} diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index aadd04cae..8b4df342c 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -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 = ` +

Aucun module n'a été spécifié

+ `; + 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 = ` +

Attention, le module doit obligatoirement être renseigné.

+

Cela vient de la configuration du semestre ou plus largement du département.

+

Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

+ `; + + 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 = ` +

Attention, l'étudiant n'est pas inscrit à ce module.

+

Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

+ `; + + 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 = ` +

L'assiduité n'a pas pu être enregistrée car une autre assiduité existe sur la période sélectionnée

+

Si c'est une erreur, veuillez voir avec le ou les responsables de votre scodoc.

+ `; + + 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é diff --git a/app/templates/assiduites/pages/ajout_assiduites.j2 b/app/templates/assiduites/pages/ajout_assiduites.j2 new file mode 100644 index 000000000..311b24a55 --- /dev/null +++ b/app/templates/assiduites/pages/ajout_assiduites.j2 @@ -0,0 +1,234 @@ +{% include "assiduites/widgets/toast.j2" %} +{% block pageContent %} +
+

Ajouter une assiduité

+ {% include "assiduites/widgets/tableau_base.j2" %} + {% if saisie_eval %} +
+
+

+ La saisie de l'assiduité a été préconfigurée en fonction de l'évaluation.
+ Une fois la saisie finie, cliquez sur le lien si dessous pour revenir sur la gestion de l'évaluation +

+ retourner sur la page de l'évaluation +
+ {% endif %} +
+
+
+
+ Date de début + + Journée entière +
+
+ Date de fin + +
+
+ +
+
+ Etat de l'assiduité + +
+
+
+
+ Module + {% with moduleid="ajout_assiduite_module_impl",label=false %} + {% include "assiduites/widgets/moduleimpl_dynamic_selector.j2" %} + {% endwith %} +
+
+ +
+
+ Raison + +
+
+ +
+ + +
+ + +
+ +
+
+ + {% include "assiduites/widgets/tableau_assi.j2" %} +
+ +
+ + + +{% endblock pageContent %} \ No newline at end of file diff --git a/app/templates/assiduites/pages/ajout_justificatif.j2 b/app/templates/assiduites/pages/ajout_justificatif.j2 index fac1b11ed..d74737146 100644 --- a/app/templates/assiduites/pages/ajout_justificatif.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif.j2 @@ -3,10 +3,7 @@

Justifier des absences ou retards

{% include "assiduites/widgets/tableau_base.j2" %} -
- - {% include "assiduites/widgets/tableau_justi.j2" %} -
+
@@ -60,6 +57,10 @@
+
+ + {% include "assiduites/widgets/tableau_justi.j2" %} +
@@ -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, diff --git a/app/templates/assiduites/pages/calendrier.j2 b/app/templates/assiduites/pages/calendrier.j2 index 0f9ef84bd..1b3b59a49 100644 --- a/app/templates/assiduites/pages/calendrier.j2 +++ b/app/templates/assiduites/pages/calendrier.j2 @@ -343,6 +343,9 @@
{group_txt}