# -*- 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.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_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) # 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): """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. """ 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): # 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=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 """ from app.models.formsemestre import FormSemestre 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 = 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 = 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 "" 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><a class="discretelink" href="{scu.SCO_ANNONCES_WEBSITE}"> Pour en savoir plus sur ScoDoc voir le site scodoc.org</a>. </div> """ ) H.append("</div>") return "\n".join(H)