1
0
Fork 0

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.
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)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
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_login import login_required, current_user
from flask_sqlalchemy.query import Query
from werkzeug.exceptions import NotFound
import app.scodoc.sco_assiduites as scass
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
@permission_required(Permission.ScoView)
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
dept: Departement = Departement.query.get_or_404(dept_id)
@ -373,7 +374,7 @@ def _create_one(
date_debut=deb,
date_fin=fin,
etat=etat,
etud=etud,
etudiant=etud,
raison=raison,
user_id=current_user.id,
external_data=external_data,
@ -419,9 +420,7 @@ def justif_edit(justif_id: int):
"""
# Récupération du justificatif à modifier
justificatif_unique: Query = Justificatif.query.filter_by(
id=justif_id
).first_or_404()
justificatif_unique = Justificatif.get_justificatif(justif_id)
errors: list[str] = []
data = request.get_json(force=True)
@ -497,7 +496,7 @@ def justif_edit(justif_id: int):
retour = {
"couverture": {
"avant": avant_ids,
"après": compute_assiduites_justified(
"apres": compute_assiduites_justified(
justificatif_unique.etudid,
[justificatif_unique],
True,
@ -561,12 +560,10 @@ def _delete_one(justif_id: int) -> tuple[int, str]:
message : OK si réussi, message d'erreur sinon
"""
# Récupération du justificatif à supprimer
justificatif_unique: Justificatif = Justificatif.query.filter_by(
id=justif_id
).first()
if justificatif_unique is None:
try:
justificatif_unique = Justificatif.get_justificatif(justif_id)
except NotFound:
return (404, "Justificatif non existant")
# Récupération de l'archive du justificatif
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")
# On récupère le justificatif auquel on va importer le fichier
query: Query = Justificatif.query.filter_by(id=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()
justificatif_unique = Justificatif.get_justificatif(justif_id)
# Récupération de l'archive si elle existe
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)
"""
# On récupère le justificatif concerné
query: Query = Justificatif.query.filter_by(id=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()
justificatif_unique = Justificatif.get_justificatif(justif_id)
# Vérification des permissions
if not (
@ -694,6 +685,7 @@ def justif_export(justif_id: int | None = None, filename: str | None = None):
@as_json
@permission_required(Permission.AbsChange)
def justif_remove(justif_id: int = None):
# XXX TODO pas de test unitaire
"""
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)
# On récupère le justificatif concerné
query: Query = Justificatif.query.filter_by(id=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()
justificatif_unique = Justificatif.get_justificatif(justif_id)
# On récupère l'archive
archive_name: str = justificatif_unique.fichier
@ -775,10 +764,7 @@ def justif_list(justif_id: int = None):
"""
# Récupération du justificatif concerné
query: Query = Justificatif.query.filter_by(id=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()
justificatif_unique = Justificatif.get_justificatif(justif_id)
# Récupération de l'archive avec l'archiver
archive_name: str = justificatif_unique.fichier
@ -820,10 +806,7 @@ def justif_justifies(justif_id: int = None):
"""
# On récupère le justificatif concerné
query: Query = Justificatif.query.filter_by(id=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()
justificatif_unique = Justificatif.get_justificatif(justif_id)
# On récupère la liste des assiduités justifiées par le justificatif
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):
"""
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
etat: str = requested.args.get("etat")
@ -871,7 +855,7 @@ def _filter_manager(requested, justificatifs_query: Query):
formsemestre: FormSemestre = None
try:
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, Justificatif, formsemestre
)
@ -906,4 +890,10 @@ def _filter_manager(requested, justificatifs_query: Query):
except ValueError:
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

View File

@ -40,6 +40,7 @@ from wtforms import (
validators,
)
from wtforms.validators import DataRequired
from app.scodoc import sco_utils as scu
class AjoutAssiOrJustForm(FlaskForm):
@ -98,15 +99,7 @@ class AjoutAssiOrJustForm(FlaskForm):
"id": "assi_date_fin",
},
)
assi_raison = TextAreaField(
"Raison",
render_kw={
"id": "assi_raison",
"cols": 75,
"rows": 4,
"maxlength": 500,
},
)
entry_date = StringField(
"Date de dépot ou saisie",
validators=[validators.Length(max=10)],
@ -122,7 +115,15 @@ class AjoutAssiOrJustForm(FlaskForm):
class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
"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(
"Signaler:",
choices=[("absent", "absence"), ("retard", "retard"), ("present", "présence")],
@ -139,16 +140,24 @@ class AjoutAssiduiteEtudForm(AjoutAssiOrJustForm):
class AjoutJustificatifEtudForm(AjoutAssiOrJustForm):
"Formulaire de saisie d'un justificatif pour un étudiant"
raison = TextAreaField(
"Raison",
render_kw={
"id": "raison",
"cols": 75,
"rows": 4,
"maxlength": 500,
},
)
etat = SelectField(
"État du justificatif",
choices=[
("", "Choisir..."), # Placeholder
("attente", "En attente de validation"),
("non_valide", "Non valide"),
("modifie", "Modifié"),
("valide", "Valide"),
(scu.EtatJustificatif.ATTENTE.value, "En attente de validation"),
(scu.EtatJustificatif.NON_VALIDE.value, "Non valide"),
(scu.EtatJustificatif.MODIFIE.value, "Modifié"),
(scu.EtatJustificatif.VALIDE.value, "Valide"),
],
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()
filenames = archiver.list_justificatifs(archive_name, self.etudiant)
accessible_filenames = []
#
for filename in filenames:
if int(filename[1]) == current_user.id or current_user.has_permission(
Permission.AbsJustifView

View File

@ -49,7 +49,6 @@
"""
import datetime
import glob
import json
import mimetypes
import os
import re
@ -58,29 +57,17 @@ import time
import chardet
import flask
from flask import flash, g, request, url_for
from flask import g
import app.scodoc.sco_utils as scu
from config import Config
from app import log, ScoDocJSONEncoder
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 import log
from app.scodoc.sco_exceptions import ScoValueError
class BaseArchiver:
"""Classe de base pour tous les archivers"""
def __init__(self, archive_type=""):
self.archive_type = archive_type
self.initialized = False
@ -306,400 +293,3 @@ class BaseArchiver:
mime = "application/octet-stream"
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:
"""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:
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)
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):
raise ValueError(f"Aucune archive pour etudid[{etud.id}]")
try:
archive_id = self.get_id_from_name(
etud.id, archive_name, dept_id=etud.dept_id
)
except ScoValueError:
raise ValueError(f"Archive Inconnue [{archive_name}]")
except ScoValueError as exc:
raise ValueError(f"Archive Inconnue [{archive_name}]") from exc
if filename is not None:
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.set_trace(filename, mode="delete")
os.remove(path)
log(f"delete_justificatif: removed {path}")
else:
if has_trace:
@ -197,12 +200,14 @@ class JustificatifArchiver(BaseArchiver):
archive_id,
)
)
log(f"delete_justificatif: deleted archive {archive_id}")
def list_justificatifs(
self, archive_name: str, etud: Identite
) -> list[tuple[str, int]]:
"""
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] = []
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)
trace: Trace = Trace(archive_id)
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):
"""

View File

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

View File

@ -115,7 +115,9 @@ _EVENT_DEFAULT_COLOR = "rgb(214, 233, 248)"
def formsemestre_edt_dict(
formsemestre: FormSemestre, group_ids: list[int] = None
formsemestre: FormSemestre,
group_ids: list[int] = None,
show_modules_titles=True,
) -> list[dict]:
"""EDT complet du semestre, comme une liste de dict serialisable en json.
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()
try:
events_scodoc = _load_and_convert_ics(formsemestre)
events_scodoc, _ = load_and_convert_ics(formsemestre)
except ScoValueError as exc:
return exc.args[0]
# 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 = []
for event in events_scodoc:
group: GroupDescr | bool = event["group"]
@ -140,7 +144,7 @@ def formsemestre_edt_dict(
</div>"""
else:
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
else f"""<div class="group-edt">{event['edt_group']}
<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>"""
bubble = "code module non trouvé dans ScoDoc. Vérifier configuration."
case _: # module EDT bien retrouvé dans ScoDoc
mod_disp = f"""<span class="mod-name mod-code" title="{
modimpl.module.abbrev or ""} ({event['edt_module']})">{
modimpl.module.code}</span>"""
bubble = f"{modimpl.module.abbrev or ''} ({event['edt_module']})"
title = f"""<div class = "module-edt" title="{bubble} {event['title_edt']}">
<a class="discretelink" href="{url_abs or ''}">{mod_disp} <span>{event['title']}</span></a>
bubble = f"{modimpl.module.abbrev or modimpl.module.titre or ''} ({event['edt_module']})"
mod_disp = (
f"""<span class="mod-name mod-code">{modimpl.module.code}</span>"""
)
# {event['title_edt']}
span_title = f" <span>{event['title']}</span>" if show_modules_titles else ""
title = f"""<div class = "module-edt" title="{bubble}">
<a class="discretelink" href="{url_abs or ''}">{mod_disp}{span_title}</a>
</div>
"""
@ -208,8 +213,15 @@ def formsemestre_edt_dict(
return events_cal
def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
"chargement fichier, filtrage et extraction des identifiants."
def load_and_convert_ics(formsemestre: FormSemestre) -> tuple[list[dict], list[str]]:
"""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
_, calendar = formsemestre_load_calendar(formsemestre)
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]
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()
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(
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,
# prend toute la promo ("tous")
group: GroupDescr = (
@ -324,7 +338,7 @@ def _load_and_convert_ics(formsemestre: FormSemestre) -> list[dict]:
"end": event.decoded("dtend").isoformat(),
}
)
return events_sco
return events_sco, sorted(edt_groups_ids)
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_exceptions import (
ScoValueError,
ScoInvalidDateError,
ScoInvalidIdType,
)
from app.scodoc import html_sco_header
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 codes_cursus
from app.scodoc import sco_compute_moy
@ -454,7 +453,9 @@ def formsemestre_status_menubar(formsemestre: FormSemestre) -> str:
"title": "Documents archivés",
"endpoint": "notes.formsemestre_list_archives",
"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-attente: yellow;
--color-justi-attente-stripe: #29b990; /* pink #fa25cb; */ /* #789dbb;*/
--color-justi-modifie: rgb(255, 230, 0);
--color-justi-invalide: #a84476;
--color-nonwork: #badfff;
@ -645,7 +646,7 @@
.assi-liste {
border: 1px solid gray;
border-radius: 12px;
margin-right: 12px;
margin-right: 24px;
padding: 12px;
}
#options-tableau label {
@ -694,6 +695,9 @@ tr.row-justificatif.valide td.assi-type {
tr.row-justificatif.attente td.assi-type {
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 {
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 {
padding: 4px;
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(
"entry_date",
"Saisie le",
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M"),
data={"order": self.ligne["entry_date"]},
self.ligne["entry_date"].strftime("%d/%m/%y à %H:%M")
if self.ligne["entry_date"]
else "?",
data={"order": self.ligne["entry_date"] or ""},
raw_content=self.ligne["entry_date"],
classes=["small-font"],
column_classes={"entry_date"},
@ -387,13 +389,20 @@ class RowAssiJusti(tb.Row):
html.append(f'<a title="Détails" href="{url}"></a>')
# Modifier
url = url_for(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
action="modifier",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
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(
"assiduites.tableau_assiduite_actions",
type=self.ligne["type"],
action="modifier",
obj_id=self.ligne["obj_id"],
scodoc_dept=g.scodoc_dept,
)
html.append(f'<a title="Modifier" href="{url}">📝</a>')
# Supprimer

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 }}
{{ render_field_errors(form, 'modimpl') }}
</div>
{# Raison #}
{# Description #}
<div>
<div>{{ form.assi_raison.label }}</div>
{{ form.assi_raison() }}
{{ render_field_errors(form, 'assi_raison') }}
<div>{{ form.description.label }}</div>
{{ form.description() }}
{{ render_field_errors(form, 'description') }}
</div>
{# Date dépot #}
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }}

View File

@ -52,8 +52,8 @@
<div class="assi-row">
<div class="assi-label">
<legend for="assi_raison">Raison</legend>
<textarea name="assi_raison" id="assi_raison" cols="75" rows="4" maxlength="500"></textarea>
<legend for="raison">Raison</legend>
<textarea name="raison" id="raison" cols="75" rows="4" maxlength="500"></textarea>
</div>
</div>
@ -135,7 +135,7 @@
const { deb, fin } = getDates()
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;
return {
@ -168,7 +168,7 @@
field.querySelector('#assi_date_debut').value = "";
field.querySelector('#assi_date_fin').value = "";
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" %}
{% import 'wtf.j2' as wtf %}
@ -16,6 +18,16 @@ form#ajout-justificatif-etud {
form#ajout-justificatif-etud > div {
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 {
margin-top: 12px;
}
@ -61,15 +73,33 @@ div.submit > input {
</div>
{# Raison #}
<div>
<div>{{ form.assi_raison.label }}</div>
{{ form.assi_raison() }}
{{ render_field_errors(form, 'assi_raison') }}
<div>{{ form.raison.label }}</div>
{{ form.raison() }}
{{ render_field_errors(form, 'raison') }}
</div>
{# Fichier(s) justificatif(s) #}
<div>
<div>{{ form.fichiers.label }}</div>
{{ form.fichiers() }}
{{ render_field_errors(form, 'fichiers') }}
<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>{{ form.fichiers.label }}</div>
{{ form.fichiers() }}
{{ render_field_errors(form, 'fichiers') }}
</div>
</div>
{# Date dépot #}
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }}
@ -83,12 +113,15 @@ div.submit > input {
</fieldset>
</form>
</section>
{% if tableau %}
<section class="assi-liste">
{{tableau | safe }}
</section>
{% endif %}
</div>
{% include "assiduites/explication_etats_justifs.j2" %}
{% endblock app_content %}
{% block scripts %}
@ -108,4 +141,54 @@ $('.timepicker').timepicker({
scrollbar: false
});
</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 %}

View File

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

View File

@ -81,7 +81,10 @@ affectent notamment les comptages d'absences de tous les bulletins des
</div>
<div class="row">
<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="config-edt">
{{ 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>
{{tableau | safe }}
</div>
{% include "assiduites/explication_etats_justifs.j2" %}
{% endblock app_content %}

View File

@ -16,6 +16,12 @@
{{ 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>
<span id="menu-navi">
<button type="button" class="btn btn-default btn-sm move-today"
@ -38,9 +44,16 @@
</li>
<li>Si vous filtrez par groupe, les évènements dont le groupe n'est pas reconnu seront affichés.
</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>
</div>
</div>
{% endblock app_content %}
{% block scripts %}
@ -107,7 +120,7 @@ document.addEventListener('DOMContentLoaded', function() {
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(events=>{
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 = (
datetime.datetime.strptime(form.entry_date.data, "%d/%m/%Y")
if form.entry_date.data
else None
else datetime.datetime.now() # local tz
)
except ValueError:
dt_entry_date = None
@ -464,7 +464,7 @@ def _record_assiduite_etud(
dt_debut_tz_server,
dt_fin_tz_server,
scu.EtatAssiduite.get(form.assi_etat.data),
description=form.assi_raison.data,
description=form.description.data,
entry_date=dt_entry_date_tz_server,
external_data=external_data,
moduleimpl=moduleimpl,
@ -596,6 +596,64 @@ def bilan_etud():
).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(
"/ajout_justificatif_etud", methods=["GET", "POST"]
) # was AjoutJustificatifEtud
@ -603,7 +661,7 @@ def bilan_etud():
@permission_required(Permission.AbsChange)
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:
etudid (int): l'identifiant de l'étudiant
@ -654,8 +712,7 @@ def ajout_justificatif_etud():
def _record_justificatif_etud(
etud: Identite,
form: AjoutJustificatifEtudForm,
etud: Identite, form: AjoutJustificatifEtudForm, justif: Justificatif | None = None
) -> bool:
"""Enregistre les données du formulaire de saisie justificatif (et ses fichiers).
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.date_debut.data : '05/12/2023'
form.heure_debut.data : '09:06' (heure locale du serveur)
Si justif, modifie le justif existant, sinon en crée un nouveau
"""
(
ok,
@ -672,30 +730,53 @@ def _record_justificatif_etud(
) = _get_dates_from_assi_form(form)
if not ok:
log("_record_justificatif_etud: dates invalides")
form.set_error("Erreur: dates invalides")
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:
just = Justificatif.create_justificatif(
etud,
dt_debut_tz_server,
dt_fin_tz_server,
etat=etat,
raison=form.assi_raison.data,
entry_date=dt_entry_date_tz_server,
user_id=current_user.id,
)
db.session.add(just)
if not _upload_justificatif_files(just, form):
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,
dt_debut_tz_server,
dt_fin_tz_server,
etat=etat,
raison=form.raison.data,
entry_date=dt_entry_date_tz_server,
user_id=current_user.id,
)
message = "Justificatif créé"
db.session.add(justif)
if not _upload_justificatif_files(justif, form):
flash("Erreur enregistrement fichiers")
log("problem in _upload_justificatif_files, rolling back")
db.session.rollback()
return False
db.session.commit()
compute_assiduites_justified(etud.id, [just])
scass.simple_invalidate_cache(just.to_dict(), etud.id)
flash("Justificatif enregistré")
compute_assiduites_justified(etud.id, [justif])
scass.simple_invalidate_cache(justif.to_dict(), etud.id)
flash(message)
return True
except ScoValueError as exc:
log(f"_record_justificatif_etud: erreur {exc.args[0]}")
db.session.rollback()
form.set_error(f"Erreur: {exc.args[0]}")
return False
@ -1474,10 +1555,10 @@ def _action_modifier_assiduite(assi: Assiduite):
def _action_modifier_justificatif(justi: Justificatif):
"Modifie le justificatif avec les valeurs dans le form"
form = request.form
# Gestion des Dates
date_debut: datetime = scu.is_iso_formated(form["date_debut"], 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:
@ -1556,40 +1637,30 @@ def _preparer_objet(
_preparer_objet("justificatif", justi, sans_gros_objet=True)
)
else:
else: # objet == "justificatif"
justif: Justificatif = objet
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["raison"] = "" if objet.raison is None else objet.raison
objet_prepare["real_etat"] = scu.EtatJustificatif(justif.etat).name.lower()
objet_prepare["raison"] = "" if justif.raison is None else justif.raison
objet_prepare["raison"] = objet_prepare["raison"].strip()
objet_prepare["justification"] = {"assiduites": [], "fichiers": {}}
if not sans_gros_objet:
assiduites: list[int] = scass.justifies(objet)
assiduites: list[int] = scass.justifies(justif)
for assi_id in assiduites:
assi: Assiduite = Assiduite.query.get(assi_id)
objet_prepare["justification"]["assiduites"].append(
_preparer_objet("assiduite", assi, sans_gros_objet=True)
)
# Récupération de l'archive avec l'archiver
archive_name: str = objet.fichier
filenames: list[str] = []
archiver: JustificatifArchiver = JustificatifArchiver()
if archive_name is not None:
filenames = archiver.list_justificatifs(archive_name, objet.etudiant)
# fichiers justificatifs archivés:
filenames, nb_files = justif.get_fichiers()
objet_prepare["justification"]["fichiers"] = {
"total": len(filenames),
"filenames": [],
"total": nb_files,
"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["real_date_fin"] = objet.date_fin.isoformat()
@ -1600,7 +1671,7 @@ def _preparer_objet(
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)
objet_prepare["saisie_par"] = user.get_nomprenom()
else:

View File

@ -103,7 +103,7 @@ from app.scodoc import html_sco_header
from app.pe import pe_view
from app.scodoc import sco_apogee_compare
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_bulletins
from app.scodoc import sco_bulletins_pdf
@ -2973,24 +2973,24 @@ sco_publish(
)
sco_publish(
"/formsemestre_archive",
sco_archive_formsemestre.formsemestre_archive,
sco_archives_formsemestre.formsemestre_archive,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_delete_archive",
sco_archive_formsemestre.formsemestre_delete_archive,
sco_archives_formsemestre.formsemestre_delete_archive,
Permission.ScoView,
methods=["GET", "POST"],
)
sco_publish(
"/formsemestre_list_archives",
sco_archive_formsemestre.formsemestre_list_archives,
sco_archives_formsemestre.formsemestre_list_archives,
Permission.ScoView,
)
sco_publish(
"/formsemestre_get_archived_file",
sco_archive_formsemestre.formsemestre_get_archived_file,
sco_archives_formsemestre.formsemestre_get_archived_file,
Permission.ScoView,
)
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.models import Formation, FormSemestre, ScoDocSiteConfig
from app.scodoc import sco_formations, sco_formation_versions
from app.scodoc import sco_groups_view
from app.scodoc import (
sco_edt_cal,
sco_formations,
sco_formation_versions,
sco_groups_view,
)
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 ScoData
@ -158,6 +163,7 @@ def formsemestre_edit_modimpls_codes(formsemestre_id: int):
@permission_required(Permission.ScoView)
def formsemestre_edt(formsemestre_id: int):
"""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)
cfg = ScoDocSiteConfig.query.filter_by(name="assi_morning_time").first()
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,
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
from app.models import Formation, FormSemestre
from app.scodoc import (
sco_archive_formsemestre,
sco_archives_formsemestre,
sco_cost_formation,
sco_debouche,
sco_edit_ue,
@ -182,8 +182,8 @@ def test_formsemestre_misc_views(test_client):
assert isinstance(ans, Response)
assert ans.status == "200 OK"
assert ans.mimetype == scu.JSON_MIMETYPE
ans = sco_archive_formsemestre.formsemestre_archive(formsemestre.id)
ans = sco_archive_formsemestre.formsemestre_list_archives(formsemestre.id)
ans = sco_archives_formsemestre.formsemestre_archive(formsemestre.id)
ans = sco_archives_formsemestre.formsemestre_list_archives(formsemestre.id)
# ----- MENU STATISTIQUES
ans = sco_report.formsemestre_report_counts(formsemestre.id)