Ré-écriture des news. Close #117

This commit is contained in:
Emmanuel Viennet 2022-04-12 17:12:51 +02:00
parent 70db38bbb4
commit 721a15d5ec
23 changed files with 461 additions and 119 deletions

View File

@ -16,7 +16,7 @@ from app import models
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 ScoValueError from app.scodoc.sco_exceptions import ScoInvalidParamError
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -358,7 +358,7 @@ def make_etud_args(
try: try:
args = {"etudid": int(etudid)} args = {"etudid": int(etudid)}
except ValueError as exc: except ValueError as exc:
raise ScoValueError("Adresse invalide") from exc raise ScoInvalidParamError() from exc
elif code_nip: elif code_nip:
args = {"code_nip": code_nip} args = {"code_nip": code_nip}
elif use_request: # use form from current request (Flask global) elif use_request: # use form from current request (Flask global)

View File

@ -2,9 +2,21 @@
"""Evenements et logs divers """Evenements et logs divers
""" """
import datetime
import re
from flask import g, url_for
from flask_login import current_user
from app import db from app import db
from app import email
from app import log
from app.auth.models import User
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models.formsemestre import FormSemestre
from app.models.moduleimpls import ModuleImpl
import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences
class Scolog(db.Model): class Scolog(db.Model):
@ -24,13 +36,213 @@ class Scolog(db.Model):
class ScolarNews(db.Model): class ScolarNews(db.Model):
"""Nouvelles pour page d'accueil""" """Nouvelles pour page d'accueil"""
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
NEWS_FORM = "FORM" # modification formation (object=formation_id)
NEWS_SEM = "SEM" # creation semestre (object=None)
NEWS_ABS = "ABS" # saisie absence
NEWS_MISC = "MISC" # unused
NEWS_MAP = {
NEWS_INSCR: "inscription d'étudiants",
NEWS_NOTE: "saisie note",
NEWS_FORM: "modification formation",
NEWS_SEM: "création semestre",
NEWS_MISC: "opération", # unused
}
NEWS_TYPES = list(NEWS_MAP.keys())
__tablename__ = "scolar_news" __tablename__ = "scolar_news"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) date = db.Column(
authenticated_user = db.Column(db.Text) # login, sans contrainte db.DateTime(timezone=True), server_default=db.func.now(), index=True
)
authenticated_user = db.Column(db.Text, index=True) # login, sans contrainte
# type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC' # type in 'INSCR', 'NOTES', 'FORM', 'SEM', 'MISC'
type = db.Column(db.String(SHORT_STR_LEN)) type = db.Column(db.String(SHORT_STR_LEN), index=True)
object = db.Column(db.Integer) # moduleimpl_id, formation_id, formsemestre_id object = db.Column(
db.Integer, index=True
) # moduleimpl_id, formation_id, formsemestre_id
text = db.Column(db.Text) text = db.Column(db.Text)
url = db.Column(db.Text) url = db.Column(db.Text)
def __repr__(self):
return (
f"<{self.__class__.__name__}(id={self.id}, date='{self.date.isoformat()}')>"
)
def __str__(self):
"'Chargement notes dans Stage (S3 FI) par Aurélie Dupont'"
formsemestre = self.get_news_formsemestre()
user = User.query.filter_by(user_name=self.authenticated_user).first()
sem_text = (
f"""(<a href="{url_for('notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}">{formsemestre.sem_modalite()}</a>)"""
if formsemestre
else ""
)
author = f"par {user.get_nomcomplet()}" if user else ""
return f"{self.text} {sem_text} {author}"
def formatted_date(self) -> str:
"06 Avr 14h23"
mois = scu.MONTH_NAMES_ABBREV[self.date.month - 1]
return f"{self.date.day} {mois} {self.date.hour:02d}h{self.date.minute:02d}"
def to_dict(self):
return {
"date": {
"display": self.date.strftime("%d/%m/%Y %H:%M"),
"timestamp": self.date.timestamp(),
},
"type": self.NEWS_MAP.get(self.type, "?"),
"authenticated_user": self.authenticated_user,
"text": self.text,
}
@classmethod
def last_news(cls, n=1) -> list:
"The most recent n news. Returns list of ScolarNews instances."
return cls.query.order_by(cls.date.desc()).limit(n).all()
@classmethod
def add(cls, typ, obj=None, text="", url=None, max_frequency=0):
"""Enregistre une nouvelle
Si max_frequency, ne génère pas 2 nouvelles "identiques"
à moins de max_frequency secondes d'intervalle.
Deux nouvelles sont considérées comme "identiques" si elles ont
même (obj, typ, user).
La nouvelle enregistrée est aussi envoyée par mail.
"""
if max_frequency:
last_news = (
cls.query.filter_by(
dept_id=g.scodoc_dept_id,
authenticated_user=current_user.user_name,
type=typ,
object=obj,
)
.order_by(cls.date.desc())
.limit(1)
.first()
)
if last_news:
now = datetime.datetime.now(tz=last_news.date.tzinfo)
if (now - last_news.date) < datetime.timedelta(seconds=max_frequency):
# on n'enregistre pas
return
news = ScolarNews(
dept_id=g.scodoc_dept_id,
authenticated_user=current_user.user_name,
type=typ,
object=obj,
text=text,
url=url,
)
db.session.add(news)
db.session.commit()
log(f"news: {news}")
news.notify_by_mail()
def get_news_formsemestre(self) -> FormSemestre:
"""formsemestre concerné par la nouvelle
None si inexistant
"""
formsemestre_id = None
if self.type == self.NEWS_INSCR:
formsemestre_id = self.object
elif self.type == self.NEWS_NOTE:
moduleimpl_id = self.object
if moduleimpl_id:
modimpl = ModuleImpl.query.get(moduleimpl_id)
if modimpl is None:
return None # module does not exists anymore
formsemestre_id = modimpl.formsemestre_id
if not formsemestre_id:
return None
formsemestre = FormSemestre.query.get(formsemestre_id)
return formsemestre
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
)
destinations = prefs["emails_notifications"] or ""
destinations = [x.strip() for x in destinations.split(",")]
destinations = [x for x in destinations if x]
if not destinations:
return
#
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>
"""
user = User.query.filter_by(user_name=self.authenticated_user).first()
if user:
txt += f"\n\nEffectué par: {user.get_nomcomplet()}\n"
txt = (
"\n"
+ txt
+ """\n
--- Ceci est un message de notification automatique issu de ScoDoc
--- vous recevez ce message car votre adresse est indiquée dans les paramètres de ScoDoc.
"""
)
# Transforme les URL en URL absolues
base = scu.ScoURL()
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)
txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
subject = "[ScoDoc] " + self.NEWS_MAP.get(self.type, "?")
sender = prefs["email_from_addr"]
email.send_email(subject, sender, destinations, txt)
@classmethod
def scolar_news_summary_html(cls, n=5) -> str:
"""News summary, formated in HTML"""
news_list = cls.last_news(n=n)
if not news_list:
return ""
H = [
f"""<div class="news"><span class="newstitle"><a href="{
url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
}">Dernières opérations</a>
</span><ul class="newslist">"""
]
for news in news_list:
H.append(
f"""<li class="newslist"><span class="newsdate">{news.formatted_date()}</span><span
class="newstext">{news}</span></li>"""
)
H.append("</ul>")
# Informations générales
H.append(
f"""<div>
Pour être informé des évolutions de ScoDoc,
vous pouvez vous
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">
abonner à la liste de diffusion</a>.
</div>
"""
)
H.append("</div>")
return "\n".join(H)

View File

@ -374,6 +374,16 @@ class FormSemestre(db.Model):
return self.titre return self.titre
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
def sem_modalite(self) -> str:
"""Le semestre et la modialité, ex "S2 FI" ou "S3 APP" """
if self.semestre_id > 0:
descr_sem = f"S{self.semestre_id}"
else:
descr_sem = ""
if self.modalite:
descr_sem += " " + self.modalite
return descr_sem
def get_abs_count(self, etudid): def get_abs_count(self, etudid):
"""Les comptes d'absences de cet étudiant dans ce semestre: """Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs, nb abs justifiées) tuple (nb abs, nb abs justifiées)

