Accès au détail d'un justificatif avec AbsJustifView: closes #824

This commit is contained in:
Emmanuel Viennet 2024-01-21 18:07:56 +01:00
parent ff63a32bbe
commit aee4f14b81
17 changed files with 274 additions and 161 deletions

View File

@ -3,9 +3,11 @@
from flask_json import as_json from flask_json import as_json
from flask import Blueprint from flask import Blueprint
from flask import request, g from flask import request, g
from flask_login import current_user
from app import db from app import db
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import AccessDenied, ScoException from app.scodoc.sco_exceptions import AccessDenied, ScoException
from app.scodoc.sco_permissions import Permission
api_bp = Blueprint("api", __name__) api_bp = Blueprint("api", __name__)
api_web_bp = Blueprint("apiweb", __name__) api_web_bp = Blueprint("apiweb", __name__)
@ -48,13 +50,21 @@ def requested_format(default_format="json", allowed_formats=None):
@as_json @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]" 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 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 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) query = model_cls.query.filter_by(id=model_id)
if g.scodoc_dept and join_cls is not None: 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, 404,
message=f"{model_cls.__name__} inexistant(e)", message=f"{model_cls.__name__} inexistant(e)",
) )
if restrict is None:
return unique.to_dict(format_api=True) return unique.to_dict(format_api=True)
return unique.to_dict(format_api=True, restrict=restrict)
from app.api import tokens from app.api import tokens

View File

@ -53,14 +53,19 @@ def justificatif(justif_id: int = None):
"date_fin": "2022-10-31T10:00+01:00", "date_fin": "2022-10-31T10:00+01:00",
"etat": "valide", "etat": "valide",
"fichier": "archive_id", "fichier": "archive_id",
"raison": "une raison", "raison": "une raison", // VIDE si pas le droit
"entry_date": "2022-10-31T08:00+01:00", "entry_date": "2022-10-31T08:00+01:00",
"user_id": 1 or null, "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 # 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 # Mise en forme des données puis retour en JSON
data_set: list[dict] = [] data_set: list[dict] = []
restrict = not current_user.has_permission(Permission.AbsJustifView)
for just in justificatifs_query.all(): 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) data_set.append(data)
return data_set 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) justificatifs_query: Query = _filter_manager(request, justificatifs_query)
# Mise en forme des données et retour JSON # Mise en forme des données et retour JSON
restrict = not current_user.has_permission(Permission.AbsJustifView)
data_set: list[dict] = [] data_set: list[dict] = []
for just in justificatifs_query: for just in justificatifs_query:
data_set.append(_set_sems(just)) data_set.append(_set_sems(just, restrict=restrict))
return data_set 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 _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 dict: La représentation de l'assiduité en dictionnaire
""" """
# Conversion du justificatif 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é # Récupération du formsemestre de l'assiduité
formsemestre: FormSemestre = get_formsemestre_from_data(justi.to_dict()) 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) justificatifs_query: Query = _filter_manager(request, justificatifs_query)
# Retour des justificatifs en JSON # Retour des justificatifs en JSON
restrict = not current_user.has_permission(Permission.AbsJustifView)
data_set: list[dict] = [] data_set: list[dict] = []
for justi in justificatifs_query.all(): 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) data_set.append(data)
return data_set return data_set

View File

@ -88,8 +88,10 @@ class Assiduite(ScoDocModel):
lazy="select", lazy="select",
) )
def to_dict(self, format_api=True) -> dict: def to_dict(self, format_api=True, restrict: bool | None = None) -> dict:
"""Retourne la représentation json de l'assiduité""" """Retourne la représentation json de l'assiduité
restrict n'est pas utilisé ici.
"""
etat = self.etat etat = self.etat
user: User | None = None user: User | None = None
if format_api: if format_api:
@ -453,8 +455,10 @@ class Justificatif(ScoDocModel):
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
return query.first_or_404() return query.first_or_404()
def to_dict(self, format_api: bool = False) -> dict: def to_dict(self, format_api: bool = False, restrict: bool = False) -> dict:
"""transformation de l'objet en dictionnaire sérialisable""" """L'objet en dictionnaire sérialisable.
Si restrict, ne donne par la raison et les fichiers et external_data
"""
etat = self.etat etat = self.etat
user: User = self.user if self.user_id is not None else None 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_debut": self.date_debut,
"date_fin": self.date_fin, "date_fin": self.date_fin,
"etat": etat, "etat": etat,
"raison": self.raison, "raison": None if restrict else self.raison,
"fichier": self.fichier, "fichier": None if restrict else self.fichier,
"entry_date": self.entry_date, "entry_date": self.entry_date,
"user_id": None if user is None else user.id, # l'uid "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_name": None if user is None else user.user_name, # le login
"user_nom_complet": None if user is None else user.get_nomcomplet(), "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 return data

