API: etudiant/create (WIP), refactoring.

This commit is contained in:
Emmanuel Viennet 2023-11-22 23:31:16 +01:00
parent 532fb3e701
commit 2377918b54
20 changed files with 252 additions and 174 deletions

View File

@ -18,6 +18,7 @@ from sqlalchemy import desc, func, or_
from sqlalchemy.dialects.postgresql import VARCHAR from sqlalchemy.dialects.postgresql import VARCHAR
import app import app
from app import db
from app.api import api_bp as bp, api_web_bp from app.api import api_bp as bp, api_web_bp
from app.api import tools from app.api import tools
from app.but import bulletin_but_court from app.but import bulletin_but_court
@ -28,10 +29,12 @@ from app.models import (
FormSemestreInscription, FormSemestreInscription,
FormSemestre, FormSemestre,
Identite, Identite,
ScolarNews,
) )
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_bulletins import do_formsemestre_bulletinetud 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_permissions import Permission
from app.scodoc.sco_utils import json_error, suppress_accents 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) data = sco_groups.get_etud_groups(etud.id, formsemestre.id)
return data 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()

View File

@ -27,7 +27,6 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS
import app.scodoc.sco_utils as scu 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@\\\-_\.]+$") VALID_LOGIN_EXP = re.compile(r"^[a-zA-Z0-9@\\\-_\.]+$")
@ -462,8 +461,8 @@ class User(UserMixin, db.Model, ScoDocModel):
"""nomplogin est le nom en majuscules suivi du prénom et du login """nomplogin est le nom en majuscules suivi du prénom et du login
e.g. Dupont Pierre (dupont) e.g. Dupont Pierre (dupont)
""" """
nom = sco_etud.format_nom(self.nom) if self.nom else self.user_name.upper() nom = scu.format_nom(self.nom) if self.nom else self.user_name.upper()
return f"{nom} {sco_etud.format_prenom(self.prenom)} ({self.user_name})" return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
@staticmethod @staticmethod
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]: def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
@ -481,29 +480,29 @@ class User(UserMixin, db.Model, ScoDocModel):
def get_nom_fmt(self): def get_nom_fmt(self):
"""Nom formaté: "Martin" """ """Nom formaté: "Martin" """
if self.nom: if self.nom:
return sco_etud.format_nom(self.nom, uppercase=False) return scu.format_nom(self.nom, uppercase=False)
else: else:
return self.user_name return self.user_name
def get_prenom_fmt(self): def get_prenom_fmt(self):
"""Prénom formaté (minuscule capitalisées)""" """Prénom formaté (minuscule capitalisées)"""
return sco_etud.format_prenom(self.prenom) return scu.format_prenom(self.prenom)
def get_nomprenom(self): def get_nomprenom(self):
"""Nom capitalisé suivi de l'initiale du prénom: """Nom capitalisé suivi de l'initiale du prénom:
Viennet E. 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() return (self.get_nom_fmt() + " " + prenom_abbrv).strip()
def get_prenomnom(self): def get_prenomnom(self):
"""L'initiale du prénom suivie du nom: "J.-C. Dupont" """ """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() return (prenom_abbrv + " " + self.get_nom_fmt()).strip()
def get_nomcomplet(self): def get_nomcomplet(self):
"Prénom et nom complets" "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) # nomnoacc était le nom en minuscules sans accents (inutile)

View File

@ -6,6 +6,7 @@ from flask import Blueprint
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.auth.models import User from app.auth.models import User
from app.models import Departement from app.models import Departement
import app.scodoc.sco_utils as scu
bp = Blueprint("entreprises", __name__) bp = Blueprint("entreprises", __name__)
@ -15,12 +16,12 @@ SIRET_PROVISOIRE_START = "xx"
@bp.app_template_filter() @bp.app_template_filter()
def format_prenom(s): def format_prenom(s):
return sco_etud.format_prenom(s) return scu.format_prenom(s)
@bp.app_template_filter() @bp.app_template_filter()
def format_nom(s): def format_nom(s):
return sco_etud.format_nom(s) return scu.format_nom(s)
@bp.app_template_filter() @bp.app_template_filter()

View File

@ -1580,8 +1580,8 @@ def edit_stage_apprentissage(entreprise_id, stage_apprentissage_id):
) )
) )
elif request.method == "GET": elif request.method == "GET":
form.etudiant.data = f"""{sco_etud.format_nom(etudiant.nom)} { form.etudiant.data = f"""{scu.format_nom(etudiant.nom)} {
sco_etud.format_prenom(etudiant.prenom)}""" scu.format_prenom(etudiant.prenom)}"""
form.etudid.data = etudiant.id form.etudid.data = etudiant.id
form.type_offre.data = stage_apprentissage.type_offre form.type_offre.data = stage_apprentissage.type_offre
form.date_debut.data = stage_apprentissage.date_debut form.date_debut.data = stage_apprentissage.date_debut
@ -1699,7 +1699,7 @@ def json_etudiants():
list = [] list = []
for etudiant in etudiants: for etudiant in etudiants:
content = {} 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: if etudiant.inscription_courante() is not None:
content = { content = {
"id": f"{etudiant.id}", "id": f"{etudiant.id}",

View File

@ -71,7 +71,7 @@ class ScoDocModel:
@classmethod @classmethod
def convert_dict_fields(cls, args: dict) -> dict: 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. By default, do nothing, but is overloaded by some subclasses.
args: dict with args in application. args: dict with args in application.
returns: dict to store in model's db. returns: dict to store in model's db.
@ -133,7 +133,6 @@ from app.models.notes import (
NotesNotesLog, NotesNotesLog,
) )
from app.models.validations import ( from app.models.validations import (
ScolarEvent,
ScolarFormSemestreValidation, ScolarFormSemestreValidation,
ScolarAutorisationInscription, ScolarAutorisationInscription,
) )
@ -152,3 +151,4 @@ from app.models.but_validations import ApcValidationAnnee, ApcValidationRCUE
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.models.assiduites import Assiduite, Justificatif from app.models.assiduites import Assiduite, Justificatif
from app.models.scolar_event import ScolarEvent

View File

@ -15,7 +15,7 @@ from sqlalchemy import desc, text
from app import db, log from app import db, log
from app import models from app import models
from app.models.scolar_event import ScolarEvent
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc.sco_bac import Baccalaureat from app.scodoc.sco_bac import Baccalaureat
from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError from app.scodoc.sco_exceptions import ScoInvalidParamError, ScoValueError
@ -170,9 +170,13 @@ class Identite(db.Model, models.ScoDocModel):
def html_link_fiche(self) -> str: def html_link_fiche(self) -> str:
"lien vers la fiche" "lien vers la fiche"
return f"""<a class="stdlink" href="{ return f"""<a class="stdlink" href="{self.url_fiche()}">{self.nomprenom}</a>"""
url_for("scolar.ficheEtud", scodoc_dept=self.departement.acronym, etudid=self.id)
}">{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 @classmethod
def from_request(cls, etudid=None, code_nip=None) -> "Identite": def from_request(cls, etudid=None, code_nip=None) -> "Identite":
@ -211,6 +215,10 @@ class Identite(db.Model, models.ScoDocModel):
etud.admission = Admission() etud.admission = Admission()
etud.adresses.append(Adresse(typeadresse="domicile")) etud.adresses.append(Adresse(typeadresse="domicile"))
db.session.flush() db.session.flush()
event = ScolarEvent(etud=etud, event_type="CREATION")
db.session.add(event)
log(f"Identite.create {etud}")
return etud return etud
@property @property

View File

@ -133,7 +133,7 @@ class ScolarNews(db.Model):
return query.order_by(cls.date.desc()).limit(n).all() return query.order_by(cls.date.desc()).limit(n).all()
@classmethod @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 """Enregistre une nouvelle
Si max_frequency, ne génère pas 2 nouvelles "identiques" Si max_frequency, ne génère pas 2 nouvelles "identiques"
à moins de max_frequency secondes d'intervalle (10 minutes par défaut). à 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). même (obj, typ, user).
La nouvelle enregistrée est aussi envoyée par mail. 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: if max_frequency:
last_news = ( last_news = (
cls.query.filter_by( cls.query.filter_by(
dept_id=g.scodoc_dept_id, dept_id=dept_id,
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
type=typ, type=typ,
object=obj, object=obj,
@ -163,7 +164,7 @@ class ScolarNews(db.Model):
return return
news = ScolarNews( news = ScolarNews(
dept_id=g.scodoc_dept_id, dept_id=dept_id,
authenticated_user=current_user.user_name, authenticated_user=current_user.user_name,
type=typ, type=typ,
object=obj, object=obj,

View 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})"

View File

@ -1,6 +1,6 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
"""Notes, décisions de jury, évènements scolaires """Notes, décisions de jury
""" """
from app import db from app import db
@ -218,47 +218,3 @@ class ScolarAutorisationInscription(db.Model):
msg=f"Passage vers S{autorisation.semestre_id}: effacé", msg=f"Passage vers S{autorisation.semestre_id}: effacé",
) )
db.session.flush() 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})"

