diff --git a/app/api/jury.py b/app/api/jury.py
index 8005910b6e5e7dcdf383f205048bc11ac234670d..b58b35c2c7e9bfc669b6bcd7216e9882fc9b6c49 100644
--- a/app/api/jury.py
+++ b/app/api/jury.py
@@ -75,14 +75,12 @@ def decisions_jury(formsemestre_id: int):
def _news_delete_jury_etud(etud: Identite, detail: str = ""):
"génère news sur effacement décision"
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
- url = url_for(
- "scolar.fiche_etud", scodoc_dept=etud.departement.acronym, etudid=etud.id
- )
ScolarNews.add(
typ=ScolarNews.NEWS_JURY,
obj=etud.id,
- text=f"""Suppression décision jury {detail} pour <a href="{url}">{etud.nomprenom}</a>""",
- url=url,
+ text=f"""Suppression décision jury {detail} pour {etud.html_link_fiche()}""",
+ url=etud.url_fiche(),
+ dept_id=etud.dept_id,
)
Scolog.logdb(
"jury_delete_manual",
diff --git a/app/but/jury_but_validation_auto.py b/app/but/jury_but_validation_auto.py
index 94ee7091fde077e69489708ed6f16376ab0423a6..72ea51ca254028109a16ed08e440b3acc940c11a 100644
--- a/app/but/jury_but_validation_auto.py
+++ b/app/but/jury_but_validation_auto.py
@@ -59,6 +59,7 @@ def formsemestre_validation_auto_but(
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
+ dept_id=formsemestre.dept_id,
)
return nb_etud_modif, decas
diff --git a/app/email.py b/app/email.py
index 3f9b03449058a7462209e7b782d1a1c17e9bc166..4f64bd14477ab177d2dbb4be37c51e4470006930 100644
--- a/app/email.py
+++ b/app/email.py
@@ -67,7 +67,10 @@ def send_message(msg: Message):
specified debugging address.
"""
email_test_mode_address = False
- if hasattr(g, "scodoc_dept"):
+ if current_app.config.get("DISABLE_EMAILS"):
+ log("send_message: emails disabled by config")
+ return
+ if getattr(g, "scodoc_dept"):
# on est dans un département, on peut accéder aux préférences
email_test_mode_address = sco_preferences.get_preference(
"email_test_mode_address"
diff --git a/app/formations/edit_formation.py b/app/formations/edit_formation.py
index 593199a09adc826d6acb007f8b48b37019b512ef..b796492a58d420d3f3da76a7c08efab8b52cfb18 100644
--- a/app/formations/edit_formation.py
+++ b/app/formations/edit_formation.py
@@ -106,13 +106,14 @@ def do_formation_delete(formation_id):
formation: Formation = db.session.get(Formation, formation_id)
if formation is None:
return
+ dept = formation.departement
acronyme = formation.acronyme
if formation.formsemestres.count():
raise ScoNonEmptyFormationObject(
type_objet="formation",
msg=formation.titre,
dest_url=url_for(
- "notes.ue_table", scodoc_dept=g.scodoc_dept, formation_id=formation.id
+ "notes.ue_table", scodoc_dept=dept.acronym, formation_id=formation.id
),
)
@@ -133,6 +134,7 @@ def do_formation_delete(formation_id):
obj=formation_id,
text=f"Suppression de la formation {acronyme}",
max_frequency=0,
+ dept_id=dept.id,
)
diff --git a/app/formations/edit_ue.py b/app/formations/edit_ue.py
index 6828eb564fcd835549f32765f87fd302f82d8027..aa0e911f052d12079b8901f74b7b3af28791bda5 100644
--- a/app/formations/edit_ue.py
+++ b/app/formations/edit_ue.py
@@ -111,6 +111,7 @@ def do_ue_create(args, allow_empty_ue_code=False) -> UniteEns:
typ=ScolarNews.NEWS_FORM,
obj=args["formation_id"],
text=f"Modification de la formation {ue.formation.acronyme}",
+ dept_id=ue.formation.dept_id,
)
return ue
@@ -195,6 +196,7 @@ def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
+ dept_id=formation.dept_id,
)
#
if not force:
diff --git a/app/models/__init__.py b/app/models/__init__.py
index 0ca9a70cd38eebfe2892c320b0fcd719d1c3e57d..05315aca97e68d5009afde47425352abdcb09a7c 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -98,19 +98,20 @@ class ScoDocModel(db.Model):
# virtual, by default, do nothing
return args
- def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
+ def from_dict(self, args: dict, excluded: set[str] | None = None) -> list[str]:
"""Update object's fields given in dict. Add to session but don't commit.
- True if modification.
+ Returns list of modified fields.
"""
args_dict = self.convert_dict_fields(
self.filter_model_attributes(args, excluded=excluded)
)
- modified = False
+ modified = []
for key, value in args_dict.items():
if hasattr(self, key) and value != getattr(self, key):
setattr(self, key, value)
- modified = True
- db.session.add(self)
+ modified.append(key)
+ if modified:
+ db.session.add(self)
return modified
def to_dict(self) -> dict:
@@ -119,9 +120,9 @@ class ScoDocModel(db.Model):
d.pop("_sa_instance_state", None)
return d
- def edit_from_form(self, form) -> bool:
+ def edit_from_form(self, form) -> list[str]:
"""Generic edit method for updating model instance.
- True if modification.
+ Returns list of modified fields.
"""
args = {field.name: field.data for field in form}
return self.from_dict(args)
diff --git a/app/models/etudiants.py b/app/models/etudiants.py
index 1dc846e14a76788d9653e0a7f139681d175b3dad..bf337158df12ea3e5787edafabf7bff1d75491f4 100644
--- a/app/models/etudiants.py
+++ b/app/models/etudiants.py
@@ -16,6 +16,7 @@ from sqlalchemy import desc, text
from app import db, log
from app import models
from app.models.departements import Departement
+from app.models.events import ScolarNews, Scolog
from app.models.scolar_event import ScolarEvent
from app.scodoc import notesdb as ndb
from app.scodoc.sco_bac import Baccalaureat
@@ -294,7 +295,22 @@ class Identite(models.ScoDocModel):
"""
check_etud_duplicate_code(args, "code_nip", etudid=self.id)
check_etud_duplicate_code(args, "code_ine", etudid=self.id)
- return super().from_dict(args, **kwargs)
+ modified = super().from_dict(args, **kwargs)
+ if modified:
+ msg = f"""Modification de l'étudiant {self.html_link_fiche()} id={self.id} nip={
+ self.code_nip or ''}: {", ".join([f"{k}={args[k]}" for k in modified])}"""
+ Scolog.logdb(
+ method="etudident_delete", etudid=self.id, msg=msg, commit=True
+ )
+ # news
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_ETUD,
+ obj=self.id,
+ text=msg,
+ max_frequency=0,
+ dept_id=self.dept_id,
+ )
+ return modified
@classmethod
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict:
@@ -497,7 +513,11 @@ class Identite(models.ScoDocModel):
elif key == "boursier":
value = scu.to_bool(value)
elif key == "date_naissance":
- value = ndb.DateDMYtoISO(value)
+ value = (
+ datetime.date.fromisoformat(ndb.DateDMYtoISO(value))
+ if value
+ else None
+ )
args_dict[key] = value
return args_dict
diff --git a/app/models/evaluations.py b/app/models/evaluations.py
index 3524579b6b45e3fc564b4ce721ff06b3b385f551..4eb738cf2c140a43812b0174c151f302cb5c4c11 100644
--- a/app/models/evaluations.py
+++ b/app/models/evaluations.py
@@ -149,6 +149,7 @@ class Evaluation(models.ScoDocModel):
text=f"""Création d'une évaluation dans <a href="{url}">{
moduleimpl.module.titre_str()}</a>""",
url=url,
+ dept_id=moduleimpl.formsemestre.dept_id,
)
return evaluation
@@ -217,6 +218,7 @@ class Evaluation(models.ScoDocModel):
url
}">{modimpl.module.titre}</a>""",
url=url,
+ dept_id=modimpl.formsemestre.dept_id,
)
def to_dict(self) -> dict:
diff --git a/app/models/events.py b/app/models/events.py
index e0285d9c94c75529bda1555dfea068037a487614..c2be93b70b0dddf3ca5577ae6ef2fc788aa49b04 100644
--- a/app/models/events.py
+++ b/app/models/events.py
@@ -67,6 +67,7 @@ class ScolarNews(ScoDocModel):
NEWS_ABS = "ABS" # saisie absence
NEWS_APO = "APO" # changements de codes APO
+ NEWS_ETUD = "ETUD" # modification étudiant (object=etudid)
NEWS_FORM = "FORM" # modification formation (object=formation_id)
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
NEWS_JURY = "JURY" # saisie jury
@@ -77,6 +78,7 @@ class ScolarNews(ScoDocModel):
NEWS_MAP = {
NEWS_ABS: "saisie absence",
NEWS_APO: "modif. code Apogée",
+ NEWS_ETUD: "modification étudiant",
NEWS_FORM: "modification formation",
NEWS_INSCR: "inscription d'étudiants",
NEWS_JURY: "saisie jury",
@@ -219,7 +221,6 @@ class ScolarNews(ScoDocModel):
def notify_by_mail(self):
"""Notify by email"""
formsemestre = self.get_news_formsemestre()
-
prefs = sco_preferences.SemPreferences(
formsemestre_id=formsemestre.id if formsemestre else None
)
@@ -232,10 +233,11 @@ class ScolarNews(ScoDocModel):
txt = self.text
if formsemestre:
txt += f"""\n\nSemestre {formsemestre.titre_mois()}\n\n"""
- txt += f"""<a href="{url_for("notes.formsemestre_status", _external=True,
- scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
- }">{formsemestre.sem_modalite()}</a>
- """
+ if formsemestre.departement:
+ txt += f"""<a href="{url_for("notes.formsemestre_status", _external=True,
+ scodoc_dept=formsemestre.departement.acronym, formsemestre_id=formsemestre.id)
+ }">{formsemestre.sem_modalite()}</a>
+ """
user = User.query.filter_by(user_name=self.authenticated_user).first()
if user:
txt += f"\n\nEffectué par: {user.get_nomcomplet()}\n"
@@ -250,10 +252,13 @@ class ScolarNews(ScoDocModel):
)
# Transforme les URL en URL absolues
- base = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
- : -len("/index_html")
- ]
- txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
+ if (
+ formsemestre and formsemestre.departement
+ ): # evite pb lors des tests unitaires
+ base = url_for(
+ "scolar.index_html", scodoc_dept=formsemestre.departement.acronym
+ )[: -len("/index_html")]
+ txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
# (si on veut des messages non html)
diff --git a/app/models/formations.py b/app/models/formations.py
index 9ed7cd0a3e8d2278ae14c767f6ce34456ecfecea..a05573721619acc63951a108cadefabf31e8b35f 100644
--- a/app/models/formations.py
+++ b/app/models/formations.py
@@ -374,6 +374,7 @@ class Matiere(ScoDocModel):
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
+ dept_id=formation.dept_id,
)
# cache
formation.invalidate_cached_sems()
@@ -401,6 +402,7 @@ class Matiere(ScoDocModel):
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
+ dept_id=formation.dept_id,
)
formation.invalidate_cached_sems()
return mat
diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py
index f8c4638d93e88ce8c7f60e59b86c1dda25bff843..acd7b3ee6d6a851d8ce121de47333cb147b823f5 100644
--- a/app/models/formsemestre.py
+++ b/app/models/formsemestre.py
@@ -270,6 +270,7 @@ class FormSemestre(models.ScoDocModel):
text=f"""Création du semestre <a href="{url}">{formsemestre.titre}</a>""",
url=url,
max_frequency=0,
+ dept_id=formsemestre.dept_id,
)
return formsemestre
diff --git a/app/models/modules.py b/app/models/modules.py
index 5a8752a55f76948bf0fe73efb05166a74046bcac..37cd3dce10c74cd1bfccdba8c65b702aef773e86 100644
--- a/app/models/modules.py
+++ b/app/models/modules.py
@@ -153,9 +153,9 @@ class Module(models.ScoDocModel):
query = query.filter(Module.id != module_id)
return query.count() == 0
- def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
+ def from_dict(self, args: dict, excluded: set[str] | None = None) -> list[str]:
"""Update object's fields given in dict. Add to session but don't commit.
- True if modification.
+ Returns list of modified fields.
- can't change ue nor formation
- can change matiere_id, iff new matiere in same ue
- can change parcours: parcours list of ApcParcour id or instances.
@@ -251,6 +251,7 @@ class Module(models.ScoDocModel):
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
+ dept_id=formation.dept_id,
)
if inval_cache:
formation.invalidate_cached_sems()
@@ -301,6 +302,7 @@ class Module(models.ScoDocModel):
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
+ dept_id=formation.dept_id,
)
formation.invalidate_cached_sems()
diff --git a/app/models/ues.py b/app/models/ues.py
index 7cf25bc63238a9511339125ec69a7061087ee6c6..dfa3ab05e694987bd85d9121bde14e11785e374a 100644
--- a/app/models/ues.py
+++ b/app/models/ues.py
@@ -126,9 +126,9 @@ class UniteEns(models.ScoDocModel):
return args
- def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
+ def from_dict(self, args: dict, excluded: set[str] | None = None) -> list[str]:
"""Update object's fields given in dict. Add to session but don't commit.
- True if modification.
+ Returns list of modified fields.
- can't change formation nor niveau_competence
"""
return super().from_dict(
diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index c926c4ff8b6cb38eeab9d43ddba0dafc925e2cad..d24d64acc149e4355e87ef356305b8a7b2243d3b 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -1534,6 +1534,7 @@ def do_formsemestre_delete(formsemestre_id: int):
No checks, no warnings: erase all !
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
+ dept_id = formsemestre.dept_id
sco_cache.EvaluationCache.invalidate_sem(formsemestre.id)
titre_sem = formsemestre.titre_annee()
# --- Destruction des modules de ce semestre
@@ -1689,6 +1690,7 @@ def do_formsemestre_delete(formsemestre_id: int):
obj=formsemestre_id,
text=f"Suppression du semestre {titre_sem}",
max_frequency=0,
+ dept_id=dept_id,
)
diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py
index 2cf1794c5c049b6fad7aab480c8ea3de63fec18a..ef289312b9b853e0c07d502fc2e4faa79ba9beb9 100644
--- a/app/scodoc/sco_preferences.py
+++ b/app/scodoc/sco_preferences.py
@@ -287,6 +287,7 @@ class BasePreferences:
def __init__(self, dept_id: int):
dept = db.session.get(Departement, dept_id)
if not dept:
+ breakpoint()
log(f"BasePreferences: Invalid departement: {dept_id}")
raise ScoValueError(f"BasePreferences: Invalid departement: {dept_id}")
self.dept_id = dept.id
diff --git a/app/views/scolar.py b/app/views/scolar.py
index 756b051e19c64f4f62f744d24ddc0f3f50ee3aa2..8543b1f698ac747977feff521bb1a90d125ba455 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -1918,7 +1918,7 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False):
ins.formsemestre_id for ins in etud.formsemestre_inscriptions
]
- # delete in all tables !
+ # delete in all tables (except scolog) !
# c'est l'ancienne façon de gérer les cascades dans notre pseudo-ORM :)
tables = [
"notes_appreciations",
@@ -1926,6 +1926,7 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False):
"scolar_formsemestre_validation",
"apc_validation_rcue",
"apc_validation_annee",
+ "validation_dut120",
"scolar_events",
"notes_notes_log",
"notes_notes",
@@ -1933,7 +1934,6 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False):
"notes_formsemestre_inscription",
"group_membership",
"etud_annotations",
- "scolog",
"adresse",
"absences",
"absences_notifications",
@@ -1943,6 +1943,19 @@ def etudident_delete(etudid: int = -1, dialog_confirmed=False):
db.session.execute(
sa.text(f"""delete from {table} where etudid=:etudid"""), {"etudid": etudid}
)
+ nomprenom = etud.nomprenom
+ nip = etud.code_nip or ""
+ ine = etud.code_ine or ""
+ msg = f"Suppression de l'étudiant: {nomprenom} {etudid} (nip {nip}, ine {ine})"
+ Scolog.logdb(method="etudident_delete", etudid=etudid, msg=msg, commit=True)
+ # news
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_ETUD,
+ obj=etudid,
+ text=msg,
+ max_frequency=0,
+ dept_id=g.scodoc_dept_id,
+ )
db.session.delete(etud)
db.session.commit()
# Inval semestres où il était inscrit:
diff --git a/config.py b/config.py
index 5781bc1eea6a857a446286e30c82acefa3e2ef5f..5bee4dff48b48b9e14f2e534eac6eea6cee4730b 100755
--- a/config.py
+++ b/config.py
@@ -80,6 +80,7 @@ class TestConfig(DevConfig):
SERVER_NAME = os.environ.get("SCODOC_TEST_SERVER_NAME") or "test.gr"
DEPT_TEST = "TEST_" # nom du département, ne pas l'utiliser pour un "vrai"
SECRET_KEY = os.environ.get("TEST_SECRET_KEY") or "c7ecff5db1594c208f573ff30e0f6bca"
+ DISABLE_EMAILS = True # ne pas envoyer de mails
class TestAPIConfig(Config):
@@ -94,6 +95,7 @@ class TestAPIConfig(Config):
)
DEPT_TEST = "TAPI_" # nom du département, ne pas l'utiliser pour un "vrai"
SECRET_KEY = os.environ.get("TEST_SECRET_KEY") or "c7ecff5db15946789Hhahbh88aja175"
+ DISABLE_EMAILS = True # ne pas envoyer de mails
mode = os.environ.get("FLASK_ENV", "production")