MonScoDocEssai/app/scodoc/sco_news.py

312 lines
10 KiB
Python

# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Gestion des "nouvelles"
"""
import datetime
import re
import time
from io import StringIO
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.header import Header
from operator import itemgetter
import six
import PyRSS2Gen # pylint: disable=import-error
from flask_login import current_user
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.notes_log import log
from app.scodoc import safehtml
from app.scodoc import sco_emails
from app.scodoc.sco_utils import SCO_ENCODING, SCO_ANNONCES_WEBSITE
from app.scodoc import sco_formsemestre
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_users
_scolar_news_editor = ndb.EditableTable(
"scolar_news",
"news_id",
("date", "authenticated_user", "type", "object", "text", "url"),
sortkey="date desc",
output_formators={"date": ndb.DateISOtoDMY},
input_formators={"date": ndb.DateDMYtoISO},
html_quote=False, # no user supplied data, needed to store html links
)
NEWS_INSCR = "INSCR" # inscription d'étudiants (object=None ou formsemestre_id)
NEWS_NOTE = "NOTES" # saisie note (object=moduleimpl_id)
NEWS_FORM = "FORM" # modification formation (object=formation_id)
NEWS_SEM = "SEM" # creation semestre (object=None)
NEWS_MISC = "MISC" # unused
NEWS_MAP = {
NEWS_INSCR: "inscription d'étudiants",
NEWS_NOTE: "saisie note",
NEWS_FORM: "modification formation",
NEWS_SEM: "création semestre",
NEWS_MISC: "opération", # unused
}
NEWS_TYPES = list(NEWS_MAP.keys())
scolar_news_create = _scolar_news_editor.create
scolar_news_list = _scolar_news_editor.list
_LAST_NEWS = {} # { (authuser_name, type, object) : time }
def add(typ, object=None, text="", url=None, max_frequency=False):
"""Ajoute une nouvelle.
Si max_frequency, ne genere pas 2 nouvelles identiques à moins de max_frequency
secondes d'intervalle.
"""
authuser_name = current_user.user_name
cnx = ndb.GetDBConnexion()
args = {
"authenticated_user": authuser_name,
"user_info": sco_users.user_info(user_name=authuser_name),
"type": typ,
"object": object,
"text": text,
"url": url,
}
t = time.time()
if max_frequency:
last_news_time = _LAST_NEWS.get((authuser_name, typ, object), False)
if last_news_time and (t - last_news_time < max_frequency):
# log("not recording")
return
log("news: %s" % args)
_LAST_NEWS[(authuser_name, typ, object)] = t
_send_news_by_mail(args)
return scolar_news_create(cnx, args, has_uniq_values=False)
def scolar_news_summary(context, n=5):
"""Return last n news.
News are "compressed", ie redondant events are joined.
"""
from app.scodoc import sco_etud
cnx = ndb.GetDBConnexion()
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute("select * from scolar_news order by date desc limit 100")
selected_news = {} # (type,object) : news dict
news = cursor.dictfetchall() # la plus récente d'abord
for r in reversed(news): # la plus ancienne d'abord
# si on a deja une news avec meme (type,object)
# et du meme jour, on la remplace
dmy = ndb.DateISOtoDMY(r["date"]) # round
key = (r["type"], r["object"], dmy)
selected_news[key] = r
news = list(selected_news.values())
# sort by date, descending
news.sort(key=itemgetter("date"), reverse=True)
news = news[:n]
# mimic EditableTable.list output formatting:
for n in news:
n["date822"] = n["date"].strftime("%a, %d %b %Y %H:%M:%S %z")
# heure
n["hm"] = n["date"].strftime("%Hh%M")
n["rssdate"] = n["date"].strftime("%d/%m %Hh%M") # pour affichage
for k in n.keys():
if n[k] is None:
n[k] = ""
if k in _scolar_news_editor.output_formators:
n[k] = _scolar_news_editor.output_formators[k](n[k])
# date resumee
j, m = n["date"].split("/")[:2]
mois = sco_etud.MONTH_NAMES_ABBREV[int(m) - 1]
n["formatted_date"] = "%s %s %s" % (j, mois, n["hm"])
# indication semestre si ajout notes:
infos = _get_formsemestre_infos_from_news(context, n)
if infos:
n["text"] += (
' (<a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>)'
% infos
)
n["text"] += (
" par "
+ sco_users.user_info(user_name=n["authenticated_user"])["nomcomplet"]
)
return news
def _get_formsemestre_infos_from_news(context, n):
"""Informations sur le semestre concerné par la nouvelle n
{} si inexistant
"""
formsemestre_id = None
if n["type"] == NEWS_INSCR:
formsemestre_id = n["object"]
elif n["type"] == NEWS_NOTE:
moduleimpl_id = n["object"]
mods = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)
if not mods:
return {} # module does not exists anymore
mod = mods[0]
formsemestre_id = mod["formsemestre_id"]
if not formsemestre_id:
return {}
try:
sem = sco_formsemestre.get_formsemestre(context, formsemestre_id)
except:
# semestre n'existe plus
return {}
if sem["semestre_id"] > 0:
descr_sem = "S%d" % sem["semestre_id"]
else:
descr_sem = ""
if sem["modalite"]:
descr_sem += " " + sem["modalite"]
return {"formsemestre_id": formsemestre_id, "sem": sem, "descr_sem": descr_sem}
def scolar_news_summary_html(context, n=5, rssicon=None):
"""News summary, formated in HTML"""
news = scolar_news_summary(context, n=n)
if not news:
return ""
H = ['<div class="news"><span class="newstitle">Dernières opérations']
if rssicon: # 2020-12-30 plus utilisé
H.append('<a href="rssnews">' + rssicon + "</a>")
H.append('</span><ul class="newslist">')
for n in news:
H.append(
'<li class="newslist"><span class="newsdate">%(formatted_date)s</span><span class="newstext">%(text)s</span></li>'
% n
)
H.append("</ul>")
# Informations générales
H.append(
"""<div>
Pour être informé des évolutions de ScoDoc,
vous pouvez vous
<a class="stdlink" href="%s">
abonner à la liste de diffusion</a>.
</div>
"""
% SCO_ANNONCES_WEBSITE
)
H.append("</div>")
return "\n".join(H)
def scolar_news_summary_rss(context, title, sco_url, n=5):
"""rss feed for scolar news"""
news = scolar_news_summary(context, n=n)
items = []
for n in news:
text = safehtml.convert_html_to_text(n["text"])
items.append(
PyRSS2Gen.RSSItem(
title=six.text_type("%s %s" % (n["rssdate"], text), SCO_ENCODING),
link=sco_url + "/" + n["url"],
pubDate=n["date822"],
)
)
rss = PyRSS2Gen.RSS2(
title=six.text_type(title, SCO_ENCODING),
link=sco_url,
description=six.text_type(title, SCO_ENCODING),
lastBuildDate=datetime.datetime.now(),
items=items,
)
f = StringIO()
rss.write_xml(f, encoding=SCO_ENCODING)
f.seek(0)
data = f.read()
f.close()
return data
def _send_news_by_mail(n):
"""Notify by email"""
context = None # #context
infos = _get_formsemestre_infos_from_news(context, n)
formsemestre_id = infos.get("formsemestre_id", None)
prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
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 = n["text"]
if infos:
txt += "\n\nSemestre %(titremois)s\n\n" % infos["sem"]
txt += (
"""<a href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>
"""
% infos
)
txt += "\n\nEffectué par: %(nomcomplet)s\n" % n["user_info"]
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 absolue
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)
msg = MIMEMultipart()
msg["Subject"] = Header("[ScoDoc] " + NEWS_MAP.get(n["type"], "?"), SCO_ENCODING)
msg["From"] = prefs["email_from_addr"]
txt = MIMEText(txt, "plain", SCO_ENCODING)
msg.attach(txt)
for email_addr in destinations:
if email_addr:
del msg["To"]
msg["To"] = email_addr
# log('xxx mail: %s' % msg)
sco_emails.sendEmail(context, msg)