1
0
forked from ScoDoc/ScoDoc

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

View File

@ -2,9 +2,21 @@
"""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 email
from app import log
from app.auth.models import User
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):
@ -24,13 +36,213 @@ class Scolog(db.Model):
class ScolarNews(db.Model):
"""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"
id = db.Column(db.Integer, primary_key=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())
authenticated_user = db.Column(db.Text) # login, sans contrainte
date = db.Column(
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 = db.Column(db.String(SHORT_STR_LEN))
object = db.Column(db.Integer) # moduleimpl_id, formation_id, formsemestre_id
type = db.Column(db.String(SHORT_STR_LEN), index=True)
object = db.Column(
db.Integer, index=True
) # moduleimpl_id, formation_id, formsemestre_id
text = 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 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):
"""Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs, nb abs justifiées)

View File

@ -32,6 +32,7 @@ from flask import g, request
from flask_login import current_user
import app
from app.models import ScolarNews
import app.scodoc.sco_utils as scu
from app.scodoc.gen_tables import GenTable
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_inscriptions
from app.scodoc import sco_modalites
from app.scodoc import sco_news
from app.scodoc import sco_preferences
from app.scodoc import sco_up_to_date
from app.scodoc import sco_users
@ -53,7 +52,7 @@ def index_html(showcodes=0, showsemtable=0):
H = []
# News:
H.append(sco_news.scolar_news_summary_html())
H.append(ScolarNews.scolar_news_summary_html())
# Avertissement de mise à jour:
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.modules import Module
from app.models.ues import UniteEns
from app.models import ScolarNews
import app.scodoc.notesdb as ndb
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 import html_sco_header
from app.scodoc import sco_cache
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_formations
from app.scodoc import sco_formsemestre
from app.scodoc import sco_news
def formation_delete(formation_id=None, dialog_confirmed=False):
@ -117,11 +115,10 @@ def do_formation_delete(oid):
sco_formations._formationEditor.delete(cnx, oid)
# news
sco_news.add(
typ=sco_news.NEWS_FORM,
object=oid,
text="Suppression de la formation %(acronyme)s" % F,
max_frequency=3,
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=oid,
text=f"Suppression de la formation {F['acronyme']}",
)
@ -281,10 +278,9 @@ def do_formation_create(args):
#
r = sco_formations._formationEditor.create(cnx, args)
sco_news.add(
typ=sco_news.NEWS_FORM,
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
text="Création de la formation %(titre)s (%(acronyme)s)" % args,
max_frequency=3,
)
return r

View File

@ -30,6 +30,7 @@
"""
import flask
from flask import g, url_for, request
from app.models.events import ScolarNews
from app.models.formations import Matiere
import app.scodoc.notesdb as ndb
@ -78,8 +79,7 @@ def do_matiere_edit(*args, **kw):
def do_matiere_create(args):
"create a matiere"
from app.scodoc import sco_edit_ue
from app.scodoc import sco_formations
from app.scodoc import sco_news
from app.models import ScolarNews
cnx = ndb.GetDBConnexion()
# check
@ -89,9 +89,9 @@ def do_matiere_create(args):
# news
formation = Formation.query.get(ue["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
object=ue["formation_id"],
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=ue["formation_id"],
text=f"Modification de la formation {formation.acronyme}",
max_frequency=3,
)
@ -174,10 +174,8 @@ def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
def do_matiere_delete(oid):
"delete matiere and attached modules"
from app.scodoc import sco_formations
from app.scodoc import sco_edit_ue
from app.scodoc import sco_edit_module
from app.scodoc import sco_news
cnx = ndb.GetDBConnexion()
# check
@ -197,9 +195,9 @@ def do_matiere_delete(oid):
# news
formation = Formation.query.get(ue["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
object=ue["formation_id"],
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=ue["formation_id"],
text=f"Modification de la formation {formation.acronyme}",
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 Formation, Matiere, Module, UniteEns
from app.models import FormSemestre, ModuleImpl
from app.models import ScolarNews
import app.scodoc.notesdb as ndb
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_edit_matiere
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
_moduleEditor = ndb.EditableTable(
"notes_modules",
@ -98,16 +98,14 @@ def module_list(*args, **kw):
def do_module_create(args) -> int:
"Create a module. Returns id of new object."
# create
from app.scodoc import sco_formations
cnx = ndb.GetDBConnexion()
r = _moduleEditor.create(cnx, args)
# news
formation = Formation.query.get(args["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
object=formation.id,
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=formation.id,
text=f"Modification de la formation {formation.acronyme}",
max_frequency=3,
)
@ -396,9 +394,9 @@ def do_module_delete(oid):
# news
formation = module.formation
sco_news.add(
typ=sco_news.NEWS_FORM,
object=mod["formation_id"],
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=mod["formation_id"],
text=f"Modification de la formation {formation.acronyme}",
max_frequency=3,
)

View File

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

View File

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

View File

@ -28,7 +28,6 @@
"""Gestion evaluations (ScoDoc7, sans SQlAlchemy)
"""
import datetime
import pprint
import flask
@ -37,6 +36,7 @@ from flask_login import current_user
from app import log
from app.models import ScolarNews
from app.models.evaluations import evaluation_enrich_dict, check_evaluation_args
import app.scodoc.sco_utils as scu
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_edit_module
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
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["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
sco_news.add(
typ=sco_news.NEWS_NOTE,
object=moduleimpl_id,
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=moduleimpl_id,
text='Création d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"],
)
@ -240,9 +238,9 @@ def do_evaluation_delete(evaluation_id):
mod["url"] = (
scu.NotesURL() + "/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
)
sco_news.add(
typ=sco_news.NEWS_NOTE,
object=moduleimpl_id,
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=moduleimpl_id,
text='Suppression d\'une évaluation dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"],
)

View File

@ -36,32 +36,27 @@ from flask import g
from flask_login import current_user
from flask import request
from app import log
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.models import ScolarNews
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
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 import html_sco_header
from app.scodoc import sco_evaluation_db
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_ue
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences
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>'
% 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,
Mod["code"] or "",
Mod["titre"] or "?",
@ -641,6 +638,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
resp,
link,
)
)
etit = E["description"] or ""
if etit:

View File

@ -63,6 +63,17 @@ class ScoFormatError(ScoValueError):
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):
"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 log
from app.models import Formation, Module
from app.models import ScolarNews
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre
from app.scodoc import sco_news
from app.scodoc import sco_preferences
from app.scodoc import sco_tag_module
from app.scodoc import sco_xml
@ -351,11 +351,14 @@ def formation_list_table(formation_id=None, args={}):
else:
but_locked = '<span class="but_placeholder"></span>'
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["acronyme"].lower().replace(" ", "-"),
suppricon,
)
)
else:
but_suppr = '<span class="but_placeholder"></span>'
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)
# news
F = formation_list(args={"formation_id": new_id})[0]
sco_news.add(
typ=sco_news.NEWS_FORM,
object=new_id,
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=new_id,
text="Nouvelle version de la formation %(acronyme)s" % F,
)
if redirect:

