forked from ScoDoc/ScoDoc
304 lines
10 KiB
Python
304 lines
10 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 ScoDocModel, SHORT_STR_LEN
|
|
import app.scodoc.sco_utils as scu
|
|
from app.scodoc import sco_preferences
|
|
|
|
|
|
class Scolog(ScoDocModel):
|
|
"""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) # user_name 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).
|
|
Par défaut ne commite pas."""
|
|
entry = Scolog(
|
|
method=method,
|
|
msg=msg,
|
|
etudid=etudid,
|
|
authenticated_user=current_user.user_name,
|
|
)
|
|
db.session.add(entry)
|
|
if commit:
|
|
db.session.commit()
|
|
|
|
def to_dict(self, convert_date=False) -> dict:
|
|
"convert to dict"
|
|
return {
|
|
"etudid": self.etudid,
|
|
"date": (
|
|
(self.date.strftime(scu.DATETIME_FMT) if convert_date else self.date)
|
|
if self.date
|
|
else ""
|
|
),
|
|
"_date_order": self.date.isoformat() if self.date else "",
|
|
"authenticated_user": self.authenticated_user or "",
|
|
"msg": self.msg or "",
|
|
"method": self.method or "",
|
|
}
|
|
|
|
|
|
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_JURY = "JURY" # saisie jury
|
|
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_JURY: "saisie jury",
|
|
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
|
|
) # user_name 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=600, dept_id=None):
|
|
"""Enregistre une nouvelle
|
|
Si max_frequency, ne génère pas 2 nouvelles "identiques"
|
|
à moins de max_frequency secondes d'intervalle (10 minutes par défaut).
|
|
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.
|
|
"""
|
|
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
|
|
if max_frequency:
|
|
last_news = (
|
|
cls.query.filter_by(
|
|
dept_id=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):
|
|
# pas de nouvel event, mais met à jour l'heure
|
|
last_news.date = datetime.datetime.now()
|
|
db.session.add(last_news)
|
|
db.session.commit()
|
|
return
|
|
|
|
news = ScolarNews(
|
|
dept_id=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
|
|
"""
|
|
from app.models.formsemestre import FormSemestre
|
|
from app.models.moduleimpls import ModuleImpl
|
|
|
|
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 = db.session.get(ModuleImpl, 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 = db.session.get(FormSemestre, 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 = url_for("scolar.index_html", scodoc_dept=g.scodoc_dept)[
|
|
: -len("/index_html")
|
|
]
|
|
txt = re.sub('href=/.*?"', 'href="' + base + "/", txt)
|
|
|
|
# Transforme les liens HTML en texte brut: '<a href="url">texte</a>' devient 'texte: url'
|
|
# (si on veut des messages non html)
|
|
txt = re.sub(r'<a.*?href\s*=\s*"(.*?)".*?>(.*?)</a>', r"\2: \1", txt)
|
|
|
|
subject = "[ScoDoc] " + self.NEWS_MAP.get(self.type, "?")
|
|
sender = email.get_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 ""
|
|
dept_news_url = url_for("scolar.dept_news", scodoc_dept=g.scodoc_dept)
|
|
H = [
|
|
f"""<div class="scobox news"><div class="scobox-title"><a href="{
|
|
dept_news_url
|
|
}">Dernières opérations</a>
|
|
</div><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(
|
|
f"""<li class="newslist">
|
|
<span class="newstext"><a href="{dept_news_url}" class="stdlink">...</a>
|
|
</span>
|
|
</li>"""
|
|
)
|
|
|
|
H.append("</ul></div>")
|
|
|
|
# Informations générales
|
|
H.append(
|
|
f"""<div>
|
|
Pour en savoir plus sur ScoDoc voir
|
|
<a class="stdlink" href="{scu.SCO_ANNONCES_WEBSITE}">scodoc.org</a>
|
|
</div>
|
|
"""
|
|
)
|
|
|
|
return "\n".join(H)
|