ScoDoc-Lille/app/models/events.py

273 lines
9.3 KiB
Python

# -*- coding: UTF-8 -*
"""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):
"""Log des actions (journal modif etudiants)"""
__tablename__ = "scolog"
id = db.Column(db.Integer, primary_key=True)
date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
method = db.Column(db.Text)
msg = db.Column(db.Text)
etudid = db.Column(db.Integer) # sans contrainte pour garder logs après suppression
authenticated_user = db.Column(db.Text) # login, sans contrainte
# zope_remote_addr suppressed
@classmethod
def logdb(
cls, method: str = None, etudid: int = None, msg: str = None, commit=False
):
"""Add entry in student's log (replacement for old scolog.logdb)"""
entry = Scolog(
method=method,
msg=msg,
etudid=etudid,
authenticated_user=current_user.user_name,
)
db.session.add(entry)
if commit:
db.session.commit()
class ScolarNews(db.Model):
"""Nouvelles pour page d'accueil"""
NEWS_ABS = "ABS" # saisie absence
NEWS_APO = "APO" # changements de codes APO
NEWS_FORM = "FORM" # modification formation (object=formation_id)
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
NEWS_MISC = "MISC" # unused
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
NEWS_SEM = "SEM" # creation semestre (object=None)
NEWS_MAP = {
NEWS_ABS: "saisie absence",
NEWS_APO: "modif. code Apogée",
NEWS_FORM: "modification formation",
NEWS_INSCR: "inscription d'étudiants",
NEWS_MISC: "opération", # unused
NEWS_NOTE: "saisie note",
NEWS_SEM: "création semestre",
}
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(), 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), 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, dept_id=None, filter_dept=True) -> list:
"The most recent n news. Returns list of ScolarNews instances."
query = cls.query
if filter_dept:
if dept_id is None:
dept_id = g.scodoc_dept_id
query = query.filter_by(dept_id=dept_id)
return 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)