diff --git a/app/api/__init__.py b/app/api/__init__.py
index a6f2b680..fb994bfd 100644
--- a/app/api/__init__.py
+++ b/app/api/__init__.py
@@ -3,9 +3,11 @@
from flask_json import as_json
from flask import Blueprint
from flask import request, g
+from flask_login import current_user
from app import db
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoException
+from app.scodoc.sco_permissions import Permission
api_bp = Blueprint("api", __name__)
api_web_bp = Blueprint("apiweb", __name__)
@@ -48,13 +50,21 @@ def requested_format(default_format="json", allowed_formats=None):
@as_json
-def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model = None):
+def get_model_api_object(
+ model_cls: db.Model,
+ model_id: int,
+ join_cls: db.Model = None,
+ restrict: bool | None = None,
+):
"""
Retourne une réponse contenant la représentation api de l'objet "Model[model_id]"
Filtrage du département en fonction d'une classe de jointure (eg: Identite, Formsemestre) -> join_cls
exemple d'utilisation : fonction "justificatif()" -> app/api/justificatifs.py
+
+ L'agument restrict est passé to_dict, est signale que l'on veut une version restreinte
+ (sans données personnelles, ou sans informations sur le justificatif d'absence)
"""
query = model_cls.query.filter_by(id=model_id)
if g.scodoc_dept and join_cls is not None:
@@ -66,8 +76,9 @@ def get_model_api_object(model_cls: db.Model, model_id: int, join_cls: db.Model
404,
message=f"{model_cls.__name__} inexistant(e)",
)
-
- return unique.to_dict(format_api=True)
+ if restrict is None:
+ return unique.to_dict(format_api=True)
+ return unique.to_dict(format_api=True, restrict=restrict)
from app.api import tokens
diff --git a/app/api/justificatifs.py b/app/api/justificatifs.py
index 9fd61fdc..a24b0f27 100644
--- a/app/api/justificatifs.py
+++ b/app/api/justificatifs.py
@@ -53,14 +53,19 @@ def justificatif(justif_id: int = None):
"date_fin": "2022-10-31T10:00+01:00",
"etat": "valide",
"fichier": "archive_id",
- "raison": "une raison",
+ "raison": "une raison", // VIDE si pas le droit
"entry_date": "2022-10-31T08:00+01:00",
"user_id": 1 or null,
}
"""
- return get_model_api_object(Justificatif, justif_id, Identite)
+ return get_model_api_object(
+ Justificatif,
+ justif_id,
+ Identite,
+ restrict=not current_user.has_permission(Permission.AbsJustifView),
+ )
# etudid
@@ -133,8 +138,9 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
# Mise en forme des données puis retour en JSON
data_set: list[dict] = []
+ restrict = not current_user.has_permission(Permission.AbsJustifView)
for just in justificatifs_query.all():
- data = just.to_dict(format_api=True)
+ data = just.to_dict(format_api=True, restrict=restrict)
data_set.append(data)
return data_set
@@ -172,14 +178,15 @@ def justificatifs_dept(dept_id: int = None, with_query: bool = False):
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
# Mise en forme des données et retour JSON
+ restrict = not current_user.has_permission(Permission.AbsJustifView)
data_set: list[dict] = []
for just in justificatifs_query:
- data_set.append(_set_sems(just))
+ data_set.append(_set_sems(just, restrict=restrict))
return data_set
-def _set_sems(justi: Justificatif) -> dict:
+def _set_sems(justi: Justificatif, restrict: bool) -> dict:
"""
_set_sems Ajoute le formsemestre associé au justificatif s'il existe
@@ -192,7 +199,7 @@ def _set_sems(justi: Justificatif) -> dict:
dict: La représentation de l'assiduité en dictionnaire
"""
# Conversion du justificatif en dictionnaire
- data = justi.to_dict(format_api=True)
+ data = justi.to_dict(format_api=True, restrict=restrict)
# Récupération du formsemestre de l'assiduité
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict())
@@ -246,9 +253,10 @@ def justificatifs_formsemestre(formsemestre_id: int, with_query: bool = False):
justificatifs_query: Query = _filter_manager(request, justificatifs_query)
# Retour des justificatifs en JSON
+ restrict = not current_user.has_permission(Permission.AbsJustifView)
data_set: list[dict] = []
for justi in justificatifs_query.all():
- data = justi.to_dict(format_api=True)
+ data = justi.to_dict(format_api=True, restrict=restrict)
data_set.append(data)
return data_set
diff --git a/app/models/assiduites.py b/app/models/assiduites.py
index c7cf8fa3..b4087e4e 100644
--- a/app/models/assiduites.py
+++ b/app/models/assiduites.py
@@ -88,8 +88,10 @@ class Assiduite(ScoDocModel):
lazy="select",
)
- def to_dict(self, format_api=True) -> dict:
- """Retourne la représentation json de l'assiduité"""
+ def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
+ """Retourne la représentation json de l'assiduité
+ restrict n'est pas utilisé ici.
+ """
etat = self.etat
user: User | None = None
if format_api:
@@ -453,8 +455,10 @@ class Justificatif(ScoDocModel):
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
return query.first_or_404()
- def to_dict(self, format_api: bool = False) -> dict:
- """transformation de l'objet en dictionnaire sérialisable"""
+ def to_dict(self, format_api: bool = False, restrict: bool = False) -> dict:
+ """L'objet en dictionnaire sérialisable.
+ Si restrict, ne donne par la raison et les fichiers et external_data
+ """
etat = self.etat
user: User = self.user if self.user_id is not None else None
@@ -469,13 +473,13 @@ class Justificatif(ScoDocModel):
"date_debut": self.date_debut,
"date_fin": self.date_fin,
"etat": etat,
- "raison": self.raison,
- "fichier": self.fichier,
+ "raison": None if restrict else self.raison,
+ "fichier": None if restrict else self.fichier,
"entry_date": self.entry_date,
"user_id": None if user is None else user.id, # l'uid
"user_name": None if user is None else user.user_name, # le login
"user_nom_complet": None if user is None else user.get_nomcomplet(),
- "external_data": self.external_data,
+ "external_data": None if restrict else self.external_data,
}
return data
diff --git a/app/scodoc/html_sco_header.py b/app/scodoc/html_sco_header.py
index 2e06370a..b76a8e77 100644
--- a/app/scodoc/html_sco_header.py
+++ b/app/scodoc/html_sco_header.py
@@ -145,7 +145,9 @@ def sco_header(
etudid=None,
formsemestre_id=None,
):
- "Main HTML page header for ScoDoc"
+ """Main HTML page header for ScoDoc
+ Utilisé dans les anciennes pages. Les nouvelles pages utilisent le template Jinja.
+ """
from app.scodoc.sco_formsemestre_status import formsemestre_page_title
if etudid is not None:
diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py
index dce59627..c0c732ce 100755
--- a/app/scodoc/html_sidebar.py
+++ b/app/scodoc/html_sidebar.py
@@ -32,12 +32,66 @@ from flask import render_template, url_for
from flask import g, request
from flask_login import current_user
+from app import db
+from app.models import Evaluation, GroupDescr, ModuleImpl, Partition
import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
from sco_version import SCOVERSION
+def retreive_formsemestre_from_request() -> int:
+ """Cherche si on a de quoi déduire le semestre affiché à partir des
+ arguments de la requête:
+ formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
+ Returns None si pas défini.
+ """
+ if request.method == "GET":
+ args = request.args
+ elif request.method == "POST":
+ args = request.form
+ else:
+ return None
+ formsemestre_id = None
+ # Search formsemestre
+ group_ids = args.get("group_ids", [])
+ if "formsemestre_id" in args:
+ formsemestre_id = args["formsemestre_id"]
+ elif "moduleimpl_id" in args and args["moduleimpl_id"]:
+ modimpl = db.session.get(ModuleImpl, args["moduleimpl_id"])
+ if not modimpl:
+ return None # suppressed ?
+ formsemestre_id = modimpl.formsemestre_id
+ elif "evaluation_id" in args:
+ evaluation = db.session.get(Evaluation, args["evaluation_id"])
+ if not evaluation:
+ return None # evaluation suppressed ?
+ formsemestre_id = evaluation.moduleimpl.formsemestre_id
+ elif "group_id" in args:
+ group = db.session.get(GroupDescr, args["group_id"])
+ if not group:
+ return None
+ formsemestre_id = group.partition.formsemestre_id
+ elif group_ids:
+ if isinstance(group_ids, str):
+ group_ids = group_ids.split(",")
+ group_id = group_ids[0]
+ group = db.session.get(GroupDescr, group_id)
+ if not group:
+ return None
+ formsemestre_id = group.partition.formsemestre_id
+ elif "partition_id" in args:
+ partition = db.session.get(Partition, args["partition_id"])
+ if not partition:
+ return None
+ formsemestre_id = partition.formsemestre_id
+
+ if formsemestre_id is None:
+ return None # no current formsemestre
+
+ return int(formsemestre_id)
+
+
def sidebar_common():
"partie commune à toutes les sidebar"
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
@@ -129,13 +183,17 @@ def sidebar(etudid: int = None):
)
H.append("
")
if current_user.has_permission(Permission.AbsChange):
+ # essaie de conserver le semestre actuellement en vue
+ cur_formsemestre_id = retreive_formsemestre_from_request()
H.append(
f"""
- Ajouter
- Justifier
"""
)
diff --git a/app/scodoc/sco_formsemestre_status.py b/app/scodoc/sco_formsemestre_status.py
index 1cea6daf..20d3ec65 100755
--- a/app/scodoc/sco_formsemestre_status.py
+++ b/app/scodoc/sco_formsemestre_status.py
@@ -76,6 +76,7 @@ from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
from app.scodoc import sco_users
from app.scodoc.gen_tables import GenTable
+from app.scodoc.html_sidebar import retreive_formsemestre_from_request
from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html
import sco_version
@@ -476,57 +477,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
return "\n".join(H)
-def retreive_formsemestre_from_request() -> int:
- """Cherche si on a de quoi déduire le semestre affiché à partir des
- arguments de la requête:
- formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
- Returns None si pas défini.
- """
- if request.method == "GET":
- args = request.args
- elif request.method == "POST":
- args = request.form
- else:
- return None
- formsemestre_id = None
- # Search formsemestre
- group_ids = args.get("group_ids", [])
- if "formsemestre_id" in args:
- formsemestre_id = args["formsemestre_id"]
- elif "moduleimpl_id" in args and args["moduleimpl_id"]:
- modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=args["moduleimpl_id"])
- if not modimpl:
- return None # suppressed ?
- modimpl = modimpl[0]
- formsemestre_id = modimpl["formsemestre_id"]
- elif "evaluation_id" in args:
- E = sco_evaluation_db.get_evaluations_dict(
- {"evaluation_id": args["evaluation_id"]}
- )
- if not E:
- return None # evaluation suppressed ?
- E = E[0]
- modimpl = sco_moduleimpl.moduleimpl_list(moduleimpl_id=E["moduleimpl_id"])[0]
- formsemestre_id = modimpl["formsemestre_id"]
- elif "group_id" in args:
- group = sco_groups.get_group(args["group_id"])
- formsemestre_id = group["formsemestre_id"]
- elif group_ids:
- if isinstance(group_ids, str):
- group_ids = group_ids.split(",")
- group_id = group_ids[0]
- group = sco_groups.get_group(group_id)
- formsemestre_id = group["formsemestre_id"]
- elif "partition_id" in args:
- partition = sco_groups.get_partition(args["partition_id"])
- formsemestre_id = partition["formsemestre_id"]
-
- if not formsemestre_id:
- return None # no current formsemestre
-
- return int(formsemestre_id)
-
-
# Element HTML decrivant un semestre (barre de menu et infos)
def formsemestre_page_title(formsemestre_id=None):
"""Element HTML decrivant un semestre (barre de menu et infos)
diff --git a/app/scodoc/sco_page_etud.py b/app/scodoc/sco_page_etud.py
index d34a933f..1f5d572f 100644
--- a/app/scodoc/sco_page_etud.py
+++ b/app/scodoc/sco_page_etud.py
@@ -46,11 +46,11 @@ from app.scodoc import (
sco_bac,
sco_cursus,
sco_etud,
- sco_formsemestre_status,
sco_groups,
sco_permissions_check,
sco_report,
)
+from app.scodoc.html_sidebar import retreive_formsemestre_from_request
from app.scodoc.sco_bulletins import etud_descr_situation_semestre
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table
@@ -751,7 +751,7 @@ def etud_info_html(etudid, with_photo="1", debug=False):
"""An HTML div with basic information and links about this etud.
Used for popups information windows.
"""
- formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request()
+ formsemestre_id = retreive_formsemestre_from_request()
with_photo = int(with_photo)
etud = Identite.get_etud(etudid)
diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py
index bf871252..6e5e53ee 100644
--- a/app/scodoc/sco_permissions.py
+++ b/app/scodoc/sco_permissions.py
@@ -24,7 +24,7 @@ _SCO_PERMISSIONS = (
(1 << 10, "EditAllNotes", "Modifier toutes les notes"),
(1 << 11, "EditAllEvals", "Modifier toutes les évaluations"),
(1 << 12, "EditFormSemestre", "Mettre en place une formation (créer un semestre)"),
- (1 << 13, "AbsChange", "Saisir des absences"),
+ (1 << 13, "AbsChange", "Saisir des absences ou justificatifs"),
(1 << 14, "AbsAddBillet", "Saisir des billets d'absences"),
# changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche
(1 << 15, "EtudChangeAdr", "Changer les adresses d'étudiants"),
@@ -63,7 +63,11 @@ _SCO_PERMISSIONS = (
#
# XXX inutilisée ? (1 << 40, "EtudChangePhoto", "Modifier la photo d'un étudiant"),
# Permissions du module Assiduité)
- (1 << 50, "AbsJustifView", "Visualisation des fichiers justificatifs"),
+ (
+ 1 << 50,
+ "AbsJustifView",
+ "Visualisation du détail des justificatifs (motif, fichiers)",
+ ),
# Attention: les permissions sont codées sur 64 bits.
)
diff --git a/app/tables/liste_assiduites.py b/app/tables/liste_assiduites.py
index 24a449b5..e2f93788 100644
--- a/app/tables/liste_assiduites.py
+++ b/app/tables/liste_assiduites.py
@@ -1,6 +1,7 @@
from datetime import datetime
from flask import url_for
+from flask_login import current_user
from flask_sqlalchemy.query import Query
from sqlalchemy import desc, literal, union, asc
@@ -10,6 +11,7 @@ from app.models import Assiduite, Identite, Justificatif
from app.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
from app.tables import table_builder as tb
from app.scodoc.sco_cache import RequeteTableauAssiduiteCache
+from app.scodoc.sco_permissions import Permission
class Pagination:
@@ -107,6 +109,11 @@ class ListeAssiJusti(tb.Table):
self.total_page: int = None
+ # Accès aux détail des justificatifs ?
+ self.can_view_justif_detail = current_user.has_permission(
+ Permission.AbsJustifView
+ )
+
# les lignes du tableau
self.rows: list["RowAssiJusti"] = []
@@ -342,7 +349,7 @@ class RowAssiJusti(tb.Row):
# Type d'objet
self._type()
- # En excel, on export les "vraes dates".
+ # En excel, on export les "vraies dates".
# En HTML, on écrit en français (on laisse les dates pour le tri)
multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
@@ -470,10 +477,21 @@ class RowAssiJusti(tb.Row):
def _optionnelles(self) -> None:
if self.table.options.show_desc:
+ if self.ligne.get("type") == "justificatif":
+ # protection de la "raison"
+ if (
+ self.ligne["user_id"] == current_user.id
+ or self.table.can_view_justif_detail
+ ):
+ description = self.ligne["desc"] if self.ligne["desc"] else ""
+ else:
+ description = "(cachée)"
+ else:
+ description = self.ligne["desc"] if self.ligne["desc"] else ""
self.add_cell(
"description",
"Description",
- self.ligne["desc"] if self.ligne["desc"] else "",
+ description,
)
if self.table.options.show_module:
if self.ligne["type"] == "assiduite":
diff --git a/app/templates/assiduites/pages/ajout_justificatif_etud.j2 b/app/templates/assiduites/pages/ajout_justificatif_etud.j2
index 61bd3197..27349ba5 100644
--- a/app/templates/assiduites/pages/ajout_justificatif_etud.j2
+++ b/app/templates/assiduites/pages/ajout_justificatif_etud.j2
@@ -17,6 +17,9 @@ form#ajout-justificatif-etud {
form#ajout-justificatif-etud > div {
margin-bottom: 16px;
}
+fieldset > div {
+ margin-bottom: 12px;
+}
div.fichiers {
margin-top: 16px;
margin-bottom: 32px;
@@ -33,9 +36,20 @@ div.submit {
div.submit > input {
margin-right: 16px;
}
+.info-saisie {
+ margin-top: 12px;
+ margin-bottom: 12px;
+ font-style: italic;
+}
-
Justifier des absences ou retards
+
{{title|safe}}
+
+ {% if justif %}
+
+ Saisie par {{justif.user.get_prenomnom()}} le {{justif.entry_date.strftime("%d/%m/%Y à %H:%M")}}
+
+ {% endif %}
@@ -72,16 +86,24 @@ div.submit > input {
{# Raison #}
-
{{ form.raison.label }}
- {{ form.raison() }}
- {{ render_field_errors(form, 'raison') }}
+ {% if (not justif) or can_view_justif_detail %}
+
{{ form.raison.label }}
+ {{ form.raison() }}
+ {{ render_field_errors(form, 'raison') }}
+
La raison sera visible aux utilisateurs ayant le droit
+ AbsJustifView et à celui ayant déposé le justificatif
+ {%- if justif %} ({{justif.user.get_prenomnom()}}){%- endif -%}.
+
+ {% else %}
+
raison confidentielle
+ {% endif %}
{# Liste des fichiers existants #}
{% if justif and nb_files > 0 %}
{{nb_files}} fichiers justificatifs déposés
{% if filenames|length < nb_files %}
- , dont {{filenames|length}} vous sont accessibles
+ , dont {{filenames|length}} vous {{'sont accessibles' if filenames|length > 1 else 'est accessible'}}
{% endif %}
@@ -104,6 +126,7 @@ div.submit > input {
{{ form.entry_date.label }} : {{ form.entry_date }}
laisser vide pour date courante
{{ render_field_errors(form, 'entry_date') }}
+
{# Submit #}
{{ form.submit }} {{ form.cancel }}
diff --git a/app/templates/assiduites/pages/tableau_assiduite_actions.j2 b/app/templates/assiduites/pages/tableau_assiduite_actions.j2
index 903fceba..705aaec3 100644
--- a/app/templates/assiduites/pages/tableau_assiduite_actions.j2
+++ b/app/templates/assiduites/pages/tableau_assiduite_actions.j2
@@ -10,11 +10,11 @@
{% if action == "modifier" %}
{% include "assiduites/widgets/tableau_actions/modifier.j2" %}
-{% else%}
+{% else %}
{% include "assiduites/widgets/tableau_actions/details.j2" %}
{% endif %}
-{% if not current_user.has_permission(sco.Permission.AbsJustifView)%}
+{% if not current_user.has_permission(sco.Permission.AbsJustifView) %}
Vous n'avez pas la permission d'ouvrir les fichiers justificatifs
déposés par d'autres personnes.
@@ -22,7 +22,7 @@
{% endif %}