View File

@ -229,7 +229,7 @@ def etapes_apo_str(etapes):
def do_formsemestre_create(args, silent=False):
"create a formsemestre"
from app.scodoc import sco_groups
from app.scodoc import sco_news
from app.models import ScolarNews
cnx = ndb.GetDBConnexion()
formsemestre_id = _formsemestreEditor.create(cnx, args)
@ -254,8 +254,8 @@ def do_formsemestre_create(args, silent=False):
args["formsemestre_id"] = formsemestre_id
args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args
if not silent:
sco_news.add(
typ=sco_news.NEWS_SEM,
ScolarNews.add(
typ=ScolarNews.NEWS_SEM,
text='Création du semestre <a href="%(url)s">%(titre)s</a>' % args,
url=args["url"],
)

View File

@ -1493,11 +1493,9 @@ def do_formsemestre_delete(formsemestre_id):
sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)
# news
from app.scodoc import sco_news
sco_news.add(
typ=sco_news.NEWS_SEM,
object=formsemestre_id,
ScolarNews.add(
typ=ScolarNews.NEWS_SEM,
obj=formsemestre_id,
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.notesdb as ndb
from app import log
from app.models import ScolarNews
from app.scodoc.sco_excel import COLORS
from app.scodoc.sco_formsemestre_inscriptions import (
do_formsemestre_inscription_with_modules,
@ -54,14 +56,13 @@ from app.scodoc.sco_exceptions import (
ScoLockedFormError,
ScoGenError,
)
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_etud
from app.scodoc import sco_formsemestre
from app.scodoc import sco_groups
from app.scodoc import sco_excel
from app.scodoc import sco_groups_view
from app.scodoc import sco_news
from app.scodoc import sco_preferences
# format description (in tools/)
@ -472,11 +473,11 @@ def scolars_import_excel_file(
diag.append("Import et inscription de %s étudiants" % len(created_etudids))
sco_news.add(
typ=sco_news.NEWS_INSCR,
ScolarNews.add(
typ=ScolarNews.NEWS_INSCR,
text="Inscription de %d étudiants" # peuvent avoir ete inscrits a des semestres differents
% len(created_etudids),
object=formsemestre_id,
obj=formsemestre_id,
)
log("scolars_import_excel_file: completing transaction")

View File

@ -350,7 +350,7 @@ class BasePreferences(object):
"initvalue": "",
"title": "e-mails à qui notifier les opérations",
"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",
"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.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.models import ScolarNews
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
@ -48,6 +49,7 @@ from app.scodoc.sco_exceptions import (
InvalidNoteValue,
NoteProcessError,
ScoGenError,
ScoInvalidParamError,
ScoValueError,
)
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_view
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_news
from app.scodoc import sco_permissions_check
from app.scodoc import sco_undo_notes
from app.scodoc import sco_etud
@ -274,9 +275,9 @@ def do_evaluation_upload_xls():
moduleimpl_id=mod["moduleimpl_id"],
_external=True,
)
sco_news.add(
typ=sco_news.NEWS_NOTE,
object=M["moduleimpl_id"],
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % mod,
url=mod["url"],
)
@ -359,9 +360,9 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
scodoc_dept=g.scodoc_dept,
moduleimpl_id=mod["moduleimpl_id"],
)
sco_news.add(
typ=sco_news.NEWS_NOTE,
object=M["moduleimpl_id"],
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=M["moduleimpl_id"],
text='Initialisation notes dans <a href="%(url)s">%(titre)s</a>' % mod,
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["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
sco_news.add(
typ=sco_news.NEWS_NOTE,
object=M["moduleimpl_id"],
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=M["moduleimpl_id"],
text='Suppression des notes d\'une évaluation dans <a href="%(url)s">%(titre)s</a>'
% mod,
url=mod["url"],
@ -893,10 +894,12 @@ def has_existing_decision(M, E, etudid):
def saisie_notes(evaluation_id, group_ids=[]):
"""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]
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
raise ScoValueError("invalid evaluation_id")
raise ScoValueError("évaluation inexistante")
E = evals[0]
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
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(
authuser, evaluation_id, L, comment=comment, do_it=True
)
sco_news.add(
typ=sco_news.NEWS_NOTE,
object=M["moduleimpl_id"],
ScolarNews.add(
typ=ScolarNews.NEWS_NOTE,
obj=M["moduleimpl_id"],
text='Chargement notes dans <a href="%(url)s">%(titre)s</a>' % Mod,
url=Mod["url"],
max_frequency=30 * 60, # 30 minutes

View File

@ -29,12 +29,14 @@
"""
import time
import pprint
from operator import itemgetter
from flask import g, url_for
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.notesdb as ndb
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_groups
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_etud
from app import log
from app.scodoc.sco_exceptions import ScoValueError
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()
raise
sco_news.add(
typ=sco_news.NEWS_INSCR,
ScolarNews.add(
typ=ScolarNews.NEWS_INSCR,
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;
}
div.news a {
color: black;
text-decoration: none;
}
div.news a:hover {
color: rgb(153, 51, 51);
text-decoration: underline;
}
span.newstitle {
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 wtforms import SubmitField
from app import db
from app import log
from app.decorators import (
scodoc,
@ -52,8 +53,10 @@ from app.decorators import (
)
from app.models.etudiants import Identite
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 ScoData
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@ -339,6 +342,67 @@ def install_info():
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(
"/trombino", sco_trombino.trombino, Permission.ScoView, methods=["GET", "POST"]
)

View File

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