View File

@ -32,6 +32,7 @@ from flask import g, request
from flask_login import current_user from flask_login import current_user
import app import app
from app.models import ScolarNews
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -40,9 +41,7 @@ import app.scodoc.notesdb as ndb
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_modalites from app.scodoc import sco_modalites
from app.scodoc import sco_news
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_up_to_date
from app.scodoc import sco_users from app.scodoc import sco_users
@ -53,7 +52,7 @@ def index_html(showcodes=0, showsemtable=0):
H = [] H = []
# News: # News:
H.append(sco_news.scolar_news_summary_html()) H.append(ScolarNews.scolar_news_summary_html())
# Avertissement de mise à jour: # Avertissement de mise à jour:
H.append("""<div id="update_warning"></div>""") H.append("""<div id="update_warning"></div>""")

View File

@ -37,6 +37,7 @@ from app.models import SHORT_STR_LEN
from app.models.formations import Formation from app.models.formations import Formation
from app.models.modules import Module from app.models.modules import Module
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.models import ScolarNews
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -44,13 +45,10 @@ from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formations from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_news
def formation_delete(formation_id=None, dialog_confirmed=False): def formation_delete(formation_id=None, dialog_confirmed=False):
@ -117,11 +115,10 @@ def do_formation_delete(oid):
sco_formations._formationEditor.delete(cnx, oid) sco_formations._formationEditor.delete(cnx, oid)
# news # news
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=oid, obj=oid,
text="Suppression de la formation %(acronyme)s" % F, text=f"Suppression de la formation {F['acronyme']}",
max_frequency=3,
) )
@ -281,10 +278,9 @@ def do_formation_create(args):
# #
r = sco_formations._formationEditor.create(cnx, args) r = sco_formations._formationEditor.create(cnx, args)
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
text="Création de la formation %(titre)s (%(acronyme)s)" % args, text="Création de la formation %(titre)s (%(acronyme)s)" % args,
max_frequency=3,
) )
return r return r