View File

@ -45,6 +45,12 @@ from app.models.etudiants import (
pivot_year, pivot_year,
) )
import app.scodoc.sco_utils as scu 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 import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
from app.scodoc import safehtml from app.scodoc import safehtml
@ -102,60 +108,6 @@ def force_uppercase(s):
return s.upper() if s else 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: def _format_etat_civil(etud: dict) -> str:
"Mme Béatrice DUPONT, en utilisant les données d'état civil si indiquées." "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"]: if etud["prenom_etat_civil"] or etud["civilite_etat_civil"]:
@ -657,16 +609,6 @@ def create_etud(cnx, args: dict = None):
db.session.commit() db.session.commit()
etudid = etud.id 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 # log
logdb( logdb(
cnx, cnx,
@ -674,16 +616,18 @@ def create_etud(cnx, args: dict = None):
etudid=etudid, etudid=etudid,
msg="creation initiale", msg="creation initiale",
) )
etud = etudident_list(cnx, {"etudid": etudid})[0] etud_dict = etudident_list(cnx, {"etudid": etudid})[0]
fill_etuds_info([etud]) fill_etuds_info([etud_dict])
etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) etud_dict["url"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid
)
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_INSCR, 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"], url=etud["url"],
max_frequency=0, max_frequency=0,
) )
return etud return etud_dict
# ---------- "EVENTS" # ---------- "EVENTS"

