Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
19 changed files with 94 additions and 34 deletions
Showing only changes of commit b70e2758c9 - Show all commits

View File

@ -5,9 +5,10 @@
############################################################################## ##############################################################################
""" """
ScoDoc 9 API : jury WIP ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions
""" """
from flask import g, url_for
from flask_json import as_json from flask_json import as_json
from flask_login import login_required from flask_login import login_required
@ -24,6 +25,7 @@ from app.models import (
Identite, Identite,
ScolarAutorisationInscription, ScolarAutorisationInscription,
ScolarFormSemestreValidation, ScolarFormSemestreValidation,
ScolarNews,
) )
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -47,6 +49,20 @@ def decisions_jury(formsemestre_id: int):
raise ScoException("non implemente") raise ScoException("non implemente")
def _news_delete_jury_etud(etud: Identite):
"génère news sur effacement décision"
# n'utilise pas g.scodoc_dept, pas toujours dispo en mode API
url = url_for(
"scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id
)
ScolarNews.add(
typ=ScolarNews.NEWS_JURY,
obj=etud.id,
text=f"""Suppression décision jury pour <a href="{url}">{etud.nomprenom}</a>""",
url=url,
)
@bp.route( @bp.route(
"/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete", "/etudiant/<int:etudid>/jury/validation_ue/<int:validation_id>/delete",
methods=["POST"], methods=["POST"],
@ -94,6 +110,7 @@ def _validation_ue_delete(etudid: int, validation_id: int):
db.session.delete(validation) db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud) sco_cache.invalidate_formsemestre_etud(etud)
db.session.commit() db.session.commit()
_news_delete_jury_etud(etud)
return "ok" return "ok"
@ -121,6 +138,7 @@ def autorisation_inscription_delete(etudid: int, validation_id: int):
db.session.delete(validation) db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud) sco_cache.invalidate_formsemestre_etud(etud)
db.session.commit() db.session.commit()
_news_delete_jury_etud(etud)
return "ok" return "ok"
@ -148,6 +166,7 @@ def validation_rcue_delete(etudid: int, validation_id: int):
db.session.delete(validation) db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud) sco_cache.invalidate_formsemestre_etud(etud)
db.session.commit() db.session.commit()
_news_delete_jury_etud(etud)
return "ok" return "ok"
@ -175,4 +194,5 @@ def validation_annee_but_delete(etudid: int, validation_id: int):
db.session.delete(validation) db.session.delete(validation)
sco_cache.invalidate_formsemestre_etud(etud) sco_cache.invalidate_formsemestre_etud(etud)
db.session.commit() db.session.commit()
_news_delete_jury_etud(etud)
return "ok" return "ok"

View File

