diff --git a/app/scodoc/sco_news.py b/app/scodoc/sco_news.py
deleted file mode 100644
index a516ddbaa6bf8709a46855a6b7eecb74dd47b2f6..0000000000000000000000000000000000000000
--- a/app/scodoc/sco_news.py
+++ /dev/null
@@ -1,274 +0,0 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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 re
-import time
-
-
-from operator import itemgetter
-
-from flask import g
-from flask_login import current_user
-
-import app.scodoc.sco_utils as scu
-import app.scodoc.notesdb as ndb
-from app import log
-from app.scodoc import sco_formsemestre
-from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_preferences
-from app import email
-
-
-_scolar_news_editor = ndb.EditableTable(
-    "scolar_news",
-    "news_id",
-    ("date", "authenticated_user", "type", "object", "text", "url"),
-    filter_dept=True,
-    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.
-    """
-    from app.scodoc import sco_users
-
-    authuser_name = current_user.user_name
-    cnx = ndb.GetDBConnexion()
-    args = {
-        "authenticated_user": authuser_name,
-        "user_info": sco_users.user_info(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)
-
-
-def scolar_news_summary(n=5):
-    """Return last n news.
-    News are "compressed", ie redondant events are joined.
-    """
-    from app.scodoc import sco_users
-
-    cnx = ndb.GetDBConnexion()
-    cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
-    cursor.execute(
-        """SELECT id AS news_id, *
-    FROM scolar_news
-    WHERE dept_id=%(dept_id)s
-    ORDER BY date DESC LIMIT 100
-    """,
-        {"dept_id": g.scodoc_dept_id},
-    )
-    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")
-        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 = scu.MONTH_NAMES_ABBREV[int(m) - 1]
-        n["formatted_date"] = f'{j} {mois} {n["hm"]}'
-        # indication semestre si ajout notes:
-        infos = _get_formsemestre_infos_from_news(n)
-        if infos:
-            n["text"] += (
-                ' (<a class="stdlink" href="Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(descr_sem)s</a>)'
-                % infos
-            )
-        n["text"] += (
-            " par " + sco_users.user_info(n["authenticated_user"])["nomcomplet"]
-        )
-    return news
-
-
-def _get_formsemestre_infos_from_news(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"]
-        if n["object"]:
-            mods = sco_moduleimpl.moduleimpl_list(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(formsemestre_id)
-    except ValueError:
-        # semestre n'existe plus
-        return {}
-
-    if sem["semestre_id"] > 0:
-        descr_sem = f'S{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(n=5):
-    """News summary, formated in HTML"""
-    news = scolar_news_summary(n=n)
-    if not news:
-        return ""
-    H = ['<div class="news"><span class="newstitle">Dernières opérations']
-    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>
-    """
-        % scu.SCO_ANNONCES_WEBSITE
-    )
-
-    H.append("</div>")
-    return "\n".join(H)
-
-
-def _send_news_by_mail(n):
-    """Notify by email"""
-    infos = _get_formsemestre_infos_from_news(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)
-
-    subject = "[ScoDoc] " + NEWS_MAP.get(n["type"], "?")
-    sender = prefs["email_from_addr"]
-
-    email.send_email(subject, sender, destinations, txt)
diff --git a/requirements-3.9.txt b/requirements-3.9.txt
index b4a10ac1642bc1cafe1a7d180162746696515c2d..2f91f07a769854eedcf00ff9b0dbba737810816e 100755
--- a/requirements-3.9.txt
+++ b/requirements-3.9.txt
@@ -1,5 +1,5 @@
 alembic==1.7.5
-astroid==2.9.1
+astroid==2.11.2
 attrs==21.4.0
 Babel==2.9.1
 blinker==1.4
@@ -35,6 +35,7 @@ isort==5.10.1
 itsdangerous==2.0.1
 Jinja2==3.0.3
 lazy-object-proxy==1.7.1
+lxml==4.8.0
 Mako==1.1.6
 MarkupSafe==2.0.1
 mccabe==0.6.1
@@ -53,6 +54,7 @@ pyOpenSSL==21.0.0
 pyparsing==3.0.6
 pytest==6.2.5
 python-dateutil==2.8.2
+python-docx==0.8.11
 python-dotenv==0.19.2
 python-editor==1.0.4
 pytz==2021.3