View File

@ -30,6 +30,7 @@
""" """
import flask import flask
from flask import g, url_for, request from flask import g, url_for, request
from app.models.events import ScolarNews
from app.models.formations import Matiere from app.models.formations import Matiere
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -78,8 +79,7 @@ def do_matiere_edit(*args, **kw):
def do_matiere_create(args): def do_matiere_create(args):
"create a matiere" "create a matiere"
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formations from app.models import ScolarNews
from app.scodoc import sco_news
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check # check
@ -89,9 +89,9 @@ def do_matiere_create(args):
# news # news
formation = Formation.query.get(ue["formation_id"]) formation = Formation.query.get(ue["formation_id"])
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=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=3, max_frequency=3,
) )
@ -174,10 +174,8 @@ def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
def do_matiere_delete(oid): def do_matiere_delete(oid):
"delete matiere and attached modules" "delete matiere and attached modules"
from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_news
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check # check
@ -197,9 +195,9 @@ def do_matiere_delete(oid):
# news # news
formation = Formation.query.get(ue["formation_id"]) formation = Formation.query.get(ue["formation_id"])
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=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=3, max_frequency=3,
) )

View File

@ -38,6 +38,7 @@ from app import models
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import Formation, Matiere, Module, UniteEns from app.models import Formation, Matiere, Module, UniteEns
from app.models import FormSemestre, ModuleImpl from app.models import FormSemestre, ModuleImpl
from app.models import ScolarNews
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -53,7 +54,6 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
_moduleEditor = ndb.EditableTable( _moduleEditor = ndb.EditableTable(
"notes_modules", "notes_modules",
@ -98,16 +98,14 @@ def module_list(*args, **kw):
def do_module_create(args) -> int: def do_module_create(args) -> int:
"Create a module. Returns id of new object." "Create a module. Returns id of new object."
# create # create
from app.scodoc import sco_formations
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
r = _moduleEditor.create(cnx, args) r = _moduleEditor.create(cnx, args)
# news # news
formation = Formation.query.get(args["formation_id"]) formation = Formation.query.get(args["formation_id"])
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=formation.id, obj=formation.id,
text=f"Modification de la formation {formation.acronyme}", text=f"Modification de la formation {formation.acronyme}",
max_frequency=3, max_frequency=3,
) )
@ -396,9 +394,9 @@ def do_module_delete(oid):
# news # news
formation = module.formation formation = module.formation
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=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=3, max_frequency=3,
) )

View File