View File

@ -145,7 +145,9 @@ def sco_header(
etudid=None, etudid=None,
formsemestre_id=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 from app.scodoc.sco_formsemestre_status import formsemestre_page_title
if etudid is not None: if etudid is not None:

View File

@ -32,12 +32,66 @@ from flask import render_template, url_for
from flask import g, request from flask import g, request
from flask_login import current_user 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 import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from sco_version import SCOVERSION 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(): def sidebar_common():
"partie commune à toutes les sidebar" "partie commune à toutes les sidebar"
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept) home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
@ -129,13 +183,17 @@ def sidebar(etudid: int = None):
) )
H.append("<ul>") H.append("<ul>")
if current_user.has_permission(Permission.AbsChange): if current_user.has_permission(Permission.AbsChange):
# essaie de conserver le semestre actuellement en vue
cur_formsemestre_id = retreive_formsemestre_from_request()
H.append( H.append(
f""" f"""
<li><a href="{ url_for('assiduites.ajout_assiduite_etud', <li><a href="{ url_for('assiduites.ajout_assiduite_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid) scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Ajouter</a></li> }">Ajouter</a></li>
<li><a href="{ url_for('assiduites.ajout_justificatif_etud', <li><a href="{ url_for('assiduites.ajout_justificatif_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid) scodoc_dept=g.scodoc_dept, etudid=etudid,
formsemestre_id=cur_formsemestre_id,
)
}">Justifier</a></li> }">Justifier</a></li>
""" """
) )

View File

@ -76,6 +76,7 @@ from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_users from app.scodoc import sco_users
from app.scodoc.gen_tables import GenTable 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 from app.scodoc.sco_formsemestre_custommenu import formsemestre_custommenu_html
import sco_version import sco_version
@ -476,57 +477,6 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
return "\n".join(H) 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) # Element HTML decrivant un semestre (barre de menu et infos)
def formsemestre_page_title(formsemestre_id=None): def formsemestre_page_title(formsemestre_id=None):
"""Element HTML decrivant un semestre (barre de menu et infos) """Element HTML decrivant un semestre (barre de menu et infos)

View File

@ -46,11 +46,11 @@ from app.scodoc import (
sco_bac, sco_bac,
sco_cursus, sco_cursus,
sco_etud, sco_etud,
sco_formsemestre_status,
sco_groups, sco_groups,
sco_permissions_check, sco_permissions_check,
sco_report, 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_bulletins import etud_descr_situation_semestre
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_formsemestre_validation import formsemestre_recap_parcours_table 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. """An HTML div with basic information and links about this etud.
Used for popups information windows. 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) with_photo = int(with_photo)
etud = Identite.get_etud(etudid) etud = Identite.get_etud(etudid)

View File

@ -24,7 +24,7 @@ _SCO_PERMISSIONS = (
(1 << 10, "EditAllNotes", "Modifier toutes les notes"), (1 << 10, "EditAllNotes", "Modifier toutes les notes"),
(1 << 11, "EditAllEvals", "Modifier toutes les évaluations"), (1 << 11, "EditAllEvals", "Modifier toutes les évaluations"),
(1 << 12, "EditFormSemestre", "Mettre en place une formation (créer un semestre)"), (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"), (1 << 14, "AbsAddBillet", "Saisir des billets d'absences"),
# changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche # changer adresse/photo ou pour envoyer bulletins par mail ou pour debouche
(1 << 15, "EtudChangeAdr", "Changer les adresses d'étudiants"), (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"), # XXX inutilisée ? (1 << 40, "EtudChangePhoto", "Modifier la photo d'un étudiant"),
# Permissions du module Assiduité) # 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. # Attention: les permissions sont codées sur 64 bits.
) )

View File

@ -1,6 +1,7 @@
from datetime import datetime from datetime import datetime
from flask import url_for from flask import url_for
from flask_login import current_user
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from sqlalchemy import desc, literal, union, asc 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.scodoc.sco_utils import EtatAssiduite, EtatJustificatif, to_bool
from app.tables import table_builder as tb from app.tables import table_builder as tb
from app.scodoc.sco_cache import RequeteTableauAssiduiteCache from app.scodoc.sco_cache import RequeteTableauAssiduiteCache
from app.scodoc.sco_permissions import Permission
class Pagination: class Pagination:
@ -107,6 +109,11 @@ class ListeAssiJusti(tb.Table):
self.total_page: int = None 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 # les lignes du tableau
self.rows: list["RowAssiJusti"] = [] self.rows: list["RowAssiJusti"] = []
@ -342,7 +349,7 @@ class RowAssiJusti(tb.Row):
# Type d'objet # Type d'objet
self._type() 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) # 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() multi_days = self.ligne["date_debut"].date() != self.ligne["date_fin"].date()
@ -470,10 +477,21 @@ class RowAssiJusti(tb.Row):
def _optionnelles(self) -> None: def _optionnelles(self) -> None:
if self.table.options.show_desc: 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( self.add_cell(
"description", "description",
"Description", "Description",
self.ligne["desc"] if self.ligne["desc"] else "", description,
) )
if self.table.options.show_module: if self.table.options.show_module:
if self.ligne["type"] == "assiduite": if self.ligne["type"] == "assiduite":

View File

@ -17,6 +17,9 @@ form#ajout-justificatif-etud {
form#ajout-justificatif-etud > div { form#ajout-justificatif-etud > div {
margin-bottom: 16px; margin-bottom: 16px;
} }
fieldset > div {
margin-bottom: 12px;
}
div.fichiers { div.fichiers {
margin-top: 16px; margin-top: 16px;
margin-bottom: 32px; margin-bottom: 32px;
@ -33,9 +36,20 @@ div.submit {
div.submit > input { div.submit > input {
margin-right: 16px; margin-right: 16px;
} }
.info-saisie {
margin-top: 12px;
margin-bottom: 12px;
font-style: italic;
}
</style> </style>
<div class="tab-content"> <div class="tab-content">
<h2>Justifier des absences ou retards</h2> <h2>{{title|safe}}</h2>
{% if justif %}
<div class="info-saisie">
Saisie par {{justif.user.get_prenomnom()}} le {{justif.entry_date.strftime("%d/%m/%Y à %H:%M")}}
</div>
{% endif %}
<section class="justi-form page"> <section class="justi-form page">
@ -72,16 +86,24 @@ div.submit > input {
</div> </div>
{# Raison #} {# Raison #}
<div> <div>
<div>{{ form.raison.label }}</div> {% if (not justif) or can_view_justif_detail %}
{{ form.raison() }} <div>{{ form.raison.label }}</div>
{{ render_field_errors(form, 'raison') }} {{ form.raison() }}
{{ render_field_errors(form, 'raison') }}
<div class="help">La raison sera visible aux utilisateurs ayant le droit
<tt>AbsJustifView</tt> et à celui ayant déposé le justificatif
{%- if justif %} (<b>{{justif.user.get_prenomnom()}}</b>){%- endif -%}.
</div>
{% else %}
<div class="unauthorized">raison confidentielle</div>
{% endif %}
</div> </div>
<div class="fichiers"> <div class="fichiers">
{# Liste des fichiers existants #} {# Liste des fichiers existants #}
{% if justif and nb_files > 0 %} {% if justif and nb_files > 0 %}
<div><b>{{nb_files}} fichiers justificatifs déposés <div><b>{{nb_files}} fichiers justificatifs déposés
{% if filenames|length < nb_files %} {% 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 %} {% endif %}
</b> </b>
</div> </div>
@ -104,6 +126,7 @@ div.submit > input {
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }} {{ form.entry_date.label }}&nbsp;: {{ form.entry_date }}
<span class="help">laisser vide pour date courante</span> <span class="help">laisser vide pour date courante</span>
{{ render_field_errors(form, 'entry_date') }} {{ render_field_errors(form, 'entry_date') }}
{# Submit #} {# Submit #}
<div class="submit"> <div class="submit">
{{ form.submit }} {{ form.cancel }} {{ form.submit }} {{ form.cancel }}

View File

@ -10,11 +10,11 @@
{% if action == "modifier" %} {% if action == "modifier" %}
{% include "assiduites/widgets/tableau_actions/modifier.j2" %} {% include "assiduites/widgets/tableau_actions/modifier.j2" %}
{% else%} {% else %}
{% include "assiduites/widgets/tableau_actions/details.j2" %} {% include "assiduites/widgets/tableau_actions/details.j2" %}
{% endif %} {% endif %}
{% if not current_user.has_permission(sco.Permission.AbsJustifView)%} {% if not current_user.has_permission(sco.Permission.AbsJustifView) %}
<div class="help fontred" style="margin-top: 16px;"> <div class="help fontred" style="margin-top: 16px;">
Vous n'avez pas la permission d'ouvrir les fichiers justificatifs Vous n'avez pas la permission d'ouvrir les fichiers justificatifs
déposés par d'autres personnes. déposés par d'autres personnes.
@ -22,7 +22,7 @@
{% endif %} {% endif %}
<div style="margin-top: 32px;"> <div style="margin-top: 32px;">
<a href="" id="lien-retour">retour</a> <a class="stdlink" href="" id="lien-retour">retour</a>
</div> </div>
<script> <script>
window.addEventListener('load', () => { window.addEventListener('load', () => {

View File

@ -1,13 +1,36 @@
<h2>Détails {{type}}</h2> <h2>Détails {{type}} concernant <span class="etudinfo"
id="etudid-{{objet.etudid}}">{{etud.html_link_fiche()|safe}}</span></h2>
<style>
.info-row {
margin-top: 12px;
}
.info-label {
font-weight: bold;
}
.info-etat {
font-size: 110%;
font-weight: bold;
background-color: rgb(253, 234, 210);
border: 1px solid grey;
border-radius: 4px;
padding: 4px;
}
.info-saisie {
margin-top: 12px;
margin-bottom: 12px;
font-style: italic;
}
</style>
<div id="informations"> <div id="informations">
<div class="info-row">
<span class="info-label">Étudiant{{etud.e}} concerné{{etud.e}}:</span> <span class="etudinfo" <div class="info-saisie">
id="etudid-{{objet.etudid}}">{{etud.html_link_fiche()|safe}}</span> <span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="info-label">Période :</span> {{objet.date_debut}} au {{objet.date_fin}} <span class="info-label">Période :</span> du <b>{{objet.date_debut}}</b> au <b>{{objet.date_fin}}</b>
</div> </div>
{% if type == "Assiduité" %} {% if type == "Assiduité" %}
@ -23,27 +46,27 @@
{% else %} {% else %}
<span class="info-label">État de l'assiduité :</span> <span class="info-label">État de l'assiduité :</span>
{% endif %} {% endif %}
<b>{{objet.etat}}</b> <span class="info-etat">{{objet.etat}}</span>
</div> </div>
<div class="info-row"> <div class="info-row">
{% if type == "Justificatif" %} {% if type == "Justificatif" %}
<div class="info-label">Raison:</div> <span class="info-label">Raison:</span>
{% if objet.raison != None %} {% if can_view_justif_detail %}
<div class="text">{{objet.raison}}</div> <span class="text">{{objet.raison or " "}}</span>
{% else %}
<span class="text unauthorized">(cachée)</span>
{% endif %}
{% else %} {% else %}
<div class="text">/div> <span class="info-label">Description:</span>
{% endif %}
{% else %}
<div class="info-label">Description:</div>
{% if objet.description != None %} {% if objet.description != None %}
<div class="text">{{objet.description}}</div> <span class="text">{{objet.description}}</span>
{% else %} {% else %}
<div class="text"></div> <span class="text"></span>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </span>
</div> </div>
{# Affichage des justificatifs si assiduité justifiée #} {# Affichage des justificatifs si assiduité justifiée #}
@ -54,7 +77,8 @@
<span class="text">Oui</span> <span class="text">Oui</span>
<div> <div>
{% for justi in objet.justification.justificatifs %} {% for justi in objet.justification.justificatifs %}
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}" <a href="{{url_for('assiduites.tableau_assiduite_actions',
type='justificatif', action='details', obj_id=justi.justif_id, scodoc_dept=g.scodoc_dept)}}"
target="_blank" rel="noopener noreferrer">Justificatif du {{justi.date_debut}} au {{justi.date_fin}}</a> target="_blank" rel="noopener noreferrer">Justificatif du {{justi.date_debut}} au {{justi.date_fin}}</a>
{% endfor %} {% endfor %}
</div> </div>
@ -69,13 +93,15 @@
<div class="info-row"> <div class="info-row">
<span class="info-label">Assiduités concernées: </span> <span class="info-label">Assiduités concernées: </span>
{% if objet.justification.assiduites %} {% if objet.justification.assiduites %}
<div> <ul>
{% for assi in objet.justification.assiduites %} {% for assi in objet.justification.assiduites %}
<a href="{{url_for('assiduites.tableau_assiduite_actions', type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)}}" <li><a href="{{url_for('assiduites.tableau_assiduite_actions',
target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au type='assiduite', action='details', obj_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept)
}}" target="_blank">Assiduité {{assi.etat}} du {{assi.date_debut}} au
{{assi.date_fin}}</a> {{assi.date_fin}}</a>
</li>
{% endfor %} {% endfor %}
</div> </ul>
{% else %} {% else %}
<span class="text">Aucune</span> <span class="text">Aucune</span>
{% endif %} {% endif %}
@ -84,27 +110,31 @@
{# Affichage des fichiers des justificatifs #} {# Affichage des fichiers des justificatifs #}
{% if type == "Justificatif"%} {% if type == "Justificatif"%}
<div class="info-row"> <div class="info-row">
<span class="info-label">Fichiers enregistrés: </span> <span class="info-label">Fichiers enregistrés: </span>
{% if objet.justification.fichiers.total != 0 %} {% if objet.justification.fichiers.total != 0 %}
<div>Total : {{objet.justification.fichiers.total}} </div> <div>Total : {{objet.justification.fichiers.total}} </div>
<ul> <ul>
{% for filename in objet.justification.fichiers.filenames %} {% for filename in objet.justification.fichiers.filenames %}
<li> <li>
<a <a
href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a> href="{{url_for('apiweb.justif_export',justif_id=objet.justif_id,
</li> filename=filename, scodoc_dept=g.scodoc_dept)}}">{{filename}}</a>
{% endfor %} </li>
{% if not objet.justification.fichiers.filenames %} {% endfor %}
<li class="fontred">fichiers non visibles</li> {% if not objet.justification.fichiers.filenames %}
<li class="fontred">fichiers non visibles</li>
{% endif %}
</ul>
{% else %}
<span class="text">Aucun</span>
{% endif %} {% endif %}
</ul> </div>
{% else %} {% if current_user.has_permission(sco.Permission.AbsChange) %}
<span class="text">Aucun</span> <div><a class="stdlink" href="{{
url_for('assiduites.edit_justificatif_etud', scodoc_dept=g.scodoc_dept, justif_id=obj_id)
}}">modifier ce justificatif</a>
</div>
{% endif %} {% endif %}
</div>
{% endif %} {% endif %}
</div>
<div class="info-row">
<span>Saisie par {{objet.saisie_par}} le {{objet.entry_date}}</span>
</div>

View File

@ -1,5 +1,7 @@
<h2>Modifier {{objet_name}} de {{ etud.html_link_fiche() | safe }}</h2> <h2>Modifier {{objet_name}} de {{ etud.html_link_fiche() | safe }}</h2>
{# XXX cette page ne semble plus utile ! remplacée par edit_justificatif_etud #}
<div> <div>
Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_debut}} au {{objet.date_fin}} Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_debut}} au {{objet.date_fin}}
</div> </div>
@ -39,8 +41,12 @@ Actuellement noté{{etud.e}} en <b>{{objet_name|lower()}}</b> du {{objet.date_de
<option value="modifie">Modifié</option> <option value="modifie">Modifié</option>
</select> </select>
<legend for="raison">Raison</legend> {% if current_user.has_permission(sco.Permission.AbsJustifView) %}
<textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea> <legend for="raison">Raison</legend>
<textarea name="raison" id="raison" cols="50" rows="5">{{objet.raison}}</textarea>
{% else %}
<div class="unauthorized">(raison non visible ni modifiable)</div>
{% endif %}
<legend>Fichiers</legend> <legend>Fichiers</legend>

View File

@ -1,6 +1,6 @@
{# -*- mode: jinja-html -*- #} {# -*- mode: jinja-html -*- #}
<h2 class="insidebar"><a href="{{ <h2 class="insidebar"><a href="{{
url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) url_for('scolar.index_html', scodoc_dept=g.scodoc_dept)
}}">Dépt. {{ prefs["DeptName"] }}</a> }}">Dépt. {{ prefs["DeptName"] }}</a>
</h2> </h2>
{% if prefs["DeptIntranetURL"] %} {% if prefs["DeptIntranetURL"] %}
@ -8,8 +8,3 @@
{{ prefs["DeptIntranetTitle"] }}</a> {{ prefs["DeptIntranetTitle"] }}</a>
{% endif %} {% endif %}
<br /> <br />
{#
# Entreprises pas encore supporté en ScoDoc8
# <br /><a href="%(ScoURL)s/Entreprises" class="sidebar">Entreprises</a> <br />
#}

View File

@ -14,6 +14,7 @@ from app.scodoc import notesdb as ndb
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
from app.scodoc import sco_formsemestre_status from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.html_sidebar import retreive_formsemestre_from_request
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
import sco_version import sco_version
@ -90,9 +91,7 @@ class ScoData:
self.etud = None self.etud = None
# --- Informations sur semestre courant, si sélectionné # --- Informations sur semestre courant, si sélectionné
if formsemestre is None: if formsemestre is None:
formsemestre_id = ( formsemestre_id = retreive_formsemestre_from_request()
sco_formsemestre_status.retreive_formsemestre_from_request()
)
if formsemestre_id is not None: if formsemestre_id is not None:
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
if formsemestre is None: if formsemestre is None:

View File

@ -31,6 +31,7 @@ from typing import Any
from flask import g, request, render_template, flash from flask import g, request, render_template, flash
from flask import abort, url_for, redirect, Response from flask import abort, url_for, redirect, Response
from flask_login import current_user from flask_login import current_user
from flask_sqlalchemy.query import Query
from app import db, log from app import db, log
from app.comp import res_sem from app.comp import res_sem
@ -81,7 +82,6 @@ from app.scodoc.sco_exceptions import ScoValueError
from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids from app.tables.visu_assiduites import TableAssi, etuds_sorted_from_ids
from app.scodoc.sco_archives_justificatifs import JustificatifArchiver from app.scodoc.sco_archives_justificatifs import JustificatifArchiver
from flask_sqlalchemy.query import Query
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@ -663,7 +663,9 @@ def bilan_etud():
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def edit_justificatif_etud(justif_id: int): def edit_justificatif_etud(justif_id: int):
""" """
Edition d'un justificatif Edition d'un justificatif.
Il faut de plus la permission pour voir/modifier la raison.
Args: Args:
justif_id (int): l'identifiant du justificatif justif_id (int): l'identifiant du justificatif
@ -704,21 +706,21 @@ def edit_justificatif_etud(justif_id: int):
"assi_limit_annee", "assi_limit_annee",
dept_id=g.scodoc_dept_id, dept_id=g.scodoc_dept_id,
), ),
can_view_justif_detail=current_user.has_permission(Permission.AbsJustifView)
or current_user.id == justif.user_id,
etud=justif.etudiant, etud=justif.etudiant,
filenames=filenames, filenames=filenames,
form=form, form=form,
justif=justif, justif=justif,
nb_files=nb_files, nb_files=nb_files,
page_title="Modification justificatif", title=f"Modification justificatif absence de {justif.etudiant.html_link_fiche()}",
redirect_url=redirect_url, redirect_url=redirect_url,
sco=ScoData(justif.etudiant), sco=ScoData(justif.etudiant),
scu=scu, scu=scu,
) )
@bp.route( @bp.route("/ajout_justificatif_etud", methods=["GET", "POST"])
"/ajout_justificatif_etud", methods=["GET", "POST"]
) # was AjoutJustificatifEtud
@scodoc @scodoc
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def ajout_justificatif_etud(): def ajout_justificatif_etud():
@ -766,7 +768,7 @@ def ajout_justificatif_etud():
), ),
etud=etud, etud=etud,
form=form, form=form,
page_title="Justificatifs", title=f"Ajout justificatif absence pour {etud.html_link_fiche()}",
redirect_url=redirect_url, redirect_url=redirect_url,
sco=ScoData(etud), sco=ScoData(etud),
scu=scu, scu=scu,
@ -1642,15 +1644,18 @@ def tableau_assiduite_actions():
return render_template( return render_template(
"assiduites/pages/tableau_assiduite_actions.j2", "assiduites/pages/tableau_assiduite_actions.j2",
action=action,
can_view_justif_detail=current_user.has_permission(Permission.AbsJustifView)
or (obj_type == "justificatif" and current_user.id == objet.user_id),
etud=objet.etudiant,
moduleimpl=module,
obj_id=obj_id,
objet_name=objet_name,
objet=_preparer_objet(obj_type, objet),
sco=ScoData(etud=objet.etudiant), sco=ScoData(etud=objet.etudiant),
title=f"Assiduité {objet.etudiant.nom_short}",
# type utilisé dans les actions modifier / détails (modifier.j2, details.j2) # type utilisé dans les actions modifier / détails (modifier.j2, details.j2)
type="Justificatif" if obj_type == "justificatif" else "Assiduité", type="Justificatif" if obj_type == "justificatif" else "Assiduité",
action=action,
etud=objet.etudiant,
objet=_preparer_objet(obj_type, objet),
objet_name=objet_name,
obj_id=obj_id,
moduleimpl=module,
) )
# ----- Cas POST # ----- Cas POST
if obj_type == "assiduite": if obj_type == "assiduite":
@ -2446,12 +2451,12 @@ class Jour:
self, matin: bool, show_pres: bool = False, show_reta: bool = False self, matin: bool, show_pres: bool = False, show_reta: bool = False
) -> str: ) -> str:
# Transformation d'une heure "HH:MM" en time(h,m) # Transformation d'une heure "HH:MM" en time(h,m)
STR_TIME = lambda x: datetime.time(*list(map(int, x.split(":")))) str2time = lambda x: datetime.time(*list(map(int, x.split(":"))))
heure_midi = STR_TIME(ScoDocSiteConfig.get("assi_lunch_time", "13:00")) heure_midi = str2time(ScoDocSiteConfig.get("assi_lunch_time", "13:00"))
if matin: if matin:
heure_matin = STR_TIME(ScoDocSiteConfig.get("assi_morning_time", "08:00")) heure_matin = str2time(ScoDocSiteConfig.get("assi_morning_time", "08:00"))
matin = ( matin = (
# date debut # date debut
scu.localize_datetime( scu.localize_datetime(
@ -2483,7 +2488,7 @@ class Jour:
return f"color {etat} {est_just}" return f"color {etat} {est_just}"
heure_soir = STR_TIME(ScoDocSiteConfig.get("assi_afternoon_time", "17:00")) heure_soir = str2time(ScoDocSiteConfig.get("assi_afternoon_time", "17:00"))
# séparation en demi journées # séparation en demi journées
aprem = ( aprem = (
@ -2522,17 +2527,17 @@ class Jour:
def generate_minitimeline(self) -> str: def generate_minitimeline(self) -> str:
# Récupérer le référenciel de la timeline # Récupérer le référenciel de la timeline
STR_TIME = lambda x: _time_to_timedelta( str2time = lambda x: _time_to_timedelta(
datetime.time(*list(map(int, x.split(":")))) datetime.time(*list(map(int, x.split(":"))))
) )
heure_matin: datetime.timedelta = STR_TIME( heure_matin: datetime.timedelta = str2time(
ScoDocSiteConfig.get("assi_morning_time", "08:00") ScoDocSiteConfig.get("assi_morning_time", "08:00")
) )
heure_midi: datetime.timedelta = STR_TIME( heure_midi: datetime.timedelta = str2time(
ScoDocSiteConfig.get("assi_lun_time", "13:00") ScoDocSiteConfig.get("assi_lun_time", "13:00")
) )
heure_soir: datetime.timedelta = STR_TIME( heure_soir: datetime.timedelta = str2time(
ScoDocSiteConfig.get("assi_afternoon_time", "17:00") ScoDocSiteConfig.get("assi_afternoon_time", "17:00")
) )
# longueur_timeline = heure_soir - heure_matin # longueur_timeline = heure_soir - heure_matin

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.79" SCOVERSION = "9.6.80"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"