1
0
forked from ScoDoc/ScoDoc

Compare commits

...

10 Commits

27 changed files with 703 additions and 570 deletions

View File

@ -569,10 +569,14 @@ def formsemestre_edt(formsemestre_id: int):
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur. Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
group_ids permet de filtrer sur les groupes ScoDoc. group_ids permet de filtrer sur les groupes ScoDoc.
show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
""" """
query = FormSemestre.query.filter_by(id=formsemestre_id) query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept: if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id) query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id) formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
group_ids = request.args.getlist("group_ids", int) group_ids = request.args.getlist("group_ids", int)
return sco_edt_cal.formsemestre_edt_dict(formsemestre, group_ids=group_ids) show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
return sco_edt_cal.formsemestre_edt_dict(
formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
)

View File

@ -11,6 +11,7 @@ from flask_json import as_json
from flask import g, request from flask import g, request
from flask_login import login_required, current_user from flask_login import login_required, current_user
from flask_sqlalchemy.query import Query from flask_sqlalchemy.query import Query
from werkzeug.exceptions import NotFound
import app.scodoc.sco_assiduites as scass import app.scodoc.sco_assiduites as scass
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -150,7 +151,7 @@ def justificatifs(etudid: int = None, nip=None, ine=None, with_query: bool = Fal
@as_json @as_json
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def justificatifs_dept(dept_id: int = None, with_query: bool = False): def justificatifs_dept(dept_id: int = None, with_query: bool = False):
""" """ """XXX TODO missing doc"""
# Récupération du département et des étudiants du département # Récupération du département et des étudiants du département
dept: Departement = Departement.query.get_or_404(dept_id) dept: Departement = Departement.query.get_or_404(dept_id)
@ -373,7 +374,7 @@ def _create_one(
date_debut=deb, date_debut=deb,
date_fin=fin, date_fin=fin,
etat=etat, etat=etat,
etud=etud, etudiant=etud,
raison=raison, raison=raison,
user_id=current_user.id, user_id=current_user.id,
external_data=external_data, external_data=external_data,
@ -419,9 +420,7 @@ def justif_edit(justif_id: int):
""" """
# Récupération du justificatif à modifier # Récupération du justificatif à modifier
justificatif_unique: Query = Justificatif.query.filter_by( justificatif_unique = Justificatif.get_justificatif(justif_id)
id=justif_id
).first_or_404()
errors: list[str] = [] errors: list[str] = []
data = request.get_json(force=True) data = request.get_json(force=True)
@ -497,7 +496,7 @@ def justif_edit(justif_id: int):
retour = { retour = {
"couverture": { "couverture": {
"avant": avant_ids, "avant": avant_ids,
"après": compute_assiduites_justified( "apres": compute_assiduites_justified(
justificatif_unique.etudid, justificatif_unique.etudid,
[justificatif_unique], [justificatif_unique],
True, True,
@ -561,12 +560,10 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
message : OK si réussi, message d'erreur sinon message : OK si réussi, message d'erreur sinon
""" """
# Récupération du justificatif à supprimer # Récupération du justificatif à supprimer
justificatif_unique: Justificatif = Justificatif.query.filter_by( try:
id=justif_id justificatif_unique = Justificatif.get_justificatif(justif_id)
).first() except NotFound:
if justificatif_unique is None:
return (404, "Justificatif non existant") return (404, "Justificatif non existant")
# Récupération de l'archive du justificatif # Récupération de l'archive du justificatif
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
@ -612,10 +609,7 @@ def justif_import(justif_id: int = None):
return json_error(404, "Il n'y a pas de fichier joint") return json_error(404, "Il n'y a pas de fichier joint")
# On récupère le justificatif auquel on va importer le fichier # On récupère le justificatif auquel on va importer le fichier
query: Query = Justificatif.query.filter_by(id=justif_id) justificatif_unique = Justificatif.get_justificatif(justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404()
# Récupération de l'archive si elle existe # Récupération de l'archive si elle existe
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
@ -658,10 +652,7 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
La permission est ScoView + (AbsJustifView ou être l'auteur du justifcatif) La permission est ScoView + (AbsJustifView ou être l'auteur du justifcatif)
""" """
# On récupère le justificatif concerné # On récupère le justificatif concerné
query: Query = Justificatif.query.filter_by(id=justif_id) justificatif_unique = Justificatif.get_justificatif(justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404()
# Vérification des permissions # Vérification des permissions
if not ( if not (
@ -694,6 +685,7 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
@as_json @as_json
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def justif_remove(justif_id: int = None): def justif_remove(justif_id: int = None):
# XXX TODO pas de test unitaire
""" """
Supression d'un fichier ou d'une archive Supression d'un fichier ou d'une archive
{ {
@ -710,10 +702,7 @@ def justif_remove(justif_id: int = None):
data: dict = request.get_json(force=True) data: dict = request.get_json(force=True)
# On récupère le justificatif concerné # On récupère le justificatif concerné
query: Query = Justificatif.query.filter_by(id=justif_id) justificatif_unique = Justificatif.get_justificatif(justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404()
# On récupère l'archive # On récupère l'archive
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
@ -775,10 +764,7 @@ def justif_list(justif_id: int = None):
""" """
# Récupération du justificatif concerné # Récupération du justificatif concerné
query: Query = Justificatif.query.filter_by(id=justif_id) justificatif_unique = Justificatif.get_justificatif(justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404()
# Récupération de l'archive avec l'archiver # Récupération de l'archive avec l'archiver
archive_name: str = justificatif_unique.fichier archive_name: str = justificatif_unique.fichier
@ -820,10 +806,7 @@ def justif_justifies(justif_id: int = None):
""" """
# On récupère le justificatif concerné # On récupère le justificatif concerné
query: Query = Justificatif.query.filter_by(id=justif_id) justificatif_unique = Justificatif.get_justificatif(justif_id)
if g.scodoc_dept:
query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id)
justificatif_unique: Justificatif = query.first_or_404()
# On récupère la liste des assiduités justifiées par le justificatif # On récupère la liste des assiduités justifiées par le justificatif
assiduites_list: list[int] = scass.justifies(justificatif_unique) assiduites_list: list[int] = scass.justifies(justificatif_unique)
@ -837,6 +820,7 @@ def justif_justifies(justif_id: int = None):
def _filter_manager(requested, justificatifs_query: Query): def _filter_manager(requested, justificatifs_query: Query):
""" """
Retourne les justificatifs entrés filtrés en fonction de la request Retourne les justificatifs entrés filtrés en fonction de la request
et du département courant s'il y en a un
""" """
# cas 1 : etat justificatif # cas 1 : etat justificatif
etat: str = requested.args.get("etat") etat: str = requested.args.get("etat")
@ -871,7 +855,7 @@ def _filter_manager(requested, justificatifs_query: Query):
formsemestre: FormSemestre = None formsemestre: FormSemestre = None
try: try:
formsemestre_id = int(formsemestre_id) formsemestre_id = int(formsemestre_id)
formsemestre = FormSemestre.query.filter_by(id=formsemestre_id).first() formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
justificatifs_query = scass.filter_by_formsemestre( justificatifs_query = scass.filter_by_formsemestre(
justificatifs_query, Justificatif, formsemestre justificatifs_query, Justificatif, formsemestre
) )
@ -906,4 +890,10 @@ def _filter_manager(requested, justificatifs_query: Query):
except ValueError: except ValueError:
group_id = None group_id = None
# Département
if g.scodoc_dept:
justificatifs_query = justificatifs_query.join(Identite).filter_by(
dept_id=g.scodoc_dept_id
)
return justificatifs_query return justificatifs_query

View File

@ -40,6 +40,7 @@ from wtforms import (
validators, validators,
) )
from wtforms.validators import DataRequired from wtforms.validators import DataRequired
from app.scodoc import sco_utils as scu
class AjoutAssiOrJustForm(FlaskForm): class AjoutAssiOrJustForm(FlaskForm):
@ -98,15 +99,7 @@ class AjoutAssiOrJustForm(FlaskForm):
"id": "assi_date_fin", "id": "assi_date_fin",
}, },
) )
assi_raison = TextAreaField(
"Raison",
render_kw={
"id": "assi_raison",
"cols": 75,
"rows": 4,
"maxlength": 500,
},
)
entry_date = StringField( entry_date = StringField(
"Date de dépot ou saisie", "Date de dépot ou saisie",
validators=[validators.Length(max=10)], validators=[validators.Length(max=10)],
@ -122,7 +115,15 @@ class AjoutAssiOrJustForm(FlaskForm):
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm): class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
"Formulaire de saisie d'une assiduité pour un étudiant" "Formulaire de saisie d'une assiduité pour un étudiant"
description = TextAreaField(
"Description",
render_kw={
"id": "description",
"cols": 75,
"rows": 4,
"maxlength": 500,
},
)
assi_etat = RadioField( assi_etat = RadioField(
"Signaler:", "Signaler:",
choices=[("absent", "absence"), ("retard", "retard"), ("present", "présence")], choices=[("absent", "absence"), ("retard", "retard"), ("present", "présence")],
@ -139,16 +140,24 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm): class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
"Formulaire de saisie d'un justificatif pour un étudiant" "Formulaire de saisie d'un justificatif pour un étudiant"
raison = TextAreaField(
"Raison",
render_kw={
"id": "raison",
"cols": 75,
"rows": 4,
"maxlength": 500,
},
)
etat = SelectField( etat = SelectField(
"État du justificatif", "État du justificatif",
choices=[ choices=[
("", "Choisir..."), # Placeholder ("", "Choisir..."), # Placeholder
("attente", "En attente de validation"), (scu.EtatJustificatif.ATTENTE.value, "En attente de validation"),
("non_valide", "Non valide"), (scu.EtatJustificatif.NON_VALIDE.value, "Non valide"),
("modifie", "Modifié"), (scu.EtatJustificatif.MODIFIE.value, "Modifié"),
("valide", "Valide"), (scu.EtatJustificatif.VALIDE.value, "Valide"),
], ],
validators=[DataRequired(message="This field is required.")], validators=[DataRequired(message="This field is required.")],
) )
fichiers = MultipleFileField() fichiers = MultipleFileField(label="Ajouter des fichiers")

View File

@ -503,6 +503,7 @@ class Justificatif(ScoDocModel):
archiver: JustificatifArchiver = JustificatifArchiver() archiver: JustificatifArchiver = JustificatifArchiver()
filenames = archiver.list_justificatifs(archive_name, self.etudiant) filenames = archiver.list_justificatifs(archive_name, self.etudiant)
accessible_filenames = [] accessible_filenames = []
#
for filename in filenames: for filename in filenames:
if int(filename[1]) == current_user.id or current_user.has_permission( if int(filename[1]) == current_user.id or current_user.has_permission(
Permission.AbsJustifView Permission.AbsJustifView

View File

@ -49,7 +49,6 @@
""" """
import datetime import datetime
import glob import glob
import json
import mimetypes import mimetypes
import os import os
import re import re
@ -58,29 +57,17 @@ import time
import chardet import chardet
import flask from flask import g
from flask import flash, g, request, url_for
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from config import Config from config import Config
from app import log, ScoDocJSONEncoder from app import log
from app.but import jury_but_pv
from app.comp import res_sem
from app.comp.res_compat import NotesTableCompat
from app.models import FormSemestre
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_exceptions import ScoException, ScoPermissionDenied
from app.scodoc import html_sco_header
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_groups
from app.scodoc import sco_groups_view
from app.scodoc import sco_pv_forms
from app.scodoc import sco_pv_lettres_inviduelles
from app.scodoc import sco_pv_pdf
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
class BaseArchiver: class BaseArchiver:
"""Classe de base pour tous les archivers"""
def __init__(self, archive_type=""): def __init__(self, archive_type=""):
self.archive_type = archive_type self.archive_type = archive_type
self.initialized = False self.initialized = False
@ -306,400 +293,3 @@ class BaseArchiver:
mime = "application/octet-stream" mime = "application/octet-stream"
return scu.send_file(data, filename, mime=mime) return scu.send_file(data, filename, mime=mime)
class SemsArchiver(BaseArchiver):
def __init__(self):
BaseArchiver.__init__(self, archive_type="")
PV_ARCHIVER = SemsArchiver()
# ----------------------------------------------------------------------------
def do_formsemestre_archive(
formsemestre_id,
group_ids: list[int] = None, # si indiqué, ne prend que ces groupes
description="",
date_jury="",
signature=None, # pour lettres indiv
date_commission=None,
numero_arrete=None,
code_vdi=None,
show_title=False,
pv_title=None,
pv_title_session=None,
with_paragraph_nom=False,
anonymous=False,
bul_version="long",
):
"""Make and store new archive for this formsemestre.
Store:
- tableau recap (xls), pv jury (xls et pdf), bulletins (xml et pdf), lettres individuelles (pdf)
"""
from app.scodoc.sco_recapcomplet import (
gen_formsemestre_recapcomplet_excel,
gen_formsemestre_recapcomplet_html_table,
gen_formsemestre_recapcomplet_json,
)
if bul_version not in scu.BULLETINS_VERSIONS:
raise ScoValueError(
"do_formsemestre_archive: version de bulletin demandée invalide"
)
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
sem_archive_id = formsemestre_id
archive_id = PV_ARCHIVER.create_obj_archive(
sem_archive_id, description, formsemestre.dept_id
)
date = PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y à %H:%M")
if not group_ids:
# tous les inscrits du semestre
group_ids = [sco_groups.get_default_group(formsemestre_id)]
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, formsemestre_id=formsemestre_id
)
groups_filename = "-" + groups_infos.groups_filename
etudids = [m["etudid"] for m in groups_infos.members]
# Tableau recap notes en XLS (pour tous les etudiants, n'utilise pas les groupes)
data, _ = gen_formsemestre_recapcomplet_excel(res, include_evaluations=True)
if data:
PV_ARCHIVER.store(
archive_id,
"Tableau_moyennes" + scu.XLSX_SUFFIX,
data,
dept_id=formsemestre.dept_id,
)
# Tableau recap notes en HTML (pour tous les etudiants, n'utilise pas les groupes)
table_html, _, _ = gen_formsemestre_recapcomplet_html_table(
formsemestre, res, include_evaluations=True
)
if table_html:
flash(f"Moyennes archivées le {date}", category="info")
data = "\n".join(
[
html_sco_header.sco_header(
page_title=f"Moyennes archivées le {date}",
no_side_bar=True,
),
f'<h2 class="fontorange">Valeurs archivées le {date}</h2>',
"""<style type="text/css">table.notes_recapcomplet tr { color: rgb(185,70,0); }
</style>""",
table_html,
html_sco_header.sco_footer(),
]
)
PV_ARCHIVER.store(
archive_id, "Tableau_moyennes.html", data, dept_id=formsemestre.dept_id
)
# Bulletins en JSON
data = gen_formsemestre_recapcomplet_json(formsemestre_id, xml_with_decisions=True)
data_js = json.dumps(data, indent=1, cls=ScoDocJSONEncoder)
if data:
PV_ARCHIVER.store(
archive_id, "Bulletins.json", data_js, dept_id=formsemestre.dept_id
)
# Décisions de jury, en XLS
if formsemestre.formation.is_apc():
response = jury_but_pv.pvjury_page_but(formsemestre_id, fmt="xls")
data = response.get_data()
else: # formations classiques
data = sco_pv_forms.formsemestre_pvjury(
formsemestre_id, fmt="xls", publish=False
)
if data:
PV_ARCHIVER.store(
archive_id,
"Decisions_Jury" + scu.XLSX_SUFFIX,
data,
dept_id=formsemestre.dept_id,
)
# Classeur bulletins (PDF)
data, _ = sco_bulletins_pdf.get_formsemestre_bulletins_pdf(
formsemestre_id, version=bul_version
)
if data:
PV_ARCHIVER.store(
archive_id,
"Bulletins.pdf",
data,
dept_id=formsemestre.dept_id,
)
# Lettres individuelles (PDF):
data = sco_pv_lettres_inviduelles.pdf_lettres_individuelles(
formsemestre_id,
etudids=etudids,
date_jury=date_jury,
date_commission=date_commission,
signature=signature,
)
if data:
PV_ARCHIVER.store(
archive_id,
f"CourriersDecisions{groups_filename}.pdf",
data,
dept_id=formsemestre.dept_id,
)
# PV de jury (PDF):
data = sco_pv_pdf.pvjury_pdf(
formsemestre,
etudids=etudids,
date_commission=date_commission,
date_jury=date_jury,
numero_arrete=numero_arrete,
code_vdi=code_vdi,
show_title=show_title,
pv_title_session=pv_title_session,
pv_title=pv_title,
with_paragraph_nom=with_paragraph_nom,
anonymous=anonymous,
)
if data:
PV_ARCHIVER.store(
archive_id,
f"PV_Jury{groups_filename}.pdf",
data,
dept_id=formsemestre.dept_id,
)
def formsemestre_archive(formsemestre_id, group_ids: list[int] = None):
"""Make and store new archive for this formsemestre.
(all students or only selected groups)
"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_pv():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
if not group_ids:
# tous les inscrits du semestre
group_ids = [sco_groups.get_default_group(formsemestre_id)]
groups_infos = sco_groups_view.DisplayedGroupsInfos(
group_ids, formsemestre_id=formsemestre_id
)
H = [
html_sco_header.html_sem_header(
"Archiver les PV et résultats du semestre",
javascripts=sco_groups_view.JAVASCRIPTS,
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
"""<p class="help">Cette page permet de générer et d'archiver tous
les documents résultant de ce semestre: PV de jury, lettres individuelles,
tableaux récapitulatifs.</p><p class="help">Les documents archivés sont
enregistrés et non modifiables, on peut les retrouver ultérieurement.
</p><p class="help">On peut archiver plusieurs versions des documents
(avant et après le jury par exemple).
</p>
""",
]
F = [
f"""<p><em>Note: les documents sont aussi affectés par les réglages sur la page
"<a class="stdlink" href="{
url_for("scolar.edit_preferences", scodoc_dept=g.scodoc_dept)
}">Paramétrage</a>"
(accessible à l'administrateur du département).</em>
</p>""",
html_sco_header.sco_footer(),
]
descr = [
(
"description",
{"input_type": "textarea", "rows": 4, "cols": 77, "title": "Description"},
),
("sep", {"input_type": "separator", "title": "Informations sur PV de jury"}),
]
descr += sco_pv_forms.descrform_pvjury(formsemestre)
descr += [
(
"signature",
{
"input_type": "file",
"size": 30,
"explanation": "optionnel: image scannée de la signature pour les lettres individuelles",
},
),
(
"bul_version",
{
"input_type": "menu",
"title": "Version des bulletins archivés",
"labels": [
"Version courte",
"Version intermédiaire",
"Version complète",
],
"allowed_values": scu.BULLETINS_VERSIONS.keys(),
"default": "long",
},
),
]
menu_choix_groupe = (
"""<div class="group_ids_sel_menu">Groupes d'étudiants à lister: """
+ sco_groups_view.menu_groups_choice(groups_infos)
+ """(pour les PV et lettres)</div>"""
)
tf = TrivialFormulator(
request.base_url,
scu.get_request_args(),
descr,
cancelbutton="Annuler",
submitlabel="Générer et archiver les documents",
name="tf",
formid="group_selector",
html_foot_markup=menu_choix_groupe,
)
if tf[0] == 0:
return "\n".join(H) + "\n" + tf[1] + "\n".join(F)
elif tf[0] == -1:
msg = "Opération annulée"
else:
# submit
sf = tf[2]["signature"]
signature = sf.read() # image of signature
if tf[2]["anonymous"]:
tf[2]["anonymous"] = True
else:
tf[2]["anonymous"] = False
do_formsemestre_archive(
formsemestre_id,
group_ids=group_ids,
description=tf[2]["description"],
date_jury=tf[2]["date_jury"],
date_commission=tf[2]["date_commission"],
signature=signature,
numero_arrete=tf[2]["numero_arrete"],
code_vdi=tf[2]["code_vdi"],
pv_title_session=tf[2]["pv_title_session"],
pv_title=tf[2]["pv_title"],
show_title=tf[2]["show_title"],
with_paragraph_nom=tf[2]["with_paragraph_nom"],
anonymous=tf[2]["anonymous"],
bul_version=tf[2]["bul_version"],
)
msg = "Nouvelle archive créée"
# submitted or cancelled:
flash(msg)
return flask.redirect(
url_for(
"notes.formsemestre_list_archives",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
def formsemestre_list_archives(formsemestre_id):
"""Page listing archives"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
sem_archive_id = formsemestre_id
L = []
for archive_id in PV_ARCHIVER.list_obj_archives(
sem_archive_id, dept_id=formsemestre.dept_id
):
a = {
"archive_id": archive_id,
"description": PV_ARCHIVER.get_archive_description(
archive_id, dept_id=formsemestre.dept_id
),
"date": PV_ARCHIVER.get_archive_date(archive_id),
"content": PV_ARCHIVER.list_archive(
archive_id, dept_id=formsemestre.dept_id
),
}
L.append(a)
H = [html_sco_header.html_sem_header("Archive des PV et résultats ")]
if not L:
H.append("<p>aucune archive enregistrée</p>")
else:
H.append("<ul>")
for a in L:
archive_name = PV_ARCHIVER.get_archive_name(a["archive_id"])
H.append(
'<li>%s : <em>%s</em> (<a href="formsemestre_delete_archive?formsemestre_id=%s&archive_name=%s">supprimer</a>)<ul>'
% (
a["date"].strftime("%d/%m/%Y %H:%M"),
a["description"],
formsemestre_id,
archive_name,
)
)
for filename in a["content"]:
H.append(
'<li><a href="formsemestre_get_archived_file?formsemestre_id=%s&archive_name=%s&filename=%s">%s</a></li>'
% (formsemestre_id, archive_name, filename, filename)
)
if not a["content"]:
H.append("<li><em>aucun fichier !</em></li>")
H.append("</ul></li>")
H.append("</ul>")
return "\n".join(H) + html_sco_header.sco_footer()
def formsemestre_get_archived_file(formsemestre_id, archive_name, filename):
"""Send file to client."""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
sem_archive_id = formsemestre.id
return PV_ARCHIVER.get_archived_file(
sem_archive_id, archive_name, filename, dept_id=formsemestre.dept_id
)
def formsemestre_delete_archive(formsemestre_id, archive_name, dialog_confirmed=False):
"""Delete an archive"""
formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
if not formsemestre.can_edit_pv():
raise ScoPermissionDenied(
dest_url=url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
)
sem_archive_id = formsemestre_id
archive_id = PV_ARCHIVER.get_id_from_name(
sem_archive_id, archive_name, dept_id=formsemestre.dept_id
)
dest_url = url_for(
"notes.formsemestre_list_archives",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id,
)
if not dialog_confirmed:
return scu.confirm_dialog(
f"""<h2>Confirmer la suppression de l'archive du {
PV_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M")
} ?</h2>
<p>La suppression sera définitive.</p>
""",
dest_url="",
cancel_url=dest_url,
parameters={
"formsemestre_id": formsemestre_id,
"archive_name": archive_name,
},
)
PV_ARCHIVER.delete_archive(archive_id, dept_id=formsemestre.dept_id)
flash("Archive supprimée")
return flask.redirect(dest_url)

View File

@ -16,7 +16,9 @@ from app import log
class Trace: class Trace:
"""gestionnaire de la trace des fichiers justificatifs""" """gestionnaire de la trace des fichiers justificatifs
XXX TODO à documenter: rôle et format des fichier strace
"""
def __init__(self, path: str) -> None: def __init__(self, path: str) -> None:
self.path: str = path + "/_trace.csv" self.path: str = path + "/_trace.csv"
@ -157,15 +159,15 @@ class JustificatifArchiver(BaseArchiver):
Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s) Si trace == True : sauvegarde le nom du/des fichier(s) supprimé(s)
dans la trace de l'étudiant dans la trace de l'étudiant
""" """
print("debug : ", archive_name, filename, has_trace) log(f"debug : {archive_name}{filename} {has_trace}")
if str(etud.id) not in self.list_oids(etud.dept_id): if str(etud.id) not in self.list_oids(etud.dept_id):
raise ValueError(f"Aucune archive pour etudid[{etud.id}]") raise ValueError(f"Aucune archive pour etudid[{etud.id}]")
try: try:
archive_id = self.get_id_from_name( archive_id = self.get_id_from_name(
etud.id, archive_name, dept_id=etud.dept_id etud.id, archive_name, dept_id=etud.dept_id
) )
except ScoValueError: except ScoValueError as exc:
raise ValueError(f"Archive Inconnue [{archive_name}]") raise ValueError(f"Archive Inconnue [{archive_name}]") from exc
if filename is not None: if filename is not None:
if filename not in self.list_archive(archive_id, dept_id=etud.dept_id): if filename not in self.list_archive(archive_id, dept_id=etud.dept_id):
@ -183,6 +185,7 @@ class JustificatifArchiver(BaseArchiver):
trace = Trace(archive_id) trace = Trace(archive_id)
trace.set_trace(filename, mode="delete") trace.set_trace(filename, mode="delete")
os.remove(path) os.remove(path)
log(f"delete_justificatif: removed {path}")
else: else:
if has_trace: if has_trace:
@ -197,12 +200,14 @@ class JustificatifArchiver(BaseArchiver):
archive_id, archive_id,
) )
) )
log(f"delete_justificatif: deleted archive {archive_id}")
def list_justificatifs( def list_justificatifs(
self, archive_name: str, etud: Identite self, archive_name: str, etud: Identite
) -> list[tuple[str, int]]: ) -> list[tuple[str, int]]:
""" """
Retourne la liste des noms de fichiers dans l'archive donnée Retourne la liste des noms de fichiers dans l'archive donnée
avec l'uid de l'utilisateur ayant saisi le fichier.
""" """
filenames: list[str] = [] filenames: list[str] = []
archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id) archive_id = self.get_id_from_name(etud.id, archive_name, dept_id=etud.dept_id)
@ -210,9 +215,8 @@ class JustificatifArchiver(BaseArchiver):
filenames = self.list_archive(archive_id, dept_id=etud.dept_id) filenames = self.list_archive(archive_id, dept_id=etud.dept_id)
trace: Trace = Trace(archive_id) trace: Trace = Trace(archive_id)
traced = trace.get_trace(filenames) traced = trace.get_trace(filenames)
retour = [(key, value[2]) for key, value in traced.items()]
return retour return [(key, value[2]) for key, value in traced.items()]
def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str): def get_justificatif_file(self, archive_name: str, etud: Identite, filename: str):
""" """

View File

@ -427,7 +427,7 @@ def create_absence(
db.session.commit() db.session.commit()
if est_just: if est_just:
justi = Justificatif.create_justificatif( justi = Justificatif.create_justificatif(
etud=etud, etudiant=etud,
date_debut=date_debut, date_debut=date_debut,
date_fin=date_fin, date_fin=date_fin,
etat=scu.EtatJustificatif.VALIDE, etat=scu.EtatJustificatif.VALIDE,

View File

@ -115,7 +115,9 @@ _EVENT_DEFAULT_COLOR = "rgb(214, 233, 248)"
def formsemestre_edt_dict( def formsemestre_edt_dict(
formsemestre: FormSemestre, group_ids: list[int] = None formsemestre: FormSemestre,
group_ids: list[int] = None,
show_modules_titles=True,
) -> list[dict]: ) -> list[dict]:
"""EDT complet du semestre, comme une liste de dict serialisable en json. """EDT complet du semestre, comme une liste de dict serialisable en json.
Fonction appelée par l'API /formsemestre/<int:formsemestre_id>/edt Fonction appelée par l'API /formsemestre/<int:formsemestre_id>/edt
@ -126,10 +128,12 @@ def formsemestre_edt_dict(
""" """
group_ids_set = set(group_ids) if group_ids else set() group_ids_set = set(group_ids) if group_ids else set()
try: try:
events_scodoc = _load_and_convert_ics(formsemestre) events_scodoc, _ = load_and_convert_ics(formsemestre)
except ScoValueError as exc: except ScoValueError as exc:
return exc.args[0] return exc.args[0]
# Génération des événements pour le calendrier html # Génération des événements pour le calendrier html
promo_icon = f"""<img height="24px" src="{scu.STATIC_DIR}/icons/promo.svg"
title="promotion complète" alt="promotion"/>"""
events_cal = [] events_cal = []
for event in events_scodoc: for event in events_scodoc:
group: GroupDescr | bool = event["group"] group: GroupDescr | bool = event["group"]
@ -140,7 +144,7 @@ def formsemestre_edt_dict(
</div>""" </div>"""
else: else:
group_disp = ( group_disp = (
f"""<div class="group-name">{group.get_nom_with_part(default="promo")}</div>""" f"""<div class="group-name">{group.get_nom_with_part(default=promo_icon)}</div>"""
if group if group
else f"""<div class="group-edt">{event['edt_group']} else f"""<div class="group-edt">{event['edt_group']}
<span title="vérifier noms de groupe ou configuration extraction edt"> <span title="vérifier noms de groupe ou configuration extraction edt">
@ -173,13 +177,14 @@ def formsemestre_edt_dict(
scu.EMO_WARNING} {event['edt_module']}</span>""" scu.EMO_WARNING} {event['edt_module']}</span>"""
bubble = "code module non trouvé dans ScoDoc. Vérifier configuration." bubble = "code module non trouvé dans ScoDoc. Vérifier configuration."
case _: # module EDT bien retrouvé dans ScoDoc case _: # module EDT bien retrouvé dans ScoDoc
mod_disp = f"""<span class="mod-name mod-code" title="{ bubble = f"{modimpl.module.abbrev or modimpl.module.titre or ''} ({event['edt_module']})"
modimpl.module.abbrev or ""} ({event['edt_module']})">{ mod_disp = (
modimpl.module.code}</span>""" f"""<span class="mod-name mod-code">{modimpl.module.code}</span>"""
bubble = f"{modimpl.module.abbrev or ''} ({event['edt_module']})" )
# {event['title_edt']}
title = f"""<div class = "module-edt" title="{bubble} {event['title_edt']}"> span_title = f" <span>{event['title']}</span>" if show_modules_titles else ""
<a class="discretelink" href="{url_abs or ''}">{mod_disp} <span>{event['title']}</span></a> title = f"""<div class = "module-edt" title="{bubble}">
<a class="discretelink" href="{url_abs or ''}">{mod_disp}{span_title}</a>
</div> </div>
""" """
@ -208,8 +213,15 @@ def formsemestre_edt_dict(
return events_cal return events_cal
def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]: def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[str]]:
"chargement fichier, filtrage et extraction des identifiants." """Chargement fichier ics, filtrage et extraction des identifiants.
Renvoie une liste d'évènements, et la liste des identifiants de groupes
trouvés (utilisée pour l'aide).
Groupes:
- False si extraction regexp non configuré
- "tous" (promo) si pas de correspondance trouvée.
"""
# Chargement du calendier ics # Chargement du calendier ics
_, calendar = formsemestre_load_calendar(formsemestre) _, calendar = formsemestre_load_calendar(formsemestre)
if not calendar: if not calendar:
@ -251,6 +263,7 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1] group_name: _COLOR_PALETTE[i % (len(_COLOR_PALETTE) - 1) + 1]
for i, group_name in enumerate(edt2group) for i, group_name in enumerate(edt2group)
} }
edt_groups_ids = set() # les ids de groupes tels que dans l'ics
default_group = formsemestre.get_default_group() default_group = formsemestre.get_default_group()
edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre) edt2modimpl = formsemestre_retreive_modimpls_from_edt_id(formsemestre)
# --- # ---
@ -271,6 +284,7 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
edt_group = extract_event_data( edt_group = extract_event_data(
event, edt_ics_group_field, edt_ics_group_pattern event, edt_ics_group_field, edt_ics_group_pattern
) )
edt_groups_ids.add(edt_group)
# si pas de groupe dans l'event, ou si groupe non reconnu, # si pas de groupe dans l'event, ou si groupe non reconnu,
# prend toute la promo ("tous") # prend toute la promo ("tous")
group: GroupDescr = ( group: GroupDescr = (
@ -324,7 +338,7 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
"end": event.decoded("dtend").isoformat(), "end": event.decoded("dtend").isoformat(),
} }
) )
return events_sco return events_sco, sorted(edt_groups_ids)
def extract_event_data( def extract_event_data(

View File

@ -56,12 +56,11 @@ from app.scodoc.sco_utils import ModuleType
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ( from app.scodoc.sco_exceptions import (
ScoValueError, ScoValueError,
ScoInvalidDateError,
ScoInvalidIdType, ScoInvalidIdType,
) )
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import htmlutils from app.scodoc import htmlutils
from app.scodoc import sco_archives from app.scodoc import sco_archives_formsemestre
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import codes_cursus from app.scodoc import codes_cursus
from app.scodoc import sco_compute_moy from app.scodoc import sco_compute_moy
@ -454,7 +453,9 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"title": "Documents archivés", "title": "Documents archivés",
"endpoint": "notes.formsemestre_list_archives", "endpoint": "notes.formsemestre_list_archives",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
"enabled": sco_archives.PV_ARCHIVER.list_obj_archives(formsemestre_id), "enabled": sco_archives_formsemestre.PV_ARCHIVER.list_obj_archives(
formsemestre_id
),
}, },
] ]

View File

@ -7,6 +7,7 @@
--color-justi-clair: #48f6ff; --color-justi-clair: #48f6ff;
--color-justi-attente: yellow; --color-justi-attente: yellow;
--color-justi-attente-stripe: #29b990; /* pink #fa25cb; */ /* #789dbb;*/ --color-justi-attente-stripe: #29b990; /* pink #fa25cb; */ /* #789dbb;*/
--color-justi-modifie: rgb(255, 230, 0);
--color-justi-invalide: #a84476; --color-justi-invalide: #a84476;
--color-nonwork: #badfff; --color-nonwork: #badfff;
@ -645,7 +646,7 @@
.assi-liste { .assi-liste {
border: 1px solid gray; border: 1px solid gray;
border-radius: 12px; border-radius: 12px;
margin-right: 12px; margin-right: 24px;
padding: 12px; padding: 12px;
} }
#options-tableau label { #options-tableau label {
@ -694,6 +695,9 @@ tr.row-justificatif.valide td.assi-type {
tr.row-justificatif.attente td.assi-type { tr.row-justificatif.attente td.assi-type {
background-color: var(--color-justi-attente); background-color: var(--color-justi-attente);
} }
tr.row-justificatif.modifie td.assi-type {
background-color: var(--color-justi-modifie);
}
tr.row-justificatif.non_valide td.assi-type { tr.row-justificatif.non_valide td.assi-type {
background-color: var(--color-justi-invalide); background-color: var(--color-justi-invalide);
} }

View File

@ -1,3 +1,8 @@
#show_modules_titles_form {
display: inline-block;
margin-left: 16px;
}
.toastui-calendar-template-time { .toastui-calendar-template-time {
padding: 4px; padding: 4px;
word-break: break-all; word-break: break-all;

140
app/static/icons/promo.svg Normal file
View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<ellipse style="fill:#F1DBC8;" cx="194.5" cy="111.67" rx="35.396" ry="35.671"/>
<ellipse style="fill:#F1DBC8;" cx="317.5" cy="111.67" rx="35.396" ry="35.671"/>
</g>
<path style="fill:#FF916C;" d="M250.277,172.685c-8.091-15.056-23.918-25.342-42.015-25.342H194.5h-13.764
c-18.097,0-33.924,10.286-42.014,25.342c16.826,2.757,29.673,17.471,29.673,35.208c0,19.701-15.848,35.671-35.395,35.671h13.763
c18.097,0,33.925,10.286,42.014,25.339c1.863-0.305,3.774-0.464,5.724-0.464c1.948,0,3.86,0.159,5.723,0.467
c8.09-15.056,23.917-25.342,42.014-25.342H256c-19.549,0-35.397-15.97-35.397-35.671
C220.603,190.155,233.449,175.441,250.277,172.685z"/>
<path style="fill:#C22C65;" d="M303.736,147.343c-18.097,0-33.924,10.286-42.014,25.342c16.827,2.757,29.673,17.471,29.673,35.208
c0,19.701-15.848,35.671-35.395,35.671h13.763c18.097,0,33.924,10.286,42.014,25.339c1.863-0.305,3.774-0.464,5.724-0.464
c1.948,0,3.859,0.159,5.723,0.467c8.09-15.056,23.917-25.342,42.014-25.342H379c-19.549,0-35.396-15.97-35.396-35.671
c0-17.737,12.846-32.451,29.673-35.208c-8.091-15.056-23.918-25.342-42.015-25.342H317.5L303.736,147.343L303.736,147.343z"/>
<g>
<path style="fill:#F1DBC8;" d="M168.395,207.893c0-17.737-12.847-32.451-29.673-35.208c-1.863-0.306-3.774-0.464-5.723-0.464
c-19.549,0-35.396,15.971-35.396,35.672s15.847,35.671,35.396,35.671S168.395,227.594,168.395,207.893z"/>
<path style="fill:#F1DBC8;" d="M250.277,172.685c-16.828,2.757-29.674,17.471-29.674,35.208c0,19.701,15.848,35.671,35.397,35.671
c19.548,0,35.395-15.97,35.395-35.671c0-17.737-12.846-32.451-29.673-35.208c-1.863-0.306-3.775-0.464-5.723-0.464
C254.052,172.221,252.14,172.379,250.277,172.685z"/>
<path style="fill:#F1DBC8;" d="M373.277,172.685c-16.827,2.757-29.673,17.471-29.673,35.208c0,19.701,15.847,35.671,35.396,35.671
c19.548,0,35.396-15.97,35.396-35.671S398.548,172.221,379,172.221C377.052,172.221,375.14,172.379,373.277,172.685z"/>
</g>
<path style="fill:#55CD8E;" d="M188.776,268.902c-8.089-15.053-23.917-25.339-42.014-25.339H133h-13.764
c-18.097,0-33.924,10.286-42.014,25.339c16.827,2.758,29.673,17.471,29.673,35.208c0,19.701-15.848,35.672-35.395,35.672h13.763
c26.256,0,47.737,21.648,47.737,48.108c0-26.46,21.481-48.108,47.736-48.108H194.5c-19.549,0-35.397-15.971-35.397-35.672
C159.103,286.373,171.949,271.66,188.776,268.902z"/>
<path style="fill:#876E67;" d="M242.236,243.564c-18.097,0-33.924,10.286-42.014,25.339c16.827,2.758,29.672,17.471,29.672,35.208
c0,19.701-15.847,35.672-35.394,35.672h13.763c26.255,0,47.737,21.648,47.737,48.108c0-26.46,21.481-48.108,47.736-48.108H317.5
c-19.549,0-35.396-15.971-35.396-35.672c0-17.737,12.845-32.45,29.672-35.205c-8.09-15.056-23.917-25.342-42.014-25.342H256
L242.236,243.564L242.236,243.564z"/>
<path style="fill:#4BC1D7;" d="M323.223,268.902c16.827,2.758,29.673,17.471,29.673,35.208c0,19.701-15.848,35.672-35.396,35.672
h13.763c26.256,0,47.737,21.648,47.737,48.108c0-26.46,21.481-48.108,47.736-48.108H440.5c-19.549,0-35.397-15.971-35.397-35.672
c0-17.737,12.846-32.45,29.674-35.205c-8.091-15.056-23.918-25.342-42.015-25.342H379h-13.764
C347.14,243.564,331.313,253.85,323.223,268.902z"/>
<g>
<path style="fill:#F1DBC8;" d="M71.5,339.782c19.548,0,35.395-15.971,35.395-35.672c0-17.737-12.846-32.45-29.673-35.208
c-1.863-0.305-3.774-0.464-5.723-0.464c-19.549,0-35.396,15.972-35.396,35.672C36.104,323.812,51.951,339.782,71.5,339.782z"/>
<path style="fill:#F1DBC8;" d="M159.103,304.11c0,19.701,15.848,35.672,35.397,35.672c19.548,0,35.394-15.971,35.394-35.672
c0-17.737-12.845-32.45-29.672-35.208c-1.862-0.305-3.774-0.464-5.723-0.464s-3.86,0.159-5.724,0.464
C171.949,271.66,159.103,286.373,159.103,304.11z"/>
<path style="fill:#F1DBC8;" d="M282.104,304.11c0,19.701,15.847,35.672,35.396,35.672c19.548,0,35.396-15.971,35.396-35.672
c0-17.737-12.846-32.45-29.673-35.208c-1.863-0.305-3.774-0.464-5.723-0.464s-3.86,0.159-5.724,0.467
C294.949,271.66,282.104,286.373,282.104,304.11z"/>
<path style="fill:#F1DBC8;" d="M405.103,304.11c0,19.701,15.848,35.672,35.397,35.672c19.548,0,35.395-15.971,35.395-35.672
c0-19.7-15.847-35.672-35.395-35.672c-1.949,0-3.86,0.159-5.723,0.467C417.949,271.66,405.103,286.373,405.103,304.11z"/>
</g>
<path style="fill:#BDD377;" d="M426.736,339.782c-26.255,0-47.736,21.648-47.736,48.108V436h123v-48.109
c0-26.46-21.482-48.108-47.737-48.108H440.5L426.736,339.782L426.736,339.782z"/>
<path style="fill:#FFBD50;" d="M379,387.891c0-26.46-21.481-48.108-47.737-48.108H317.5h-13.764
c-26.255,0-47.736,21.648-47.736,48.108V436h123V387.891z"/>
<path style="fill:#76C8D6;" d="M256,387.891c0-26.46-21.482-48.108-47.737-48.108H194.5h-13.764
c-26.255,0-47.736,21.648-47.736,48.108V436h123V387.891z"/>
<path style="fill:#F13D7C;" d="M133,387.891c0-26.46-21.481-48.108-47.737-48.108H71.5H57.736
C31.481,339.782,10,361.431,10,387.891V436h123V387.891z"/>
</g>
<path d="M432.234,388.82h-0.236c-5.522,0-10,4.478-10,10s4.478,10,10,10h0.236c5.522,0,10-4.478,10-10
S437.757,388.82,432.234,388.82z"/>
<path d="M501.999,446c5.523,0,10-4.478,10-10v-37.162c0-0.007,0.001-0.013,0.001-0.02s-0.001-0.013-0.001-0.02V387.89
c0-24.602-15.375-45.666-37.015-54.129c6.793-7.986,10.91-18.341,10.91-29.651c0-25.184-20.364-45.672-45.395-45.672
c-0.145,0-0.287,0.01-0.431,0.011c-6.659-9.589-15.928-16.842-26.524-20.975c6.758-7.977,10.851-18.305,10.851-29.582
c0-25.184-20.364-45.672-45.396-45.672c-0.144,0-0.285,0.01-0.429,0.011c-6.659-9.591-15.929-16.845-26.526-20.978
c6.758-7.977,10.851-18.305,10.851-29.583C362.896,86.488,342.531,66,317.5,66s-45.396,20.488-45.396,45.671
c0,11.278,4.093,21.607,10.851,29.583c-10.599,4.133-19.867,11.387-26.527,20.978c-0.143-0.001-0.285-0.011-0.428-0.011
c-0.144,0-0.286,0.01-0.429,0.011c-6.66-9.591-15.929-16.845-26.527-20.978c6.758-7.977,10.851-18.305,10.851-29.583
C239.894,86.488,219.531,66,194.5,66c-25.032,0-45.397,20.488-45.397,45.671c0,11.278,4.093,21.607,10.851,29.583
c-10.598,4.133-19.867,11.387-26.526,20.978c-0.143-0.001-0.285-0.011-0.428-0.011c-25.031,0-45.396,20.488-45.396,45.672
c0,11.277,4.093,21.605,10.851,29.582c-10.597,4.133-19.866,11.385-26.525,20.975c-0.144-0.001-0.286-0.011-0.43-0.011
c-25.031,0-45.396,20.488-45.396,45.672c0,11.31,4.117,21.665,10.911,29.651C15.375,342.225,0,363.289,0,387.891V436
c0,5.522,4.477,10,10,10H501.999 M465.895,304.11c0,14.155-11.392,25.671-25.395,25.671c-14.004,0-25.397-11.516-25.397-25.671
s11.393-25.672,25.397-25.672C454.503,278.439,465.895,289.955,465.895,304.11z M395.103,304.11c0,11.31,4.117,21.665,10.911,29.651
c-11.037,4.316-20.448,11.9-27.015,21.574c-6.566-9.674-15.978-17.258-27.015-21.574c6.793-7.986,10.91-18.341,10.91-29.651
c0-17.234-9.54-32.266-23.584-40.041c6.92-6.64,16.14-10.506,25.925-10.506h27.526c9.785,0,19.006,3.866,25.925,10.506
C404.645,271.845,395.103,286.876,395.103,304.11z M256,355.336c-6.567-9.674-15.979-17.259-27.016-21.574
c6.793-7.986,10.91-18.341,10.91-29.651c0-17.234-9.539-32.266-23.583-40.04c6.92-6.641,16.14-10.507,25.925-10.507h27.526
c9.785,0,19.005,3.866,25.925,10.507c-14.044,7.774-23.584,22.806-23.584,40.04c0,11.31,4.117,21.665,10.911,29.651
C271.977,338.078,262.566,345.662,256,355.336z M169.103,304.11c0-14.155,11.393-25.672,25.397-25.672
c14.002,0,25.394,11.517,25.394,25.672s-11.392,25.671-25.394,25.671C180.497,329.781,169.103,318.266,169.103,304.11z M256,182.221
c14.003,0,25.395,11.517,25.395,25.672S270.002,233.564,256,233.564c-14.003,0-25.396-11.516-25.396-25.671
C230.604,193.737,241.997,182.221,256,182.221z M292.103,304.11c0-14.155,11.393-25.672,25.397-25.672
c14.003,0,25.396,11.517,25.396,25.672s-11.393,25.671-25.396,25.671C303.496,329.781,292.103,318.266,292.103,304.11z
M404.396,207.893c0,14.155-11.393,25.671-25.396,25.671s-25.396-11.516-25.396-25.671s11.393-25.672,25.396-25.672
S404.396,193.737,404.396,207.893z M317.5,86c14.003,0,25.396,11.516,25.396,25.671s-11.393,25.672-25.396,25.672
s-25.396-11.517-25.396-25.672S303.497,86,317.5,86z M303.736,157.343h27.526c9.786,0,19.007,3.867,25.927,10.508
c-14.044,7.774-23.585,22.807-23.585,40.042c0,11.277,4.093,21.605,10.851,29.582c-10.598,4.133-19.865,11.385-26.524,20.975
c-0.144-0.001-0.286-0.011-0.431-0.011s-0.287,0.01-0.432,0.011c-6.659-9.589-15.927-16.842-26.524-20.975
c6.758-7.977,10.851-18.305,10.851-29.582c0-17.235-9.541-32.268-23.586-40.042C284.729,161.21,293.95,157.343,303.736,157.343z
M194.5,86c14.002,0,25.394,11.516,25.394,25.671s-11.392,25.672-25.394,25.672c-14.003,0-25.397-11.517-25.397-25.672
S180.497,86,194.5,86z M180.736,157.343h27.526c9.786,0,19.007,3.867,25.927,10.509c-14.045,7.773-23.585,22.806-23.585,40.041
c0,11.277,4.093,21.605,10.851,29.582c-10.597,4.133-19.866,11.385-26.525,20.975c-0.144-0.001-0.286-0.011-0.43-0.011
c-0.145,0-0.286,0.01-0.43,0.011c-6.659-9.59-15.928-16.842-26.525-20.975c6.758-7.977,10.851-18.305,10.851-29.582
c0-17.235-9.541-32.268-23.585-40.042C161.729,161.21,170.951,157.343,180.736,157.343z M133,182.221
c14.003,0,25.395,11.517,25.395,25.672S147.002,233.564,133,233.564c-14.003,0-25.396-11.516-25.396-25.671
C107.604,193.737,118.997,182.221,133,182.221z M119.236,253.564h27.526c9.786,0,19.006,3.866,25.925,10.506
c-14.044,7.774-23.585,22.807-23.585,40.041c0,11.31,4.117,21.666,10.911,29.651c-11.037,4.316-20.448,11.9-27.015,21.574
c-6.567-9.674-15.978-17.258-27.015-21.574c6.794-7.986,10.911-18.341,10.911-29.651c0-17.234-9.54-32.266-23.584-40.041
C100.231,257.43,109.451,253.564,119.236,253.564z M71.5,278.439c14.003,0,25.395,11.517,25.395,25.672S85.502,329.782,71.5,329.782
c-14.003,0-25.396-11.516-25.396-25.671C46.104,289.955,57.497,278.439,71.5,278.439z M20,408.818h31.035c5.523,0,10-4.477,10-10
c0-5.522-4.477-10-10-10H20v-0.928c0-21.014,17.096-38.109,38.109-38.109H84.89c21.014,0,38.109,17.096,38.109,38.109V426H20
V408.818z M143,387.891c0-21.014,17.096-38.109,38.109-38.109h26.781c21.014,0,38.109,17.096,38.109,38.109V426H143V387.891z
M266,387.891c0-21.014,17.096-38.109,38.108-38.109h26.782c21.014,0,38.109,17.096,38.109,38.109V426H266V387.891z M389,426
v-38.109c0-21.014,17.096-38.109,38.109-38.109h26.781c21.013,0,38.108,17.096,38.108,38.109v0.928h-31.034c-5.522,0-10,4.478-10,10
c0,5.523,4.478,10,10,10h31.034V426H389z"/>
<path d="M80.001,388.82h-0.235c-5.523,0-10,4.478-10,10s4.477,10,10,10h0.235c5.523,0,10-4.478,10-10S85.524,388.82,80.001,388.82z"
/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -277,8 +277,10 @@ class RowAssiJusti(tb.Row):
self.add_cell( self.add_cell(
"entry_date", "entry_date",
"Saisie le", "Saisie le",
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M"), self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M")
data={"order": self.ligne["entry_date"]}, if self.ligne["entry_date"]
else "?",
data={"order": self.ligne["entry_date"] or ""},
raw_content=self.ligne["entry_date"], raw_content=self.ligne["entry_date"],
classes=["small-font"], classes=["small-font"],
column_classes={"entry_date"}, column_classes={"entry_date"},
@ -387,6 +389,13 @@ class RowAssiJusti(tb.Row):
html.append(f'<a title="Détails" href="{url}"></a>') html.append(f'<a title="Détails" href="{url}"></a>')
# Modifier # Modifier
if self.ligne["type"] == "justificatif":
url = url_for(
"assiduites.edit_justificatif_etud",
justif_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
else:
url = url_for( url = url_for(
"assiduites.tableau_assiduite_actions", "assiduites.tableau_assiduite_actions",
type=self.ligne["type"], type=self.ligne["type"],

View File

@ -0,0 +1,68 @@
{# Explication des états des justificatifs #}
<div class="explication-etats-justifs">
<div class="explication-titre">États des justificatifs</div>
<div class="explication-etats">
<div class="valide">Justificatif valide</div>
<div class="legend">ayant été considéré comme valide, justifie les absences
ou retards de la période
</div>
<div class="attente">Justificatif soumis</div>
<div class="legend">en attente de validation. Les absences ne sont pas
encore considérées comme justifiées.
</div>
<div class="modifie">Justificatif modifié</div>
<div class="legend">une information a été ajoutée ou modifiée. Doit être validé avant
d'être pris en en compte.
</div>
<div class="invalide">Justificatif invalide</div>
<div class="legend">proposé mais considéré comme non valide.
Les absences ne sont pas justifiées.
</div>
</div>
</div>
<style>
.explication-etats-justifs {
margin-top: 32px;
margin-left: 12px;
padding: 8px;
}
.explication-etats-justifs .explication-titre {
font-size: 110%;
font-weight: bold;
margin-bottom: 8px;
}
.explication-etats-justifs .explication-etats {
display: grid;
grid-template-columns: auto 1fr;
font-size: 80%;
}
.explication-etats > div {
margin-bottom: 8px;
margin-right: 8px;
padding: 6px;
}
.explication-etats-justifs div.legend {
font-style: italic;
}
.valide {
background-color: var(--color-justi);
}
.attente {
background-color: var(--color-justi-attente);
}
.modifie {
background-color: var(--color-justi-modifie);
}
.invalide {
background-color: var(--color-justi-invalide);
}
</style>

View File

@ -88,11 +88,11 @@ div.submit > input {
{{ form.modimpl }} {{ form.modimpl }}
{{ render_field_errors(form, 'modimpl') }} {{ render_field_errors(form, 'modimpl') }}
</div> </div>
{# Raison #} {# Description #}
<div> <div>
<div>{{ form.assi_raison.label }}</div> <div>{{ form.description.label }}</div>
{{ form.assi_raison() }} {{ form.description() }}
{{ render_field_errors(form, 'assi_raison') }} {{ render_field_errors(form, 'description') }}
</div> </div>
{# Date dépot #} {# Date dépot #}
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }} {{ form.entry_date.label }}&nbsp;: {{ form.entry_date }}

View File

@ -52,8 +52,8 @@
<div class="assi-row"> <div class="assi-row">
<div class="assi-label"> <div class="assi-label">
<legend for="assi_raison">Raison</legend> <legend for="raison">Raison</legend>
<textarea name="assi_raison" id="assi_raison" cols="75" rows="4" maxlength="500"></textarea> <textarea name="raison" id="raison" cols="75" rows="4" maxlength="500"></textarea>
</div> </div>
</div> </div>
@ -135,7 +135,7 @@
const { deb, fin } = getDates() const { deb, fin } = getDates()
const etat = field.querySelector('#assi_etat').value; const etat = field.querySelector('#assi_etat').value;
const raison = field.querySelector('#assi_raison').value; const raison = field.querySelector('#raison').value;
const module = field.querySelector("#ajout_assiduite_module_impl").value; const module = field.querySelector("#ajout_assiduite_module_impl").value;
return { return {
@ -168,7 +168,7 @@
field.querySelector('#assi_date_debut').value = ""; field.querySelector('#assi_date_debut').value = "";
field.querySelector('#assi_date_fin').value = ""; field.querySelector('#assi_date_fin').value = "";
field.querySelector('#assi_etat').value = "attente"; field.querySelector('#assi_etat').value = "attente";
field.querySelector('#assi_raison').value = ""; field.querySelector('#raison').value = "";
} }

View File

@ -1,3 +1,5 @@
{# Formulaire ajout ou modification de justificatif
Si justif, edit #}
{% extends "sco_page.j2" %} {% extends "sco_page.j2" %}
{% import 'wtf.j2' as wtf %} {% import 'wtf.j2' as wtf %}
@ -16,6 +18,16 @@ form#ajout-justificatif-etud {
form#ajout-justificatif-etud > div { form#ajout-justificatif-etud > div {
margin-bottom: 16px; margin-bottom: 16px;
} }
div.fichiers {
margin-top: 16px;
margin-bottom: 32px;
}
div.fichiers ul {
list-style-type: none;
}
span.suppr_fichier_just {
margin-right: 8px;
}
div.submit { div.submit {
margin-top: 12px; margin-top: 12px;
} }
@ -61,16 +73,34 @@ div.submit > input {
</div> </div>
{# Raison #} {# Raison #}
<div> <div>
<div>{{ form.assi_raison.label }}</div> <div>{{ form.raison.label }}</div>
{{ form.assi_raison() }} {{ form.raison() }}
{{ render_field_errors(form, 'assi_raison') }} {{ render_field_errors(form, 'raison') }}
</div> </div>
{# Fichier(s) justificatif(s) #} <div class="fichiers">
{# Liste des fichiers existants #}
{% if justif and nb_files > 0 %}
<div><b>{{nb_files}} fichiers justificatifs déposés
{% if filenames|length < nb_files %}
, dont {{filenames|length}} vous sont accessibles
{% endif %}
</b>
</div>
<ul>
{% for filename in filenames %}
<li><span data-justif_id="{{justif.id}}" class="suppr_fichier_just"
>{{scu.icontag("delete_img", alt="supprimer", title="Supprimer")|safe}}</span>
{{filename}}</li>
{% endfor %}
</ul>
{% endif %}
{# Ajout fichier(s) justificatif(s) #}
<div> <div>
<div>{{ form.fichiers.label }}</div> <div>{{ form.fichiers.label }}</div>
{{ form.fichiers() }} {{ form.fichiers() }}
{{ render_field_errors(form, 'fichiers') }} {{ render_field_errors(form, 'fichiers') }}
</div> </div>
</div>
{# Date dépot #} {# Date dépot #}
{{ 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>
@ -83,12 +113,15 @@ div.submit > input {
</fieldset> </fieldset>
</form> </form>
</section> </section>
{% if tableau %}
<section class="assi-liste"> <section class="assi-liste">
{{tableau | safe }} {{tableau | safe }}
</section> </section>
{% endif %}
</div> </div>
{% include "assiduites/explication_etats_justifs.j2" %}
{% endblock app_content %} {% endblock app_content %}
{% block scripts %} {% block scripts %}
@ -108,4 +141,54 @@ $('.timepicker').timepicker({
scrollbar: false scrollbar: false
}); });
</script> </script>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Suppression d'un fichier justificatif
function delete_file(justif_id, fileName, liElement) {
// Construct the URL
var url = "{{url_for('apiweb.justif_remove', justif_id=-1, scodoc_dept=g.scodoc_dept)}}".replace('-1', justif_id);
payload = {
"remove": "list",
"filenames" : [ fileName ],
}
// Send API request
fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
})
.then(response => {
if (response.ok) {
// Hide the <li> element on successful deletion
liElement.style.display = 'none';
sco_message("fichier supprimé");
} else {
// Handle non-successful responses here
console.error('Deletion failed:', response.statusText);
sco_error_message("erreur lors de la suppression du fichier");
}
})
.catch(error => {
console.error('Error:', error);
sco_error_message("erreur lors de la suppression du fichier (2)");
});
}
// Add event listeners to all elements with class 'suppr_fichier_just'
var deleteButtons = document.querySelectorAll('.suppr_fichier_just');
deleteButtons.forEach(function(button) {
button.addEventListener('click', function() {
// Get the text content of the next sibling node
var justif_id = this.dataset.justif_id;
var fileName = this.nextSibling.nodeValue.trim();
var liElement = this.parentNode; // Get the parent <li> element
delete_file(justif_id, fileName, liElement);
});
});
});
</script>
{% endblock scripts %} {% endblock scripts %}

View File

@ -169,7 +169,7 @@
right: 0; right: 0;
background-color: var(--color-justi-invalide) !important; background-color: var(--color-justi-invalide) !important;
} }
.color.attente::before { .color.attente::before, .color.modifie::before {
content: ""; content: "";
position: absolute; position: absolute;
width: 25%; width: 25%;
@ -475,6 +475,10 @@
let est_just = "" let est_just = ""
if (dayJustificatifs.some((j) => j.etat.toLowerCase() === "valide")) { if (dayJustificatifs.some((j) => j.etat.toLowerCase() === "valide")) {
est_just = "est_just"; est_just = "est_just";
} else if (dayJustificatifs.some((j) => j.etat.toLowerCase() === "attente")) {
est_just = "attente";
} else if (dayJustificatifs.some((j) => j.etat.toLowerCase() === "modifie")) {
est_just = "modifie";
} else if (dayJustificatifs.some((j) => j.etat.toLowerCase() !== "valide")) { } else if (dayJustificatifs.some((j) => j.etat.toLowerCase() !== "valide")) {
est_just = "invalide"; est_just = "invalide";
} }
@ -535,6 +539,8 @@
est_just = ["est_just"]; est_just = ["est_just"];
} else if (justificatifsMatin.some((j) => j.etat.toLowerCase() === "attente")) { } else if (justificatifsMatin.some((j) => j.etat.toLowerCase() === "attente")) {
est_just = ["attente"]; est_just = ["attente"];
} else if (justificatifsMatin.some((j) => j.etat.toLowerCase() === "modifie")) {
est_just = ["modifie"];
} }
else if (justificatifsMatin.some((j) => j.etat.toLowerCase() !== "valide")) { else if (justificatifsMatin.some((j) => j.etat.toLowerCase() !== "valide")) {
est_just = ["invalide"]; est_just = ["invalide"];
@ -579,8 +585,9 @@
est_just = ["est_just"]; est_just = ["est_just"];
} else if (justificatifsAprem.some((j) => j.etat.toLowerCase() === "attente")) { } else if (justificatifsAprem.some((j) => j.etat.toLowerCase() === "attente")) {
est_just = ["attente"]; est_just = ["attente"];
} } else if (justificatifsAprem.some((j) => j.etat.toLowerCase() === "modifie")) {
else if (justificatifsAprem.some((j) => j.etat.toLowerCase() !== "valide")) { est_just = ["modifie"];
} else if (justificatifsAprem.some((j) => j.etat.toLowerCase() !== "valide")) {
est_just = ["invalide"]; est_just = ["invalide"];
} }

View File

@ -81,7 +81,10 @@ affectent notamment les comptages d'absences de tous les bulletins des
</div> </div>
<div class="row"> <div class="row">
<h1>Emplois du temps</h1> <h1>Emplois du temps</h1>
<div class="help">ScoDoc peut récupérer les emplois du temps de chaque session.</div> <div class="help">ScoDoc peut récupérer les emplois du temps de chaque session.
Voir <a href="https://scodoc.org/EmploisDuTemps" class="stdlink"
target="_blank">la documentation</a>.
</div>
<div class="col-md-8"> <div class="col-md-8">
<div class="config-edt"> <div class="config-edt">
{{ wtf.form_field(form.edt_ics_path) }} {{ wtf.form_field(form.edt_ics_path) }}

View File

@ -4,4 +4,7 @@
<h2>Liste de l'assiduité et des justificatifs de {{sco.etud.html_link_fiche()|safe}}</h2> <h2>Liste de l'assiduité et des justificatifs de {{sco.etud.html_link_fiche()|safe}}</h2>
{{tableau | safe }} {{tableau | safe }}
</div> </div>
{% include "assiduites/explication_etats_justifs.j2" %}
{% endblock app_content %} {% endblock app_content %}

View File

@ -16,6 +16,12 @@
{{ form_groups_choice|safe }} {{ form_groups_choice|safe }}
<form id="show_modules_titles_form" method="GET">
<input type="checkbox" name="show_modules_titles" {{
'checked' if show_modules_titles else ''}}
onchange="this.form.submit()"/> noms complets des modules</input>
</form>
<div> <div>
<span id="menu-navi"> <span id="menu-navi">
<button type="button" class="btn btn-default btn-sm move-today" <button type="button" class="btn btn-default btn-sm move-today"
@ -38,9 +44,16 @@
</li> </li>
<li>Si vous filtrez par groupe, les évènements dont le groupe n'est pas reconnu seront affichés. <li>Si vous filtrez par groupe, les évènements dont le groupe n'est pas reconnu seront affichés.
</li> </li>
{% if formsemestre.can_be_edited_by(current_user) %}
<li><a class="stdlink" href="{{
url_for('notes.formsemestre_edt_help_config',
scodoc_dept=g.scodoc_dept, formsemestre_id= formsemestre.id)
}}">Aide à la configuration de l'emploi du temps</a>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
{% endblock app_content %} {% endblock app_content %}
{% block scripts %} {% block scripts %}
@ -107,7 +120,7 @@ document.addEventListener('DOMContentLoaded', function() {
const calendar = new Calendar(container, options); const calendar = new Calendar(container, options);
fetch(`${SCO_URL}/../api/formsemestre/{{formsemestre.id}}/edt?{{groups_query_args|safe}}`) fetch(`${SCO_URL}/../api/formsemestre/{{formsemestre.id}}/edt?{{groups_query_args|safe}}&show_modules_titles={{show_modules_titles}}`)
.then(r=>{return r.json()}) .then(r=>{return r.json()})
.then(events=>{ .then(events=>{
if (typeof events == 'string') { if (typeof events == 'string') {

View File

@ -0,0 +1,87 @@
{% extends "sco_page.j2" %}
{% block app_content %}
<style>
table#edt2group {
border-collapse: collapse;
margin: 25px 0;
font-size: 0.9em;
font-family: sans-serif;
min-width: 400px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
table#edt2group thead tr th {
background-color: #009879;
color: #ffffff;
text-align: left;
}
table#edt2group thead tr th,
table#edt2group tbody tr td {
padding: 4px 8px;
}
table#edt2group tbody tr {
border-bottom: 1px solid #dddddd !important;
}
table#edt2group tbody tr:nth-of-type(even) {
background-color: #f3f3f3 !important;
}
table#edt2group tbody tr:last-of-type {
border-bottom: 2px solid #009879 !important;
}
table#edt2group tbody tr.active-row {
font-weight: bold !important;
color: #009879 !important;
}
</style>
<div class="tab-content">
<h2>Aide à la configuration de l'emploi du temps</h2>
<ul>
<li>Nombre d'évènements dans le calendrier ics de ce semestre: {{events_sco|length}}</li>
</ul>
<h3>Identifiants de groupes trouvés dans ce calendrier</h3>
<div class="help">
si vous voyez ici de nombreuses lignes, il est possible que l'expression régulière
d'extraction soit incorrecte (voir configuration globale) ou bien que votre logiciel d'emploi du temps génère de nombreux évènements non associés à un groupe donné.
</div>
<div>Voici ce qui a été extrait de l'emploi du temps par l'expression régulière configurée:
</div>
<ul>
{% for gr in edt_groups_ids %}
<li>{{ gr }}</li>
{% endfor %}
</ul>
<h3>Table de correspondance entre groupes EDT et groupes ScoDoc</h3>
<div class="help">
Si votre logiciel d'emploi du temps utilise des identifiants de groupes différents de ceux de ScoDoc, il faut l'indiquer
{% if formsemestre.can_change_groups(current_user) %}
<a class="stdlink" href="{{ url_for( 'scolar.partition_editor',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
edit_partition=1 ) }}
">dans l'éditeur de partitions</a>
{% else %}
dans l'éditeur de partitions (vous n'avez pas l'autorisation de le faire vous même).
{% endif %}
</div>
<table id="edt2group">
<thead>
<tr><th>Groupe EDT</th><th>Groupe ScoDoc</th><th>group_id</th></tr>
</thead>
<tbody>
{% for edt_gr in edt2group %}
<tr><td>{{edt_gr or "*"}}</td>
<td>{{edt2group[edt_gr].group_name or "tous"}}</td>
<td>{{edt2group[edt_gr].id}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock app_content %}

View File

@ -400,7 +400,7 @@ def _get_dates_from_assi_form(
dt_entry_date = ( dt_entry_date = (
datetime.datetime.strptime(form.entry_date.data, "%d/%m/%Y") datetime.datetime.strptime(form.entry_date.data, "%d/%m/%Y")
if form.entry_date.data if form.entry_date.data
else None else datetime.datetime.now() # local tz
) )
except ValueError: except ValueError:
dt_entry_date = None dt_entry_date = None
@ -464,7 +464,7 @@ def _record_assiduite_etud(
dt_debut_tz_server, dt_debut_tz_server,
dt_fin_tz_server, dt_fin_tz_server,
scu.EtatAssiduite.get(form.assi_etat.data), scu.EtatAssiduite.get(form.assi_etat.data),
description=form.assi_raison.data, description=form.description.data,
entry_date=dt_entry_date_tz_server, entry_date=dt_entry_date_tz_server,
external_data=external_data, external_data=external_data,
moduleimpl=moduleimpl, moduleimpl=moduleimpl,
@ -596,6 +596,64 @@ def bilan_etud():
).build() ).build()
@bp.route("/edit_justificatif_etud/<int:justif_id>", methods=["GET", "POST"])
@scodoc
@permission_required(Permission.AbsChange)
def edit_justificatif_etud(justif_id: int):
"""
Edition d'un justificatif
Args:
justif_id (int): l'identifiant du justificatif
Returns:
str: l'html généré
"""
justif = Justificatif.get_justificatif(justif_id)
form = AjoutJustificatifEtudForm(obj=justif)
# Set the default value for the etat field
if request.method == "GET":
form.date_debut.data = justif.date_debut.strftime("%d/%m/%Y")
form.date_fin.data = justif.date_fin.strftime("%d/%m/%Y")
if form.date_fin.data == form.date_debut.data:
# un seul jour: pas de date de fin, indique les heures
form.date_fin.data = ""
form.heure_debut.data = justif.date_debut.strftime("%H:%M")
form.heure_fin.data = justif.date_fin.strftime("%H:%M")
form.entry_date.data = (
justif.entry_date.strftime("%d/%m/%Y") if justif.entry_date else ""
)
form.etat.data = str(justif.etat)
redirect_url = url_for(
"assiduites.liste_assiduites_etud",
scodoc_dept=g.scodoc_dept,
etudid=justif.etudiant.id,
)
if form.validate_on_submit():
if _record_justificatif_etud(justif.etudiant, form, justif):
return redirect(redirect_url)
# Fichiers
filenames, nb_files = justif.get_fichiers()
return render_template(
"assiduites/pages/ajout_justificatif_etud.j2",
assi_limit_annee=sco_preferences.get_preference(
"assi_limit_annee",
dept_id=g.scodoc_dept_id,
),
etud=justif.etudiant,
filenames=filenames,
form=form,
justif=justif,
nb_files=nb_files,
page_title="Modification justificatif",
redirect_url=redirect_url,
sco=ScoData(justif.etudiant),
scu=scu,
)
@bp.route( @bp.route(
"/ajout_justificatif_etud", methods=["GET", "POST"] "/ajout_justificatif_etud", methods=["GET", "POST"]
) # was AjoutJustificatifEtud ) # was AjoutJustificatifEtud
@ -603,7 +661,7 @@ def bilan_etud():
@permission_required(Permission.AbsChange) @permission_required(Permission.AbsChange)
def ajout_justificatif_etud(): def ajout_justificatif_etud():
""" """
ajout_justificatif_etud : Affichage et création/modification des justificatifs de l'étudiant ajout_justificatif_etud : Affichage et création des justificatifs de l'étudiant
Args: Args:
etudid (int): l'identifiant de l'étudiant etudid (int): l'identifiant de l'étudiant
@ -654,8 +712,7 @@ def ajout_justificatif_etud():
def _record_justificatif_etud( def _record_justificatif_etud(
etud: Identite, etud: Identite, form: AjoutJustificatifEtudForm, justif: Justificatif | None = None
form: AjoutJustificatifEtudForm,
) -> bool: ) -> bool:
"""Enregistre les données du formulaire de saisie justificatif (et ses fichiers). """Enregistre les données du formulaire de saisie justificatif (et ses fichiers).
Returns ok if successfully recorded, else put error info in the form. Returns ok if successfully recorded, else put error info in the form.
@ -663,6 +720,7 @@ def _record_justificatif_etud(
form.assi_etat.data : 'absent' form.assi_etat.data : 'absent'
form.date_debut.data : '05/12/2023' form.date_debut.data : '05/12/2023'
form.heure_debut.data : '09:06' (heure locale du serveur) form.heure_debut.data : '09:06' (heure locale du serveur)
Si justif, modifie le justif existant, sinon en crée un nouveau
""" """
( (
ok, ok,
@ -672,30 +730,53 @@ def _record_justificatif_etud(
) = _get_dates_from_assi_form(form) ) = _get_dates_from_assi_form(form)
if not ok: if not ok:
log("_record_justificatif_etud: dates invalides")
form.set_error("Erreur: dates invalides")
return False return False
etat = scu.EtatJustificatif.get(form.etat.data) if not form.etat.data:
log("_record_justificatif_etud: etat invalide")
form.set_error("Erreur: état invalide")
return False
etat = int(form.etat.data)
if not scu.EtatJustificatif.is_valid_etat(etat):
log(f"_record_justificatif_etud: etat invalide ({etat})")
form.set_error("Erreur: état invalide")
return False
try: try:
just = Justificatif.create_justificatif( message = ""
if justif:
form.date_debut.data = dt_debut_tz_server
form.date_fin.data = dt_fin_tz_server
form.entry_date.data = dt_entry_date_tz_server
if justif.edit_from_form(form):
message = "Justificatif modifié"
else:
message = "Pas de modification"
else:
justif = Justificatif.create_justificatif(
etud, etud,
dt_debut_tz_server, dt_debut_tz_server,
dt_fin_tz_server, dt_fin_tz_server,
etat=etat, etat=etat,
raison=form.assi_raison.data, raison=form.raison.data,
entry_date=dt_entry_date_tz_server, entry_date=dt_entry_date_tz_server,
user_id=current_user.id, user_id=current_user.id,
) )
db.session.add(just) message = "Justificatif créé"
if not _upload_justificatif_files(just, form): db.session.add(justif)
if not _upload_justificatif_files(justif, form):
flash("Erreur enregistrement fichiers") flash("Erreur enregistrement fichiers")
log("problem in _upload_justificatif_files, rolling back") log("problem in _upload_justificatif_files, rolling back")
db.session.rollback() db.session.rollback()
return False return False
db.session.commit() db.session.commit()
compute_assiduites_justified(etud.id, [just]) compute_assiduites_justified(etud.id, [justif])
scass.simple_invalidate_cache(just.to_dict(), etud.id) scass.simple_invalidate_cache(justif.to_dict(), etud.id)
flash("Justificatif enregistré") flash(message)
return True return True
except ScoValueError as exc: except ScoValueError as exc:
log(f"_record_justificatif_etud: erreur {exc.args[0]}")
db.session.rollback() db.session.rollback()
form.set_error(f"Erreur: {exc.args[0]}") form.set_error(f"Erreur: {exc.args[0]}")
return False return False
@ -1474,10 +1555,10 @@ def _action_modifier_assiduite(assi: Assiduite):
def _action_modifier_justificatif(justi: Justificatif): def _action_modifier_justificatif(justi: Justificatif):
"Modifie le justificatif avec les valeurs dans le form"
form = request.form form = request.form
# Gestion des Dates # Gestion des Dates
date_debut: datetime = scu.is_iso_formated(form["date_debut"], True) date_debut: datetime = scu.is_iso_formated(form["date_debut"], True)
date_fin: datetime = scu.is_iso_formated(form["date_fin"], True) date_fin: datetime = scu.is_iso_formated(form["date_fin"], True)
if date_debut is None or date_fin is None or date_fin < date_debut: if date_debut is None or date_fin is None or date_fin < date_debut:
@ -1556,40 +1637,30 @@ def _preparer_objet(
_preparer_objet("justificatif", justi, sans_gros_objet=True) _preparer_objet("justificatif", justi, sans_gros_objet=True)
) )
else: else: # objet == "justificatif"
justif: Justificatif = objet
objet_prepare["etat"] = ( objet_prepare["etat"] = (
scu.EtatJustificatif(objet.etat).version_lisible().capitalize() scu.EtatJustificatif(justif.etat).version_lisible().capitalize()
) )
objet_prepare["real_etat"] = scu.EtatJustificatif(objet.etat).name.lower() objet_prepare["real_etat"] = scu.EtatJustificatif(justif.etat).name.lower()
objet_prepare["raison"] = "" if objet.raison is None else objet.raison objet_prepare["raison"] = "" if justif.raison is None else justif.raison
objet_prepare["raison"] = objet_prepare["raison"].strip() objet_prepare["raison"] = objet_prepare["raison"].strip()
objet_prepare["justification"] = {"assiduites": [], "fichiers": {}} objet_prepare["justification"] = {"assiduites": [], "fichiers": {}}
if not sans_gros_objet: if not sans_gros_objet:
assiduites: list[int] = scass.justifies(objet) assiduites: list[int] = scass.justifies(justif)
for assi_id in assiduites: for assi_id in assiduites:
assi: Assiduite = Assiduite.query.get(assi_id) assi: Assiduite = Assiduite.query.get(assi_id)
objet_prepare["justification"]["assiduites"].append( objet_prepare["justification"]["assiduites"].append(
_preparer_objet("assiduite", assi, sans_gros_objet=True) _preparer_objet("assiduite", assi, sans_gros_objet=True)
) )
# Récupération de l'archive avec l'archiver # fichiers justificatifs archivés:
archive_name: str = objet.fichier filenames, nb_files = justif.get_fichiers()
filenames: list[str] = []
archiver: JustificatifArchiver = JustificatifArchiver()
if archive_name is not None:
filenames = archiver.list_justificatifs(archive_name, objet.etudiant)
objet_prepare["justification"]["fichiers"] = { objet_prepare["justification"]["fichiers"] = {
"total": len(filenames), "total": nb_files,
"filenames": [], "filenames": filenames,
} }
for filename in filenames:
if int(filename[1]) == current_user.id or current_user.has_permission(
Permission.AbsJustifView
):
objet_prepare["justification"]["fichiers"]["filenames"].append(
filename[0]
)
objet_prepare["date_fin"] = objet.date_fin.strftime("%d/%m/%y à %H:%M") objet_prepare["date_fin"] = objet.date_fin.strftime("%d/%m/%y à %H:%M")
objet_prepare["real_date_fin"] = objet.date_fin.isoformat() objet_prepare["real_date_fin"] = objet.date_fin.isoformat()
@ -1600,7 +1671,7 @@ def _preparer_objet(
objet_prepare["etud_nom"] = objet.etudiant.nomprenom objet_prepare["etud_nom"] = objet.etudiant.nomprenom
if objet.user_id != None: if objet.user_id is not None:
user: User = User.query.get(objet.user_id) user: User = User.query.get(objet.user_id)
objet_prepare["saisie_par"] = user.get_nomprenom() objet_prepare["saisie_par"] = user.get_nomprenom()
else: else:

View File

@ -103,7 +103,7 @@ from app.scodoc import html_sco_header
from app.pe import pe_view from app.pe import pe_view
from app.scodoc import sco_apogee_compare from app.scodoc import sco_apogee_compare
from app.scodoc import sco_archives from app.scodoc import sco_archives
from app.scodoc import sco_archive_formsemestre from app.scodoc import sco_archives_formsemestre
from app.scodoc import sco_assiduites from app.scodoc import sco_assiduites
from app.scodoc import sco_bulletins from app.scodoc import sco_bulletins
from app.scodoc import sco_bulletins_pdf from app.scodoc import sco_bulletins_pdf
@ -2973,24 +2973,24 @@ sco_publish(
) )
sco_publish( sco_publish(
"/formsemestre_archive", "/formsemestre_archive",
sco_archive_formsemestre.formsemestre_archive, sco_archives_formsemestre.formsemestre_archive,
Permission.ScoView, Permission.ScoView,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/formsemestre_delete_archive", "/formsemestre_delete_archive",
sco_archive_formsemestre.formsemestre_delete_archive, sco_archives_formsemestre.formsemestre_delete_archive,
Permission.ScoView, Permission.ScoView,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/formsemestre_list_archives", "/formsemestre_list_archives",
sco_archive_formsemestre.formsemestre_list_archives, sco_archives_formsemestre.formsemestre_list_archives,
Permission.ScoView, Permission.ScoView,
) )
sco_publish( sco_publish(
"/formsemestre_get_archived_file", "/formsemestre_get_archived_file",
sco_archive_formsemestre.formsemestre_get_archived_file, sco_archives_formsemestre.formsemestre_get_archived_file,
Permission.ScoView, Permission.ScoView,
) )
sco_publish("/view_apo_csv", sco_etape_apogee_view.view_apo_csv, Permission.EditApogee) sco_publish("/view_apo_csv", sco_etape_apogee_view.view_apo_csv, Permission.EditApogee)

View File

@ -39,9 +39,14 @@ from app.decorators import (
) )
from app.forms.formsemestre import change_formation, edit_modimpls_codes_apo from app.forms.formsemestre import change_formation, edit_modimpls_codes_apo
from app.models import Formation, FormSemestre, ScoDocSiteConfig from app.models import Formation, FormSemestre, ScoDocSiteConfig
from app.scodoc import sco_formations, sco_formation_versions from app.scodoc import (
from app.scodoc import sco_groups_view sco_edt_cal,
sco_formations,
sco_formation_versions,
sco_groups_view,
)
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
from app.views import notes_bp as bp from app.views import notes_bp as bp
from app.views import ScoData from app.views import ScoData
@ -158,6 +163,7 @@ def formsemestre_edit_modimpls_codes(formsemestre_id: int):
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
def formsemestre_edt(formsemestre_id: int): def formsemestre_edt(formsemestre_id: int):
"""Expérimental: affiche emploi du temps du semestre""" """Expérimental: affiche emploi du temps du semestre"""
show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
formsemestre = FormSemestre.get_formsemestre(formsemestre_id) formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
cfg = ScoDocSiteConfig.query.filter_by(name="assi_morning_time").first() cfg = ScoDocSiteConfig.query.filter_by(name="assi_morning_time").first()
hour_start = cfg.value.split(":")[0].lstrip(" 0") if cfg else "7" hour_start = cfg.value.split(":")[0].lstrip(" 0") if cfg else "7"
@ -182,4 +188,25 @@ def formsemestre_edt(formsemestre_id: int):
), ),
groups_query_args=groups_infos.groups_query_args, groups_query_args=groups_infos.groups_query_args,
sco=ScoData(formsemestre=formsemestre), sco=ScoData(formsemestre=formsemestre),
show_modules_titles=show_modules_titles,
)
@bp.route("/formsemestre/edt_help_config/<int:formsemestre_id>")
@scodoc
@permission_required(Permission.ScoView)
def formsemestre_edt_help_config(formsemestre_id: int):
"""Page d'aide à la configuration de l'extraction emplois du temps
Affiche les identifiants extraits de l'ics et ceux de ScoDoc.
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
edt2group = sco_edt_cal.formsemestre_retreive_groups_from_edt_id(formsemestre)
events_sco, edt_groups_ids = sco_edt_cal.load_and_convert_ics(formsemestre)
return render_template(
"formsemestre/edt_help_config.j2",
formsemestre=formsemestre,
edt2group=edt2group,
edt_groups_ids=edt_groups_ids,
events_sco=events_sco,
sco=ScoData(formsemestre=formsemestre),
) )

View File

@ -10,7 +10,7 @@ from tests.unit import yaml_setup, call_view
import app import app
from app.models import Formation, FormSemestre from app.models import Formation, FormSemestre
from app.scodoc import ( from app.scodoc import (
sco_archive_formsemestre, sco_archives_formsemestre,
sco_cost_formation, sco_cost_formation,
sco_debouche, sco_debouche,
sco_edit_ue, sco_edit_ue,
@ -182,8 +182,8 @@ def test_formsemestre_misc_views(test_client):
assert isinstance(ans, Response) assert isinstance(ans, Response)
assert ans.status == "200 OK" assert ans.status == "200 OK"
assert ans.mimetype == scu.JSON_MIMETYPE assert ans.mimetype == scu.JSON_MIMETYPE
ans = sco_archive_formsemestre.formsemestre_archive(formsemestre.id) ans = sco_archives_formsemestre.formsemestre_archive(formsemestre.id)
ans = sco_archive_formsemestre.formsemestre_list_archives(formsemestre.id) ans = sco_archives_formsemestre.formsemestre_list_archives(formsemestre.id)
# ----- MENU STATISTIQUES # ----- MENU STATISTIQUES
ans = sco_report.formsemestre_report_counts(formsemestre.id) ans = sco_report.formsemestre_report_counts(formsemestre.id)