@ -37,6 +37,7 @@ from app import db
from app import log from app import log
from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
from app.models import Formation, UniteEns, ModuleImpl, Module from app.models import Formation, UniteEns, ModuleImpl, Module
from app.models import ScolarNews
from app.models.formations import Matiere from app.models.formations import Matiere
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -55,15 +56,11 @@ from app.scodoc import html_sco_header
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_apc from app.scodoc import sco_edit_apc
from app.scodoc import sco_edit_formation
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
@ -138,9 +135,9 @@ def do_ue_create(args):
ue = UniteEns.query.get(ue_id) ue = UniteEns.query.get(ue_id)
flash(f"UE créée (code {ue.ue_code})") flash(f"UE créée (code {ue.ue_code})")
formation = Formation.query.get(args["formation_id"]) formation = Formation.query.get(args["formation_id"])
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=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=3, max_frequency=3,
) )
@ -222,9 +219,9 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
# news # news
F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0] F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0]
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=ue.formation_id, obj=ue.formation_id,
text="Modification de la formation %(acronyme)s" % F, text="Modification de la formation %(acronyme)s" % F,
max_frequency=3, max_frequency=3,
) )

View File

@ -637,7 +637,7 @@ def create_etud(cnx, args={}):
Returns: Returns:
etud, l'étudiant créé. etud, l'étudiant créé.
""" """
from app.scodoc import sco_news from app.models import ScolarNews
# creation d'un etudiant # creation d'un etudiant
etudid = etudident_create(cnx, args) etudid = etudident_create(cnx, args)
@ -671,9 +671,8 @@ def create_etud(cnx, args={}):
etud = etudident_list(cnx, {"etudid": etudid})[0] etud = etudident_list(cnx, {"etudid": etudid})[0]
fill_etuds_info([etud]) fill_etuds_info([etud])
etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid) etud["url"] = url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid)
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
object=None, # pas d'object pour ne montrer qu'un etudiant
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"],
) )

View File

@ -28,7 +28,6 @@
"""Gestion evaluations (ScoDoc7, sans SQlAlchemy) """Gestion evaluations (ScoDoc7, sans SQlAlchemy)
""" """
import datetime
import pprint import pprint
import flask import flask
@ -37,6 +36,7 @@ from flask_login import current_user
from app import log from app import log
from app.models import ScolarNews
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -44,9 +44,7 @@ from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
@ -179,9 +177,9 @@ def do_evaluation_create(
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"] mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=moduleimpl_id, obj=moduleimpl_id,
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod, text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"], url=mod["url"],
) )
@ -240,9 +238,9 @@ def do_evaluation_delete(evaluation_id):
mod["url"] = ( mod["url"] = (
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
) )
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=moduleimpl_id, obj=moduleimpl_id,
text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod, text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"], url=mod["url"],
) )

View File

@ -36,32 +36,27 @@ from flask import g
from flask_login import current_user from flask_login import current_user
from flask import request from flask import request
from app import 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 FormSemestre from app.models import FormSemestre
from app.models import ScolarNews
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
import sco_version
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_cache
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
import sco_version
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -633,7 +628,9 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
'<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>' '<span class="evallink"><a class="stdlink" href="evaluation_listenotes?moduleimpl_id=%s">voir toutes les notes du module</a></span>'
% moduleimpl_id % moduleimpl_id
) )
mod_descr = '<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s' % ( mod_descr = (
'<a href="moduleimpl_status?moduleimpl_id=%s">%s %s</a> <span class="resp">(resp. <a title="%s">%s</a>)</span> %s'
% (
moduleimpl_id, moduleimpl_id,
Mod["code"] or "", Mod["code"] or "",
Mod["titre"] or "?", Mod["titre"] or "?",
@ -641,6 +638,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
resp, resp,
link, link,
) )
)
etit = E["description"] or "" etit = E["description"] or ""
if etit: if etit:

View File

@ -63,6 +63,17 @@ class ScoFormatError(ScoValueError):
pass pass
class ScoInvalidParamError(ScoValueError):
"""Paramètres requete invalides.
A utilisée lorsqu'une route est appelée avec des paramètres invalides
(id strings, ...)
"""
def __init__(self, msg=None, dest_url=None):
msg = msg or "Adresse invalide. Vérifiez vos signets."
super().__init__(msg, dest_url=dest_url)
class ScoPDFFormatError(ScoValueError): class ScoPDFFormatError(ScoValueError):
"erreur génération PDF (templates platypus, ...)" "erreur génération PDF (templates platypus, ...)"

View File