View File

@ -42,6 +42,7 @@ from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_utils as scu
def form_search_etud( def form_search_etud(
@ -271,7 +272,7 @@ def search_etud_by_name(term: str) -> list:
data = [ data = [
{ {
"label": "%s %s %s" "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"], "value": x["code_nip"],
} }
for x in r for x in r
@ -290,7 +291,7 @@ def search_etud_by_name(term: str) -> list:
data = [ 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"], "value": x["etudid"],
} }
for x in r for x in r

View File

@ -39,7 +39,7 @@ from app.comp.res_compat import NotesTableCompat
from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog from app.models import Formation, FormSemestre, FormSemestreInscription, Scolog
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.groups import Partition, GroupDescr 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 import app.scodoc.sco_utils as scu
from app import log from app import log
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
@ -222,10 +222,10 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
"""SELECT Im.id AS moduleimpl_inscription_id """SELECT Im.id AS moduleimpl_inscription_id
FROM notes_moduleimpl_inscription Im, notes_moduleimpl M FROM notes_moduleimpl_inscription Im, notes_moduleimpl M
WHERE Im.etudid=%(etudid)s WHERE Im.etudid=%(etudid)s
and Im.moduleimpl_id = M.id and Im.moduleimpl_id = M.id
and M.formsemestre_id = %(formsemestre_id)s and M.formsemestre_id = %(formsemestre_id)s
""", """,
{"etudid": etudid, "formsemestre_id": formsemestre_id}, {"etudid": etudid, "formsemestre_id": formsemestre_id},
@ -253,7 +253,7 @@ def do_formsemestre_desinscription(etudid, formsemestre_id):
nbinscrits = len(inscrits) nbinscrits = len(inscrits)
if nbinscrits == 0: if nbinscrits == 0:
log( log(
f"""do_formsemestre_desinscription: f"""do_formsemestre_desinscription:
suppression du semestre extérieur {formsemestre}""" suppression du semestre extérieur {formsemestre}"""
) )
flash("Semestre exterieur supprimé") flash("Semestre exterieur supprimé")
@ -436,7 +436,7 @@ def formsemestre_inscription_with_modules(
if inscr is not None: if inscr is not None:
H.append( H.append(
f""" f"""
<p class="warning">{etud.nomprenom} est déjà inscrit <p class="warning">{etud.nomprenom} est déjà inscrit
dans le semestre {formsemestre.titre_mois()} dans le semestre {formsemestre.titre_mois()}
</p> </p>
<ul> <ul>
@ -482,8 +482,8 @@ def formsemestre_inscription_with_modules(
H.append("</ul>") H.append("</ul>")
H.append( H.append(
f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules", f"""<p><a href="{ url_for( "notes.formsemestre_inscription_with_modules",
scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id, scodoc_dept=g.scodoc_dept, etudid=etudid, formsemestre_id=formsemestre_id,
multiple_ok=1, multiple_ok=1,
group_ids=group_ids ) group_ids=group_ids )
}">Continuer quand même l'inscription</a> }">Continuer quand même l'inscription</a>
</p>""" </p>"""
@ -644,7 +644,7 @@ function chkbx_select(field_id, state) {
""" """
<p>Voici la liste des modules du semestre choisi.</p> <p>Voici la liste des modules du semestre choisi.</p>
<p> <p>
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. Vous pouvez l'inscrire ou le désincrire d'un ou plusieurs modules.
</p> </p>
<p>Attention: cette méthode ne devrait être utilisée que pour les modules <p>Attention: cette méthode ne devrait être utilisée que pour les modules

View File

@ -53,6 +53,7 @@ from app.scodoc import codes_cursus
from app.scodoc import sco_cursus from app.scodoc import sco_cursus
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc.sco_etud import etud_sort_key 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 import sco_xml
from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError from app.scodoc.sco_exceptions import ScoException, AccessDenied, ScoValueError
from app.scodoc.TrivialFormulator import TrivialFormulator from app.scodoc.TrivialFormulator import TrivialFormulator
@ -573,8 +574,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
etudid=str(e["etudid"]), etudid=str(e["etudid"]),
civilite=etud["civilite_str"] or "", civilite=etud["civilite_str"] or "",
sexe=etud["civilite_str"] or "", # compat sexe=etud["civilite_str"] or "", # compat
nom=sco_etud.format_nom(etud["nom"] or ""), nom=scu.format_nom(etud["nom"] or ""),
prenom=sco_etud.format_prenom(etud["prenom"] or ""), prenom=scu.format_prenom(etud["prenom"] or ""),
origin=_comp_etud_origin(etud, formsemestre), origin=_comp_etud_origin(etud, formsemestre),
) )
) )
@ -599,8 +600,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
"etud", "etud",
etudid=str(etud["etudid"]), etudid=str(etud["etudid"]),
sexe=etud["civilite_str"] or "", sexe=etud["civilite_str"] or "",
nom=sco_etud.format_nom(etud["nom"] or ""), nom=scu.format_nom(etud["nom"] or ""),
prenom=sco_etud.format_prenom(etud["prenom"] or ""), prenom=scu.format_prenom(etud["prenom"] or ""),
origin=_comp_etud_origin(etud, formsemestre), origin=_comp_etud_origin(etud, formsemestre),
) )
) )