@ -459,10 +459,7 @@ class DecisionsProposeesAnnee(DecisionsProposees):
"""informations, for debugging purpose.""" """informations, for debugging purpose."""
text = f"""<b>DecisionsProposeesAnnee</b> text = f"""<b>DecisionsProposeesAnnee</b>
<ul> <ul>
<li>Etudiant: <a href="{url_for("scolar.ficheEtud", <li>Étudiant: {self.etud.html_link_fiche()}</li>
scodoc_dept=g.scodoc_dept, etudid=self.etud.id)
}">{self.etud.nomprenom}</a>
</li>
""" """
for formsemestre, title in ( for formsemestre, title in (
(self.formsemestre_impair, "formsemestre_impair"), (self.formsemestre_impair, "formsemestre_impair"),

View File

@ -6,11 +6,11 @@
"""Jury BUT: calcul des décisions de jury annuelles "automatiques" """Jury BUT: calcul des décisions de jury annuelles "automatiques"
""" """
from flask import g, url_for
from app import db from app import db
from app.but import jury_but from app.but import jury_but
from app.models.etudiants import Identite from app.models import Identite, FormSemestre, ScolarNews
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
@ -39,4 +39,14 @@ def formsemestre_validation_auto_but(
nb_etud_modif += deca.record_all(only_validantes=only_adm) nb_etud_modif += deca.record_all(only_validantes=only_adm)
db.session.commit() db.session.commit()
ScolarNews.add(
typ=ScolarNews.NEWS_JURY,
obj=formsemestre.id,
text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status()}""",
url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
)
return nb_etud_modif return nb_etud_modif

View File

@ -31,6 +31,7 @@ from app.models import (
UniteEns, UniteEns,
ScolarAutorisationInscription, ScolarAutorisationInscription,
ScolarFormSemestreValidation, ScolarFormSemestreValidation,
ScolarNews,
) )
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
@ -369,6 +370,16 @@ def jury_but_semestriel(
flash( flash(
f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée" f"autorisation de passage en S{formsemestre.semestre_id + 1} annulée"
) )
ScolarNews.add(
typ=ScolarNews.NEWS_JURY,
obj=formsemestre.id,
text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
)
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.formsemestre_validation_but", "notes.formsemestre_validation_but",

View File

@ -54,14 +54,17 @@ class ScolarNews(db.Model):
NEWS_APO = "APO" # changements de codes APO NEWS_APO = "APO" # changements de codes APO
NEWS_FORM = "FORM" # modification formation (object=formation_id) NEWS_FORM = "FORM" # modification formation (object=formation_id)
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id) NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
NEWS_JURY = "JURY" # saisie jury
NEWS_MISC = "MISC" # unused NEWS_MISC = "MISC" # unused
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id) NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
NEWS_SEM = "SEM" # creation semestre (object=None) NEWS_SEM = "SEM" # creation semestre (object=None)
NEWS_MAP = { NEWS_MAP = {
NEWS_ABS: "saisie absence", NEWS_ABS: "saisie absence",
NEWS_APO: "modif. code Apogée", NEWS_APO: "modif. code Apogée",
NEWS_FORM: "modification formation", NEWS_FORM: "modification formation",
NEWS_INSCR: "inscription d'étudiants", NEWS_INSCR: "inscription d'étudiants",
NEWS_JURY: "saisie jury",
NEWS_MISC: "opération", # unused NEWS_MISC: "opération", # unused
NEWS_NOTE: "saisie note", NEWS_NOTE: "saisie note",
NEWS_SEM: "création semestre", NEWS_SEM: "création semestre",
@ -130,10 +133,10 @@ 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=0): def add(cls, typ, obj=None, text="", url=None, max_frequency=600):
"""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. à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
Deux nouvelles sont considérées comme "identiques" si elles ont Deux nouvelles sont considérées comme "identiques" si elles ont
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.
@ -153,7 +156,10 @@ class ScolarNews(db.Model):
if last_news: if last_news:
now = datetime.datetime.now(tz=last_news.date.tzinfo) now = datetime.datetime.now(tz=last_news.date.tzinfo)
if (now - last_news.date) < datetime.timedelta(seconds=max_frequency): if (now - last_news.date) < datetime.timedelta(seconds=max_frequency):
# on n'enregistre pas # pas de nouvel event, mais met à jour l'heure
last_news.date = datetime.datetime.now()
db.session.add(last_news)
db.session.commit()
return return
news = ScolarNews( news = ScolarNews(

View File

@ -132,6 +132,7 @@ def do_formation_delete(formation_id):
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=formation_id, obj=formation_id,
text=f"Suppression de la formation {acronyme}", text=f"Suppression de la formation {acronyme}",
max_frequency=0,
) )
@ -329,6 +330,7 @@ def do_formation_create(args: dict) -> Formation:
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
text=f"""Création de la formation { text=f"""Création de la formation {
formation.titre} ({formation.acronyme}) version {formation.version}""", formation.titre} ({formation.acronyme}) version {formation.version}""",
max_frequency=0,
) )
return formation return formation

View File

@ -93,7 +93,6 @@ def do_matiere_create(args):
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=ue["formation_id"], obj=ue["formation_id"],
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()
return r return r
@ -199,7 +198,6 @@ def do_matiere_delete(oid):
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=ue["formation_id"], obj=ue["formation_id"],
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()

View File

@ -114,7 +114,6 @@ def do_module_create(args) -> int:
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=formation.id, obj=formation.id,
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()
return module_id return module_id
@ -186,7 +185,6 @@ def do_module_delete(oid):
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=mod["formation_id"], obj=mod["formation_id"],
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()

View File

@ -145,7 +145,6 @@ def do_ue_create(args):
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=args["formation_id"], obj=args["formation_id"],
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=10 * 60,
) )
formation.invalidate_cached_sems() formation.invalidate_cached_sems()
return ue_id return ue_id
@ -230,7 +229,6 @@ def do_ue_delete(ue: UniteEns, delete_validations=False, force=False):
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=formation.id, obj=formation.id,
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=10 * 60,
) )
# #
if not force: if not force:

View File

@ -671,6 +671,7 @@ def create_etud(cnx, args: dict = None):
typ=ScolarNews.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud, text='Nouvel étudiant <a href="%(url)s">%(nomprenom)s</a>' % etud,
url=etud["url"], url=etud["url"],
max_frequency=0,
) )
return etud return etud

View File

@ -638,6 +638,7 @@ def formation_create_new_version(formation_id, redirect=True):
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
obj=new_id, obj=new_id,
text=f"Nouvelle version de la formation {formation.acronyme}", text=f"Nouvelle version de la formation {formation.acronyme}",
max_frequency=0,
) )
if redirect: if redirect:
flash("Nouvelle version !") flash("Nouvelle version !")

View File

@ -261,6 +261,7 @@ def do_formsemestre_create(args, silent=False):
typ=ScolarNews.NEWS_SEM, typ=ScolarNews.NEWS_SEM,
text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args, text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
url=args["url"], url=args["url"],
max_frequency=0,
) )
return formsemestre_id return formsemestre_id

View File

@ -1521,6 +1521,7 @@ def do_formsemestre_delete(formsemestre_id):
typ=ScolarNews.NEWS_SEM, typ=ScolarNews.NEWS_SEM,
obj=formsemestre_id, obj=formsemestre_id,
text="Suppression du semestre %(titre)s" % sem, text="Suppression du semestre %(titre)s" % sem,
max_frequency=0,
) )