@ -39,12 +39,12 @@ import app.scodoc.notesdb as ndb
from app import db from app import db
from app import log from app import log
from app.models import Formation, Module from app.models import Formation, Module
from app.models import ScolarNews
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_news
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module from app.scodoc import sco_tag_module
from app.scodoc import sco_xml from app.scodoc import sco_xml
@ -351,11 +351,14 @@ def formation_list_table(formation_id=None, args={}):
else: else:
but_locked = '<span class="but_placeholder"></span>' but_locked = '<span class="but_placeholder"></span>'
if editable and not locked: if editable and not locked:
but_suppr = '<a class="stdlink" href="formation_delete?formation_id=%s" id="delete-formation-%s">%s</a>' % ( but_suppr = (
'<a class="stdlink" href="formation_delete?formation_id=%s" id="delete-formation-%s">%s</a>'
% (
f["formation_id"], f["formation_id"],
f["acronyme"].lower().replace(" ", "-"), f["acronyme"].lower().replace(" ", "-"),
suppricon, suppricon,
) )
)
else: else:
but_suppr = '<span class="but_placeholder"></span>' but_suppr = '<span class="but_placeholder"></span>'
if editable: if editable:
@ -422,9 +425,9 @@ def formation_create_new_version(formation_id, redirect=True):
new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data) new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data)
# news # news
F = formation_list(args={"formation_id": new_id})[0] F = formation_list(args={"formation_id": new_id})[0]
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_FORM, typ=ScolarNews.NEWS_FORM,
object=new_id, obj=new_id,
text="Nouvelle version de la formation %(acronyme)s" % F, text="Nouvelle version de la formation %(acronyme)s" % F,
) )
if redirect: if redirect:

View File

@ -229,7 +229,7 @@ def etapes_apo_str(etapes):
def do_formsemestre_create(args, silent=False): def do_formsemestre_create(args, silent=False):
"create a formsemestre" "create a formsemestre"
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_news from app.models import ScolarNews
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
formsemestre_id = _formsemestreEditor.create(cnx, args) formsemestre_id = _formsemestreEditor.create(cnx, args)
@ -254,8 +254,8 @@ def do_formsemestre_create(args, silent=False):
args["formsemestre_id"] = formsemestre_id args["formsemestre_id"] = formsemestre_id
args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args
if not silent: if not silent:
sco_news.add( ScolarNews.add(
typ=sco_news.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"],
) )

View File

@ -1493,11 +1493,9 @@ def do_formsemestre_delete(formsemestre_id):
sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id) sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)
# news # news
from app.scodoc import sco_news ScolarNews.add(
typ=ScolarNews.NEWS_SEM,
sco_news.add( obj=formsemestre_id,
typ=sco_news.NEWS_SEM,
object=formsemestre_id,
text="Suppression du semestre %(titre)s" % sem, text="Suppression du semestre %(titre)s" % sem,
) )

View File

@ -40,6 +40,8 @@ from flask import g, url_for
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
from app.models import ScolarNews
from app.scodoc.sco_excel import COLORS from app.scodoc.sco_excel import COLORS
from app.scodoc.sco_formsemestre_inscriptions import ( from app.scodoc.sco_formsemestre_inscriptions import (
do_formsemestre_inscription_with_modules, do_formsemestre_inscription_with_modules,
@ -54,14 +56,13 @@ from app.scodoc.sco_exceptions import (
ScoLockedFormError, ScoLockedFormError,
ScoGenError, ScoGenError,
) )
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_excel from app.scodoc import sco_excel
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_news
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
# format description (in tools/) # format description (in tools/)
@ -472,11 +473,11 @@ def scolars_import_excel_file(
diag.append("Import et inscription de %s étudiants" % len(created_etudids)) diag.append("Import et inscription de %s étudiants" % len(created_etudids))
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
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),
object=formsemestre_id, obj=formsemestre_id,
) )
log("scolars_import_excel_file: completing transaction") log("scolars_import_excel_file: completing transaction")

View File

@ -350,7 +350,7 @@ class BasePreferences(object):
"initvalue": "", "initvalue": "",
"title": "e-mails à qui notifier les opérations", "title": "e-mails à qui notifier les opérations",
"size": 70, "size": 70,
"explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc). (vous pouvez préférer utiliser le flux rss)", "explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc).",
"category": "general", "category": "general",
"only_global": False, # peut être spécifique à un semestre "only_global": False, # peut être spécifique à un semestre
}, },