View File

@ -244,8 +244,8 @@ def feuille_preparation_jury(formsemestre_id):
[ [
etud.id, etud.id,
etud.civilite_str, etud.civilite_str,
sco_etud.format_nom(etud.nom), scu.format_nom(etud.nom),
sco_etud.format_prenom(etud.prenom), scu.format_prenom(etud.prenom),
etud.date_naissance, etud.date_naissance,
etud.admission.bac if etud.admission else "", etud.admission.bac if etud.admission else "",
etud.admission.specialite if etud.admission else "", etud.admission.specialite if etud.admission else "",

View File

@ -1244,7 +1244,7 @@ def _form_saisie_notes(
'<span class="%s">' % classdem '<span class="%s">' % classdem
+ e["civilite_str"] + e["civilite_str"]
+ " " + " "
+ sco_etud.format_nomprenom(e, reverse=True) + scu.format_nomprenom(e, reverse=True)
+ "</span>" + "</span>"
) )

View File

@ -129,7 +129,7 @@ def trombino_html(groups_infos):
H = [ H = [
f"""<table style="padding-top: 10px; padding-bottom: 10px;"> f"""<table style="padding-top: 10px; padding-bottom: 10px;">
<tr> <tr>
<td><span <td><span
style="font-style: bold; font-size: 150%%; padding-right: 20px;" style="font-style: bold; font-size: 150%%; padding-right: 20px;"
>{group_txt}</span></td>""" >{group_txt}</span></td>"""
] ]
@ -164,9 +164,9 @@ def trombino_html(groups_infos):
H.append("</span>") H.append("</span>")
H.append( H.append(
'<span class="trombi_legend"><span class="trombi_prenom">' '<span class="trombi_legend"><span class="trombi_prenom">'
+ sco_etud.format_prenom(t["prenom"]) + scu.format_prenom(t["prenom"])
+ '</span><span class="trombi_nom">' + '</span><span class="trombi_nom">'
+ sco_etud.format_nom(t["nom"]) + scu.format_nom(t["nom"])
+ (" <i>(dem.)</i>" if t["etat"] == "D" else "") + (" <i>(dem.)</i>" if t["etat"] == "D" else "")
) )
H.append("</span></span></span>") H.append("</span></span></span>")
@ -175,10 +175,10 @@ def trombino_html(groups_infos):
H.append("</div>") H.append("</div>")
H.append( H.append(
f"""<div style="margin-bottom:15px;"> f"""<div style="margin-bottom:15px;">
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept, <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
fmt='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a> fmt='pdf', group_ids=groups_infos.group_ids)}">Version PDF</a>
&nbsp;&nbsp; &nbsp;&nbsp;
<a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept, <a class="stdlink" href="{url_for('scolar.trombino', scodoc_dept=g.scodoc_dept,
fmt='doc', group_ids=groups_infos.group_ids)}">Version doc</a> fmt='doc', group_ids=groups_infos.group_ids)}">Version doc</a>
</div>""" </div>"""
) )
@ -202,9 +202,9 @@ def check_local_photos_availability(groups_infos, fmt=""):
return ( return (
False, False,
scu.confirm_dialog( scu.confirm_dialog(
f"""<p>Attention: {nb_missing} photos ne sont pas disponibles f"""<p>Attention: {nb_missing} photos ne sont pas disponibles
et ne peuvent pas être exportées.</p> et ne peuvent pas être exportées.</p>
<p>Vous pouvez <a class="stdlink" <p>Vous pouvez <a class="stdlink"
href="{groups_infos.base_url}&dialog_confirmed=1&fmt={fmt}" href="{groups_infos.base_url}&dialog_confirmed=1&fmt={fmt}"
>exporter seulement les photos existantes</a>""", >exporter seulement les photos existantes</a>""",
dest_url="trombino", dest_url="trombino",
@ -263,11 +263,11 @@ def trombino_copy_photos(group_ids=[], dialog_confirmed=False):
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
f"""<h2>Copier les photos du portail vers ScoDoc ?</h2> f"""<h2>Copier les photos du portail vers ScoDoc ?</h2>
<p>Les photos du groupe {groups_infos.groups_titles} présentes <p>Les photos du groupe {groups_infos.groups_titles} présentes
dans ScoDoc seront remplacées par celles du portail (si elles existent). dans ScoDoc seront remplacées par celles du portail (si elles existent).
</p> </p>
<p>(les photos sont normalement automatiquement copiées <p>(les photos sont normalement automatiquement copiées
lors de leur première utilisation, l'usage de cette fonction 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) n'est nécessaire que si les photos du portail ont été modifiées)
</p> </p>
""", """,
@ -349,7 +349,7 @@ def _trombino_pdf(groups_infos):
[img], [img],
[ [
Paragraph( Paragraph(
SU(sco_etud.format_nomprenom(t)), SU(scu.format_nomprenom(t)),
style_sheet["Normal"], style_sheet["Normal"],
) )
], ],
@ -428,7 +428,7 @@ def _listeappel_photos_pdf(groups_infos):
t = groups_infos.members[i] t = groups_infos.members[i]
img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH) img = _get_etud_platypus_image(t, image_width=PHOTO_WIDTH)
txt = Paragraph( txt = Paragraph(
SU(sco_etud.format_nomprenom(t)), SU(scu.format_nomprenom(t)),
style_sheet["Normal"], style_sheet["Normal"],
) )
if currow: if currow:

View File

@ -55,7 +55,7 @@ def trombino_doc(groups_infos):
cell = table.rows[2 * li + 1].cells[co] cell = table.rows[2 * li + 1].cells[co]
cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP
cell_p, cell_f, cell_r = _paragraph_format_run(cell) 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) cell_f.space_after = Mm(8)
return scu.send_docx(document, filename) return scu.send_docx(document, filename)

View File

@ -196,9 +196,9 @@ def pdf_trombino_tours(
Paragraph( Paragraph(
SU( SU(
"<para align=center><font size=8>" "<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 + text_group
+ "</font></para>" + "</font></para>"
), ),
@ -413,11 +413,7 @@ def pdf_feuille_releve_absences(
for m in members: for m in members:
currow = [ currow = [
Paragraph( Paragraph(
SU( SU(scu.format_nom(m["nom"]) + " " + scu.format_prenom(m["prenom"])),
sco_etud.format_nom(m["nom"])
+ " "
+ sco_etud.format_prenom(m["prenom"])
),
StyleSheet["Normal"], StyleSheet["Normal"],
) )
] ]

View File

@ -64,6 +64,7 @@ from config import Config
from app import log, ScoDocJSONEncoder from app import log, ScoDocJSONEncoder
from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL from app.scodoc.codes_cursus import NOTES_TOLERANCE, CODES_EXPL
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc import sco_xml from app.scodoc import sco_xml
import sco_version import sco_version
@ -1139,6 +1140,61 @@ def abbrev_prenom(prenom):
return abrv 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(): def timedate_human_repr():
"representation du temps courant pour utilisateur" "representation du temps courant pour utilisateur"
@ -1480,6 +1536,7 @@ def is_assiduites_module_forced(
def get_assiduites_time_config(config_type: str) -> str: def get_assiduites_time_config(config_type: str) -> str:
from app.models import ScoDocSiteConfig from app.models import ScoDocSiteConfig
match config_type: match config_type:
case "matin": case "matin":
return ScoDocSiteConfig.get("assi_morning_time", "08:00:00") return ScoDocSiteConfig.get("assi_morning_time", "08:00:00")

View File

@ -28,10 +28,11 @@ from tests.api.setup_test_api import (
API_URL, API_URL,
API_USER_ADMIN, API_USER_ADMIN,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
DEPT_ACRONYM,
POST_JSON, POST_JSON,
api_headers,
get_auth_headers, get_auth_headers,
) )
from tests.api.setup_test_api import api_headers # pylint: disable=unused-import
from tests.api.tools_test_api import ( from tests.api.tools_test_api import (
BULLETIN_ETUDIANT_FIELDS, BULLETIN_ETUDIANT_FIELDS,
BULLETIN_FIELDS, BULLETIN_FIELDS,
@ -923,3 +924,20 @@ def test_etudiant_groups(api_headers):
group = groups[0] group = groups[0]
fields_ok = verify_fields(group, fields) fields_ok = verify_fields(group, fields)
assert fields_ok is True 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()