# -*- 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 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)