View File

@ -39,6 +39,7 @@ from flask_login import current_user
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 FormSemestre from app.models import FormSemestre
from app.models import ScolarNews
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -48,6 +49,7 @@ from app.scodoc.sco_exceptions import (
InvalidNoteValue, InvalidNoteValue,
NoteProcessError, NoteProcessError,
ScoGenError, ScoGenError,
ScoInvalidParamError,
ScoValueError, ScoValueError,
) )
from app.scodoc.TrivialFormulator import TrivialFormulator, TF from app.scodoc.TrivialFormulator import TrivialFormulator, TF
@ -64,7 +66,6 @@ from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_groups_view from app.scodoc import sco_groups_view
from app.scodoc import sco_moduleimpl from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check from app.scodoc import sco_permissions_check
from app.scodoc import sco_undo_notes from app.scodoc import sco_undo_notes
from app.scodoc import sco_etud from app.scodoc import sco_etud
@ -274,9 +275,9 @@ def do_evaluation_upload_xls():
moduleimpl_id=mod["moduleimpl_id"], moduleimpl_id=mod["moduleimpl_id"],
_external=True, _external=True,
) )
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=M["moduleimpl_id"], obj=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod, text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"], url=mod["url"],
) )
@ -359,9 +360,9 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
moduleimpl_id=mod["moduleimpl_id"], moduleimpl_id=mod["moduleimpl_id"],
) )
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=M["moduleimpl_id"], obj=M["moduleimpl_id"],
text='Initialisation notes dans <a href="%(url)s">%(titre)s</a>' % mod, text='Initialisation notes dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"], url=mod["url"],
) )
@ -451,9 +452,9 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0] mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"] mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=M["moduleimpl_id"], obj=M["moduleimpl_id"],
text='Suppression des notes d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' text='Suppression des notes d\'une évaluation dans <a href="%(url)s">%(titre)s</a>'
% mod, % mod,
url=mod["url"], url=mod["url"],
@ -893,10 +894,12 @@ def has_existing_decision(M, E, etudid):
def saisie_notes(evaluation_id, group_ids=[]): def saisie_notes(evaluation_id, group_ids=[]):
"""Formulaire saisie notes d'une évaluation pour un groupe""" """Formulaire saisie notes d'une évaluation pour un groupe"""
if not isinstance(evaluation_id, int):
raise ScoInvalidParamError()
group_ids = [int(group_id) for group_id in group_ids] group_ids = [int(group_id) for group_id in group_ids]
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id}) evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals: if not evals:
raise ScoValueError("invalid evaluation_id") raise ScoValueError("évaluation inexistante")
E = evals[0] E = evals[0]
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0] M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
formsemestre_id = M["formsemestre_id"] formsemestre_id = M["formsemestre_id"]
@ -1283,9 +1286,9 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
nbchanged, _, existing_decisions = notes_add( nbchanged, _, existing_decisions = notes_add(
authuser, evaluation_id, L, comment=comment, do_it=True authuser, evaluation_id, L, comment=comment, do_it=True
) )
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_NOTE, typ=ScolarNews.NEWS_NOTE,
object=M["moduleimpl_id"], obj=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod, text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
url=Mod["url"], url=Mod["url"],
max_frequency=30 * 60, # 30 minutes max_frequency=30 * 60, # 30 minutes

View File

@ -29,12 +29,14 @@
""" """
import time import time
import pprint
from operator import itemgetter from operator import itemgetter
from flask import g, url_for from flask import g, url_for
from flask_login import current_user from flask_login import current_user
from app import log
from app.models import ScolarNews
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
@ -43,11 +45,8 @@ from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc import sco_inscr_passage from app.scodoc import sco_inscr_passage
from app.scodoc import sco_news
from app.scodoc import sco_excel
from app.scodoc import sco_portal_apogee from app.scodoc import sco_portal_apogee
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app import log
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
@ -701,10 +700,10 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
sco_cache.invalidate_formsemestre() sco_cache.invalidate_formsemestre()
raise raise
sco_news.add( ScolarNews.add(
typ=sco_news.NEWS_INSCR, typ=ScolarNews.NEWS_INSCR,
text="Import Apogée de %d étudiants en " % len(created_etudids), text="Import Apogée de %d étudiants en " % len(created_etudids),
object=sem["formsemestre_id"], obj=sem["formsemestre_id"],
) )

