# -*- 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): "exemple: '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"""({formsemestre.sem_modalite()})""" 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"""{formsemestre.sem_modalite()} """ 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: 'texte' devient 'texte: url' # (si on veut des messages non html) txt = re.sub(r'(.*?)', 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"""Dernières opérations """ ] for news in news_list: H.append( f"""{news.formatted_date()}{news}""" ) H.append( f""" ... """ ) H.append("") # Informations générales H.append( f""" Pour en savoir plus sur ScoDoc voir scodoc.org """ ) return "\n".join(H)