Les étudiants démissionnaires ou défaillants ne sont pas pris en compte dans cette table.
+ """
+ )
H.append(
html_sco_header.sco_footer(),
)
diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py
index 43b58590..6152e881 100644
--- a/app/scodoc/sco_evaluations.py
+++ b/app/scodoc/sco_evaluations.py
@@ -36,32 +36,27 @@ from flask import g
from flask_login import current_user
from flask import request
-from app import log
-
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
+from app.models import ScolarNews
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
-from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
-import sco_version
from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_evaluation_db
from app.scodoc import sco_abs
-from app.scodoc import sco_cache
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
-from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_news
from app.scodoc import sco_permissions_check
from app.scodoc import sco_preferences
from app.scodoc import sco_users
+import sco_version
# --------------------------------------------------------------------
@@ -633,13 +628,16 @@ def evaluation_describe(evaluation_id="", edit_in_place=True):
'Dernières opérations']
- H.append('')
-
- for n in news:
- H.append(
- '- %(formatted_date)s%(text)s
'
- % n
- )
- H.append("
")
-
- # Informations générales
- H.append(
- """
- """
- % scu.SCO_ANNONCES_WEBSITE
- )
-
- H.append("
")
- 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 += (
- """(.*?)', 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/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py
index 112b0da5..9ccb6362 100644
--- a/app/scodoc/sco_preferences.py
+++ b/app/scodoc/sco_preferences.py
@@ -350,7 +350,7 @@ class BasePreferences(object):
"initvalue": "",
"title": "e-mails à qui notifier les opérations",
"size": 70,
- "explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc). (vous pouvez préférer utiliser le flux rss)",
+ "explanation": "adresses séparées par des virgules; notifie les opérations (saisies de notes, etc).",
"category": "general",
"only_global": False, # peut être spécifique à un semestre
},
diff --git a/app/scodoc/sco_saisie_notes.py b/app/scodoc/sco_saisie_notes.py
index fbef9245..9cda2372 100644
--- a/app/scodoc/sco_saisie_notes.py
+++ b/app/scodoc/sco_saisie_notes.py
@@ -39,6 +39,7 @@ from flask_login import current_user
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
+from app.models import ScolarNews
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
@@ -48,6 +49,7 @@ from app.scodoc.sco_exceptions import (
InvalidNoteValue,
NoteProcessError,
ScoGenError,
+ ScoInvalidParamError,
ScoValueError,
)
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
@@ -64,7 +66,6 @@ from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_news
from app.scodoc import sco_permissions_check
from app.scodoc import sco_undo_notes
from app.scodoc import sco_etud
@@ -274,11 +275,12 @@ def do_evaluation_upload_xls():
moduleimpl_id=mod["moduleimpl_id"],
_external=True,
)
- sco_news.add(
- typ=sco_news.NEWS_NOTE,
- object=M["moduleimpl_id"],
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_NOTE,
+ obj=M["moduleimpl_id"],
text='Chargement notes dans %(titre)s' % mod,
url=mod["url"],
+ max_frequency=30 * 60, # 30 minutes
)
msg = (
@@ -359,11 +361,12 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
scodoc_dept=g.scodoc_dept,
moduleimpl_id=mod["moduleimpl_id"],
)
- sco_news.add(
- typ=sco_news.NEWS_NOTE,
- object=M["moduleimpl_id"],
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_NOTE,
+ obj=M["moduleimpl_id"],
text='Initialisation notes dans %(titre)s' % mod,
url=mod["url"],
+ max_frequency=30 * 60,
)
return (
html_sco_header.sco_header()
@@ -451,9 +454,9 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
mod = sco_edit_module.module_list(args={"module_id": M["module_id"]})[0]
mod["moduleimpl_id"] = M["moduleimpl_id"]
mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod
- sco_news.add(
- typ=sco_news.NEWS_NOTE,
- object=M["moduleimpl_id"],
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_NOTE,
+ obj=M["moduleimpl_id"],
text='Suppression des notes d\'une évaluation dans %(titre)s'
% mod,
url=mod["url"],
@@ -893,10 +896,12 @@ def has_existing_decision(M, E, etudid):
def saisie_notes(evaluation_id, group_ids=[]):
"""Formulaire saisie notes d'une évaluation pour un groupe"""
+ if not isinstance(evaluation_id, int):
+ raise ScoInvalidParamError()
group_ids = [int(group_id) for group_id in group_ids]
evals = sco_evaluation_db.do_evaluation_list({"evaluation_id": evaluation_id})
if not evals:
- raise ScoValueError("invalid evaluation_id")
+ raise ScoValueError("évaluation inexistante")
E = evals[0]
M = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=E["moduleimpl_id"])[0]
formsemestre_id = M["formsemestre_id"]
@@ -1283,9 +1288,9 @@ def save_note(etudid=None, evaluation_id=None, value=None, comment=""):
nbchanged, _, existing_decisions = notes_add(
authuser, evaluation_id, L, comment=comment, do_it=True
)
- sco_news.add(
- typ=sco_news.NEWS_NOTE,
- object=M["moduleimpl_id"],
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_NOTE,
+ obj=M["moduleimpl_id"],
text='Chargement notes dans %(titre)s' % Mod,
url=Mod["url"],
max_frequency=30 * 60, # 30 minutes
diff --git a/app/scodoc/sco_synchro_etuds.py b/app/scodoc/sco_synchro_etuds.py
index db775438..6483ffcf 100644
--- a/app/scodoc/sco_synchro_etuds.py
+++ b/app/scodoc/sco_synchro_etuds.py
@@ -29,12 +29,14 @@
"""
import time
-import pprint
from operator import itemgetter
from flask import g, url_for
from flask_login import current_user
+from app import log
+from app.models import ScolarNews
+
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc import html_sco_header
@@ -43,11 +45,8 @@ from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_inscr_passage
-from app.scodoc import sco_news
-from app.scodoc import sco_excel
from app.scodoc import sco_portal_apogee
from app.scodoc import sco_etud
-from app import log
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_permissions import Permission
@@ -701,10 +700,10 @@ def do_import_etuds_from_portal(sem, a_importer, etudsapo_ident):
sco_cache.invalidate_formsemestre()
raise
- sco_news.add(
- typ=sco_news.NEWS_INSCR,
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_INSCR,
text="Import Apogée de %d étudiants en " % len(created_etudids),
- object=sem["formsemestre_id"],
+ obj=sem["formsemestre_id"],
)
diff --git a/app/static/css/scodoc.css b/app/static/css/scodoc.css
index 9811b304..5a244863 100644
--- a/app/static/css/scodoc.css
+++ b/app/static/css/scodoc.css
@@ -487,6 +487,16 @@ div.news {
border-radius: 8px;
}
+div.news a {
+ color: black;
+ text-decoration: none;
+}
+
+div.news a:hover {
+ color: rgb(153, 51, 51);
+ text-decoration: underline;
+}
+
span.newstitle {
font-weight: bold;
}
@@ -987,9 +997,34 @@ span.wtf-field ul.errors li {
font-weight: bold;
}
-.configuration_logo div.img {}
+.configuration_logo summary {
+ display: list-item !important;
+}
-.configuration_logo div.img-container {
+.configuration_logo h1 {
+ display: inline-block;
+}
+
+.configuration_logo h2 {
+ display: inline-block;
+}
+
+.configuration_logo h3 {
+ display: inline-block;
+}
+
+.configuration_logo details>*:not(summary) {
+ margin-left: 32px;
+}
+
+.configuration_logo .content {
+ display: grid;
+ grid-template-columns: auto auto 1fr;
+}
+
+.configuration_logo .image_logo {
+ vertical-align: top;
+ grid-column: 1/2;
width: 256px;
}
@@ -997,8 +1032,27 @@ span.wtf-field ul.errors li {
max-width: 100%;
}
-.configuration_logo div.img-data {
- vertical-align: top;
+.configuration_logo .infos_logo {
+ grid-column: 2/3;
+}
+
+.configuration_logo .actions_logo {
+ grid-column: 3/5;
+ display: grid;
+ grid-template-columns: auto auto;
+ grid-column-gap: 10px;
+ align-self: start;
+ grid-row-gap: 10px;
+}
+
+.configuration_logo .actions_logo .action_label {
+ grid-column: 1/2;
+ grid-template-columns: auto auto;
+}
+
+.configuration_logo .actions_logo .action_button {
+ grid-column: 2/3;
+ align-self: start;
}
.configuration_logo logo-edit titre {
@@ -3847,4 +3901,12 @@ table.evaluations_recap tr.evaluation.incomplete td a {
table.evaluations_recap tr.evaluation.incomplete td a.incomplete {
font-weight: bold;
+}
+
+table.evaluations_recap td.inscrits,
+table.evaluations_recap td.manquantes,
+table.evaluations_recap td.nb_abs,
+table.evaluations_recap td.nb_att,
+table.evaluations_recap td.nb_exc {
+ text-align: center;
}
\ No newline at end of file
diff --git a/app/templates/config_logos.html b/app/templates/config_logos.html
index f4bd543c..a4974ca7 100644
--- a/app/templates/config_logos.html
+++ b/app/templates/config_logos.html
@@ -20,73 +20,70 @@
{% endmacro %}
{% macro render_add_logo(add_logo_form) %}
-
-
Ajouter un logo
- {{ add_logo_form.hidden_tag() }}
- {{ render_field(add_logo_form.name) }}
- {{ render_field(add_logo_form.upload) }}
- {{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }}
-
+
+
+ Ajouter un logo
+
+
+ {{ render_field(add_logo_form.name) }}
+ {{ render_field(add_logo_form.upload) }}
+ {{ render_field(add_logo_form.do_insert, False, onSubmit="submit_form") }}
+
+
{% endmacro %}
{% macro render_logo(dept_form, logo_form) %}
-
- {{ logo_form.hidden_tag() }}
- {% if logo_form.titre %}
-
-
-
- {{ logo_form.titre }}
-
- {{ logo_form.description or "" }}
- |
-
- {% else %}
-
-
-
- Logo personalisé: {{ logo_form.logo_id.data }}
-
- {{ logo_form.description or "" }}
- |
-
- {% endif %}
-
-
-
+
+ {{ logo_form.hidden_tag() }}
+
+ {% if logo_form.titre %}
+ {{ logo_form.titre }}
+ {% if logo_form.description %}
+ {{ logo_form.description }}
+ {% endif %}
+ {% else %}
+ Logo personalisé: {{ logo_form.logo_id.data }}
+ {% if logo_form.description %}
+ {{ logo_form.description }}
+ {% endif %}
+ {% endif %}
+
+ |
-
- {{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})
- Taille: {{ logo_form.logo.size }} px
- {% if logo_form.logo.mm %} / {{ logo_form.logo.mm }} mm {% endif %}
- Aspect ratio: {{ logo_form.logo.aspect_ratio }}
- Usage: {{ logo_form.logo.get_usage() }}
- |
-
- Modifier l'image
- {{ render_field(logo_form.upload, False, onchange="submit_form()") }}
- {% if logo_form.can_delete %}
- Supprimer l'image
- {{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }}
- {% endif %}
- |
-
-
+
+
{{ logo_form.logo.logoname }} (Format: {{ logo_form.logo.suffix }})
+ Taille: {{ logo_form.logo.size }} px
+ {% if logo_form.logo.mm %} / {{ logo_form.logo.mm }} mm {% endif %}
+ Aspect ratio: {{ logo_form.logo.aspect_ratio }}
+ Usage: {{ logo_form.logo.get_usage() }}
+
+
+
Modifier l'image
+
+ {{ render_field(logo_form.upload, False, onchange="submit_form()") }}
+
+ {% if logo_form.can_delete %}
+
Supprimer l'image
+
+ {{ render_field(logo_form.do_delete, False, onSubmit="submit_form()") }}
+
+ {% endif %}
+
+
+
{% endmacro %}
{% macro render_logos(dept_form) %}
-
{% for logo_entry in dept_form.logos.entries %}
{% set logo_form = logo_entry.form %}
{{ render_logo(dept_form, logo_form) }}
{% else %}
-
-
Aucun logo défini en propre à ce département
+
+
Aucun logo défini en propre à ce département
{% endfor %}
-
{% endmacro %}
{% block app_content %}
@@ -100,25 +97,26 @@
Bibliothèque de logos
{% for dept_entry in form.depts.entries %}
- {% set dept_form = dept_entry.form %}
- {{ dept_entry.form.hidden_tag() }}
- {% if dept_entry.form.is_local() %}
-
-
Département {{ dept_form.dept_name.data }}
-
Logos locaux
-
Les paramètres donnés sont spécifiques à ce département.
- Les logos du département se substituent aux logos de même nom définis globalement:
-
- {% else %}
-
-
Logos généraux
-
Les images de cette section sont utilisé pour tous les départements,
- mais peuvent être redéfinies localement au niveau de chaque département
- (il suffit de définir un logo local de même nom)
-
- {% endif %}
- {{ render_logos(dept_form) }}
- {{ render_add_logo(dept_form.add_logo.form) }}
+
+ {% set dept_form = dept_entry.form %}
+ {{ dept_entry.form.hidden_tag() }}
+
+ {% if dept_entry.form.is_local() %}
+ Département {{ dept_form.dept_name.data }}
+ Les paramètres donnés sont spécifiques à ce département.
+ Les logos du département se substituent aux logos de même nom définis globalement:
+ {% else %}
+ Logos généraux
+ Les images de cette section sont utilisé pour tous les départements,
+ mais peuvent être redéfinies localement au niveau de chaque département
+ (il suffit de définir un logo local de même nom)
+ {% endif %}
+
+
+ {{ render_logos(dept_form) }}
+ {{ render_add_logo(dept_form.add_logo.form) }}
+
+
{% endfor %}
diff --git a/app/templates/dept_news.html b/app/templates/dept_news.html
new file mode 100644
index 00000000..23e3f26c
--- /dev/null
+++ b/app/templates/dept_news.html
@@ -0,0 +1,47 @@
+{# -*- mode: jinja-html -*- #}
+{% extends "sco_page.html" %}
+{% block styles %}
+{{super()}}
+{% endblock %}
+
+{% block app_content %}
+Opérations dans le département {{g.scodoc_dept}}
+
+
+
+
+ Date |
+ Type |
+ Auteur |
+ Détail |
+
+
+
+
+
+{% endblock %}
+
+
+{% block scripts %}
+{{super()}}
+
+{% endblock %}
diff --git a/app/views/scolar.py b/app/views/scolar.py
index 1b88575e..de987cb1 100644
--- a/app/views/scolar.py
+++ b/app/views/scolar.py
@@ -41,6 +41,7 @@ from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SubmitField
+from app import db
from app import log
from app.decorators import (
scodoc,
@@ -52,8 +53,10 @@ from app.decorators import (
)
from app.models.etudiants import Identite
from app.models.etudiants import make_etud_args
+from app.models.events import ScolarNews
from app.views import scolar_bp as bp
+from app.views import ScoData
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
@@ -339,6 +342,67 @@ def install_info():
return sco_up_to_date.is_up_to_date()
+@bp.route("/dept_news")
+@scodoc
+@permission_required(Permission.ScoView)
+def dept_news():
+ "Affiche table des dernières opérations"
+ return render_template(
+ "dept_news.html", title=f"Opérations {g.scodoc_dept}", sco=ScoData()
+ )
+
+
+@bp.route("/dept_news_json")
+@scodoc
+@permission_required(Permission.ScoView)
+def dept_news_json():
+ "Table des news du département"
+ start = request.args.get("start", type=int)
+ length = request.args.get("length", type=int)
+
+ log(f"dept_news_json( start={start}, length={length})")
+ query = ScolarNews.query.filter_by(dept_id=g.scodoc_dept_id)
+ # search
+ search = request.args.get("search[value]")
+ if search:
+ query = query.filter(
+ db.or_(
+ ScolarNews.authenticated_user.like(f"%{search}%"),
+ ScolarNews.text.like(f"%{search}%"),
+ )
+ )
+ total_filtered = query.count()
+ # sorting
+ order = []
+ i = 0
+ while True:
+ col_index = request.args.get(f"order[{i}][column]")
+ if col_index is None:
+ break
+ col_name = request.args.get(f"columns[{col_index}][data]")
+ if col_name not in ["date", "type", "authenticated_user"]:
+ col_name = "date"
+ descending = request.args.get(f"order[{i}][dir]") == "desc"
+ col = getattr(ScolarNews, col_name)
+ if descending:
+ col = col.desc()
+ order.append(col)
+ i += 1
+ if order:
+ query = query.order_by(*order)
+
+ # pagination
+ query = query.offset(start).limit(length)
+ data = [news.to_dict() for news in query]
+ # response
+ return {
+ "data": data,
+ "recordsFiltered": total_filtered,
+ "recordsTotal": ScolarNews.query.count(),
+ "draw": request.args.get("draw", type=int),
+ }
+
+
sco_publish(
"/trombino", sco_trombino.trombino, Permission.ScoView, methods=["GET", "POST"]
)
diff --git a/requirements-3.9.txt b/requirements-3.9.txt
index b4a10ac1..2f91f07a 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
diff --git a/sco_version.py b/sco_version.py
index 4b082410..0f7ec6c5 100644
--- a/sco_version.py
+++ b/sco_version.py
@@ -1,13 +1,21 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
-SCOVERSION = "9.2.0api"
+SCOVERSION = "9.2.1"
SCONAME = "ScoDoc"
SCONEWS = """
Année 2021
+- ScoDoc 9.2:
+
+ - Tableau récap. complet pour BUT et autres formations.
+ - Tableau état évaluations
+ - Version alpha du module "relations entreprises"
+ - Export des trombinoscope en document docx
+ - Très nombreux correctifs
+
- ScoDoc 9.1.75: bulletins BUT pdf
- ScoDoc 9.1.50: nombreuses amélioration gestion BUT
- ScoDoc 9.1: gestion des formations par compétences, type BUT.
diff --git a/scodoc.py b/scodoc.py
index c728bed9..1d3b1726 100755
--- a/scodoc.py
+++ b/scodoc.py
@@ -77,6 +77,7 @@ def make_shell_context():
"pp": pp,
"Role": Role,
"scolar": scolar,
+ "ScolarNews": models.ScolarNews,
"scu": scu,
"UniteEns": UniteEns,
"User": User,