View File

@ -487,6 +487,16 @@ div.news {
border-radius: 8px; border-radius: 8px;
} }
div.news a {
color: black;
text-decoration: none;
}
div.news a:hover {
color: rgb(153, 51, 51);
text-decoration: underline;
}
span.newstitle { span.newstitle {
font-weight: bold; font-weight: bold;
} }

View File

@ -0,0 +1,47 @@
{# -*- mode: jinja-html -*- #}
{% extends "sco_page.html" %}
{% block styles %}
{{super()}}
{% endblock %}
{% block app_content %}
<h2>Opérations dans le département {{g.scodoc_dept}}</h2>
<table id="dept_news" class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Auteur</th>
<th>Détail</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{% endblock %}
{% block scripts %}
{{super()}}
<script>
$(document).ready(function () {
$('#dept_news').DataTable({
ajax: '{{url_for("scolar.dept_news_json", scodoc_dept=g.scodoc_dept)}}',
serverSide: true,
columns: [
{
data: {
_: "date.display",
sort: "date.timestamp"
}
},
{data: 'type', searchable: false},
{data: 'authenticated_user', orderable: false, searchable: true},
{data: 'text', orderable: false, searchable: true}
],
"order": [[ 0, "desc" ]]
});
});
</script>
{% endblock %}

View File

@ -41,6 +41,7 @@ from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed from flask_wtf.file import FileField, FileAllowed
from wtforms import SubmitField from wtforms import SubmitField
from app import db
from app import log from app import log
from app.decorators import ( from app.decorators import (
scodoc, scodoc,
@ -52,8 +53,10 @@ from app.decorators import (
) )
from app.models.etudiants import Identite from app.models.etudiants import Identite
from app.models.etudiants import make_etud_args from app.models.etudiants import make_etud_args
from app.models.events import ScolarNews
from app.views import scolar_bp as bp from app.views import scolar_bp as bp
from app.views import ScoData
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
@ -339,6 +342,67 @@ def install_info():
return sco_up_to_date.is_up_to_date() return sco_up_to_date.is_up_to_date()
@bp.route("/dept_news")
@scodoc
@permission_required(Permission.ScoView)
def dept_news():
"Affiche table des dernières opérations"
return render_template(
"dept_news.html", title=f"Opérations {g.scodoc_dept}", sco=ScoData()
)
@bp.route("/dept_news_json")
@scodoc
@permission_required(Permission.ScoView)
def dept_news_json():
"Table des news du département"
start = request.args.get("start", type=int)
length = request.args.get("length", type=int)
log(f"dept_news_json( start={start}, length={length})")
query = ScolarNews.query.filter_by(dept_id=g.scodoc_dept_id)
# search
search = request.args.get("search[value]")
if search:
query = query.filter(
db.or_(
ScolarNews.authenticated_user.like(f"%{search}%"),
ScolarNews.text.like(f"%{search}%"),
)
)
total_filtered = query.count()
# sorting
order = []
i = 0
while True:
col_index = request.args.get(f"order[{i}][column]")
if col_index is None:
break
col_name = request.args.get(f"columns[{col_index}][data]")
if col_name not in ["date", "type", "authenticated_user"]:
col_name = "date"
descending = request.args.get(f"order[{i}][dir]") == "desc"
col = getattr(ScolarNews, col_name)
if descending:
col = col.desc()
order.append(col)
i += 1
if order:
query = query.order_by(*order)
# pagination
query = query.offset(start).limit(length)
data = [news.to_dict() for news in query]
# response
return {
"data": data,
"recordsFiltered": total_filtered,
"recordsTotal": ScolarNews.query.count(),
"draw": request.args.get("draw", type=int),
}
sco_publish( sco_publish(
"/trombino", sco_trombino.trombino, Permission.ScoView, methods=["GET", "POST"] "/trombino", sco_trombino.trombino, Permission.ScoView, methods=["GET", "POST"]
) )

View File

@ -77,6 +77,7 @@ def make_shell_context():
"pp": pp, "pp": pp,
"Role": Role, "Role": Role,
"scolar": scolar, "scolar": scolar,
"ScolarNews": models.ScolarNews,
"scu": scu, "scu": scu,
"UniteEns": UniteEns, "UniteEns": UniteEns,
"User": User, "User": User,