View File

@ -39,7 +39,7 @@ from app import db, log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat from app.comp.res_compat import NotesTableCompat
from app.models import Formation, FormSemestre, UniteEns from app.models import Formation, FormSemestre, UniteEns, ScolarNews
from app.models.notes import etud_has_notes_attente from app.models.notes import etud_has_notes_attente
from app.models.validations import ( from app.models.validations import (
ScolarAutorisationInscription, ScolarAutorisationInscription,
@ -992,16 +992,26 @@ def do_formsemestre_validation_auto(formsemestre_id):
) )
nb_valid += 1 nb_valid += 1
log( log(
"do_formsemestre_validation_auto: %d validations, %d conflicts" f"do_formsemestre_validation_auto: {nb_valid} validations, {len(conflicts)} conflicts"
% (nb_valid, len(conflicts))
) )
H = [html_sco_header.sco_header(page_title="Saisie automatique")] ScolarNews.add(
H.append( typ=ScolarNews.NEWS_JURY,
"""<h2>Saisie automatique des décisions du semestre %s</h2> obj=formsemestre.id,
text=f"""Calcul jury automatique du semestre {formsemestre.html_link_status()
} ({nb_valid} décisions)""",
url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
)
H = [
f"""{html_sco_header.sco_header(page_title="Saisie automatique")}
<h2>Saisie automatique des décisions du semestre {formsemestre.titre_annee()}</h2>
<p>Opération effectuée.</p> <p>Opération effectuée.</p>
<p>%d étudiants validés (sur %s)</p>""" <p>{nb_valid} étudiants validés sur {len(etudids)}</p>
% (sem["titreannee"], nb_valid, len(etudids)) """
) ]
if conflicts: if conflicts:
H.append( H.append(
f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés f"""<p><b>Attention:</b> {len(conflicts)} étudiants non modifiés

View File

@ -480,6 +480,7 @@ def scolars_import_excel_file(
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
% len(created_etudids), % len(created_etudids),
obj=formsemestre_id, obj=formsemestre_id,
max_frequency=0,
) )
log("scolars_import_excel_file: completing transaction") log("scolars_import_excel_file: completing transaction")

View File

@ -704,7 +704,6 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
typ=ScolarNews.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
text=f"Import Apogée de {len(created_etudids)} étudiants en ", text=f"Import Apogée de {len(created_etudids)} étudiants en ",
obj=sem["formsemestre_id"], obj=sem["formsemestre_id"],
max_frequency=10 * 60, # 10'
) )

View File

@ -629,7 +629,7 @@ div.news {
border-radius: 8px; border-radius: 8px;
} }
div.news a { div.news a, div.news a.stdlink {
color: black; color: black;
text-decoration: none; text-decoration: none;
} }

View File

@ -2410,6 +2410,16 @@ def formsemestre_validation_but(
if request.method == "POST": if request.method == "POST":
if not read_only: if not read_only:
deca.record_form(request.form) deca.record_form(request.form)
ScolarNews.add(
typ=ScolarNews.NEWS_JURY,
obj=formsemestre.id,
text=f"""Saisie décision jury dans {formsemestre.html_link_status()}""",
url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
),
)
flash("codes enregistrés") flash("codes enregistrés")
return flask.redirect( return flask.redirect(
url_for( url_for(
@ -3059,7 +3069,6 @@ def formsemestre_set_apo_etapes():
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_APO, typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})", text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
max_frequency=10 * 60,
) )
return ("", 204) return ("", 204)
@ -3081,7 +3090,6 @@ def formsemestre_set_elt_annee_apo():
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_APO, typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})", text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
max_frequency=10 * 60,
) )
return ("", 204) return ("", 204)
@ -3103,7 +3111,6 @@ def formsemestre_set_elt_sem_apo():
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_APO, typ=ScolarNews.NEWS_APO,
text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})", text=f"Modification code Apogée du semestre {formsemestre.titre_annee()})",
max_frequency=10 * 60,
) )
return ("", 204) return ("", 204)
@ -3125,7 +3132,6 @@ def ue_set_apo():
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
text=f"Modification code Apogée d'UE dans la formation {ue.formation.titre} ({ue.formation.acronyme})", text=f"Modification code Apogée d'UE dans la formation {ue.formation.titre} ({ue.formation.acronyme})",
max_frequency=10 * 60,
) )
return ("", 204) return ("", 204)
@ -3146,8 +3152,8 @@ def module_set_apo():
db.session.commit() db.session.commit()
ScolarNews.add( ScolarNews.add(
typ=ScolarNews.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
text=f"Modification code Apogée d'UE dans la formation {mod.formation.titre} ({mod.formation.acronyme})", text=f"""Modification code Apogée d'UE dans la formation {
max_frequency=10 * 60, mod.formation.titre} ({mod.formation.acronyme})""",
) )
return ("", 204) return ("", 204)

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.4.90" SCOVERSION = "9.4.91"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"