diff --git a/app/forms/assiduite/ajout_assiduite_etud.py b/app/forms/assiduite/ajout_assiduite_etud.py index 8c3423acc..302f73776 100644 --- a/app/forms/assiduite/ajout_assiduite_etud.py +++ b/app/forms/assiduite/ajout_assiduite_etud.py @@ -62,6 +62,11 @@ class AjoutAssiOrJustForm(FlaskForm): if field: field.errors.append(err_msg) + def disable_all(self): + "Disable all fields" + for field in self: + field.render_kw = {"disabled": True} + date_debut = StringField( "Date de début", validators=[validators.Length(max=10)], @@ -175,36 +180,3 @@ class AjoutJustificatifEtudForm(AjoutAssiOrJustForm): validators=[DataRequired(message="This field is required.")], ) fichiers = MultipleFileField(label="Ajouter des fichiers") - - -class ChoixDateForm(FlaskForm): - """ - Formulaire de choix de date - (utilisé par la page de choix de date - si la date courante n'est pas dans le semestre) - """ - - def __init__(self, *args, **kwargs): - "Init form, adding a filed for our error messages" - super().__init__(*args, **kwargs) - self.ok = True - self.error_messages: list[str] = [] # used to report our errors - - def set_error(self, err_msg, field=None): - "Set error message both in form and field" - self.ok = False - self.error_messages.append(err_msg) - if field: - field.errors.append(err_msg) - - date = StringField( - "Date", - validators=[validators.Length(max=10)], - render_kw={ - "class": "datepicker", - "size": 10, - "id": "date", - }, - ) - submit = SubmitField("Enregistrer") - cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/forms/assiduite/edit_assiduite_etud.py b/app/forms/assiduite/edit_assiduite_etud.py new file mode 100644 index 000000000..ca284c014 --- /dev/null +++ b/app/forms/assiduite/edit_assiduite_etud.py @@ -0,0 +1,58 @@ +""" """ + +from flask_wtf import FlaskForm +from wtforms import SelectField, RadioField, TextAreaField, validators, SubmitField +from app.scodoc.sco_utils import EtatAssiduite + + +class EditAssiForm(FlaskForm): + """ + Formulaire de modification d'une assiduité + """ + + def __init__(self, *args, **kwargs): + "Init form, adding a filed for our error messages" + super().__init__(*args, **kwargs) + self.ok = True + self.error_messages: list[str] = [] # used to report our errors + + def set_error(self, err_msg, field=None): + "Set error message both in form and field" + self.ok = False + self.error_messages.append(err_msg) + if field: + field.errors.append(err_msg) + + def disable_all(self): + "Disable all fields" + for field in self: + field.render_kw = {"disabled": True} + + assi_etat = RadioField( + "État:", + choices=[ + (EtatAssiduite.ABSENT.value, EtatAssiduite.ABSENT.version_lisible()), + (EtatAssiduite.RETARD.value, EtatAssiduite.RETARD.version_lisible()), + (EtatAssiduite.PRESENT.value, EtatAssiduite.PRESENT.version_lisible()), + ], + default="absent", + validators=[ + validators.DataRequired("spécifiez le type d'évènement à signaler"), + ], + ) + modimpl = SelectField( + "Module", + choices={}, # will be populated dynamically + ) + description = TextAreaField( + "Description", + render_kw={ + "id": "description", + "cols": 75, + "rows": 4, + "maxlength": 500, + }, + ) + + submit = SubmitField("Enregistrer") + cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/models/assiduites.py b/app/models/assiduites.py index 241e44aa6..b741005e4 100644 --- a/app/models/assiduites.py +++ b/app/models/assiduites.py @@ -360,6 +360,16 @@ class Assiduite(ScoDocModel): return "Module non spécifié" if traduire else None + def get_moduleimpl_id(self) -> int | str | None: + """ + Retourne le ModuleImpl associé à l'assiduité + """ + if self.moduleimpl_id is not None: + return self.moduleimpl_id + if self.external_data is not None and "module" in self.external_data: + return self.external_data["module"] + return None + def get_saisie(self) -> str: """ retourne le texte "saisie le par " @@ -395,6 +405,14 @@ class Assiduite(ScoDocModel): if force: raise ScoValueError("Module non renseigné") + @classmethod + def get_assiduite(cls, assiduite_id: int) -> "Assiduite": + """Assiduité ou 404, cherche uniquement dans le département courant""" + query = Assiduite.query.filter_by(id=assiduite_id) + if g.scodoc_dept: + query = query.join(Identite).filter_by(dept_id=g.scodoc_dept_id) + return query.first_or_404() + class Justificatif(ScoDocModel): """ diff --git a/app/static/js/assiduites.js b/app/static/js/assiduites.js index e796923f2..91c651e37 100644 --- a/app/static/js/assiduites.js +++ b/app/static/js/assiduites.js @@ -870,21 +870,12 @@ function setupAssiduiteBubble(el, assiduite) { const infos = document.createElement("a"); infos.className = ""; infos.textContent = `ℹ️`; - infos.title = "Cliquez pour plus d'informations"; + infos.title = "Détails / Modifier"; infos.target = "_blank"; - infos.href = `tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assiduite.assiduite_id}`; - - // Ajout d'un lien pour modifier l'assiduité - const modifs = document.createElement("a"); - modifs.className = ""; - modifs.textContent = `📝`; - modifs.title = "Cliquez pour modifier l'assiduité"; - modifs.target = "_blank"; - modifs.href = `tableau_assiduite_actions?type=assiduite&action=modifier&obj_id=${assiduite.assiduite_id}`; + infos.href = `edit_assiduite_etud/${assiduite.assiduite_id}`; const actionsDiv = document.createElement("div"); actionsDiv.className = "assiduite-actions"; - actionsDiv.appendChild(modifs); actionsDiv.appendChild(infos); bubble.appendChild(actionsDiv); diff --git a/app/tables/liste_assiduites.py b/app/tables/liste_assiduites.py index c0c7dbbba..7b0204b92 100644 --- a/app/tables/liste_assiduites.py +++ b/app/tables/liste_assiduites.py @@ -600,33 +600,22 @@ class RowAssiJusti(tb.Row): url: str html: list[str] = [] - # Détails - url = url_for( - "assiduites.tableau_assiduite_actions", - type=self.ligne["type"], - action="details", - obj_id=self.ligne["obj_id"], - scodoc_dept=g.scodoc_dept, - ) - html.append(f'ℹ️') - - # Modifier if self.ligne["type"] == "justificatif": + # Détails/Modifier assiduité url = url_for( "assiduites.edit_justificatif_etud", justif_id=self.ligne["obj_id"], scodoc_dept=g.scodoc_dept, - back_url=request.url, ) + html.append(f'ℹ️') else: + # Détails/Modifier assiduité url = url_for( - "assiduites.tableau_assiduite_actions", - type=self.ligne["type"], - action="modifier", - obj_id=self.ligne["obj_id"], + "assiduites.edit_assiduite_etud", + assiduite_id=self.ligne["obj_id"], scodoc_dept=g.scodoc_dept, ) - html.append(f'📝') + html.append(f'ℹ️') # Supprimer url = url_for( diff --git a/app/templates/assiduites/pages/ajout_justificatif_etud.j2 b/app/templates/assiduites/pages/ajout_justificatif_etud.j2 index 221f3b496..a6527203b 100644 --- a/app/templates/assiduites/pages/ajout_justificatif_etud.j2 +++ b/app/templates/assiduites/pages/ajout_justificatif_etud.j2 @@ -44,11 +44,33 @@ div.submit > input {

{{title|safe}}

- + {% if readonly %} +

Vous n'avez pas la permission de modifier ce justificatif

+ {% endif %} {% if justif %} +
+
- Saisie par {{justif.user.get_prenomnom() if justif.user else "inconnu"}} - le {{justif.entry_date.strftime(scu.DATEATIME_FMT) if justif.entry_date else "?"}} + Saisie par {{justif.saisie_par}} le {{justif.entry_date}} +
+ +
+ Assiduités concernées: + {% if justif.justification.assiduites %} + + {% else %} + Aucune + {% endif %} +
+
{% endif %} @@ -110,7 +132,9 @@ div.submit > input { {% for filename in filenames %}
  • {{scu.icontag("delete_img", alt="supprimer", title="Supprimer")|safe}} - {{filename}}
  • + {{filename}} + {% endfor %} {% endif %} @@ -126,11 +150,28 @@ div.submit > input { laisser vide pour date courante {{ render_field_errors(form, 'entry_date') }} + {% if readonly == False %} {# Submit #}
    {{ form.submit }} {{ form.cancel }}
    + + {% endif %} + + + diff --git a/app/templates/assiduites/pages/calendrier_assi_etud.j2 b/app/templates/assiduites/pages/calendrier_assi_etud.j2 index d78222944..b4fa38547 100644 --- a/app/templates/assiduites/pages/calendrier_assi_etud.j2 +++ b/app/templates/assiduites/pages/calendrier_assi_etud.j2 @@ -242,7 +242,7 @@ Calendrier de l'assiduité document.querySelectorAll('[assi_id]').forEach((el, i) => { el.addEventListener('click', () => { const assi_id = el.getAttribute('assi_id'); - window.open(`${SCO_URL}Assiduites/tableau_assiduite_actions?type=assiduite&action=details&obj_id=${assi_id}`); + window.open(`${SCO_URL}Assiduites/edit_assiduite_etud/${assi_id}`); }) }); diff --git a/app/templates/assiduites/pages/edit_assiduite_etud.j2 b/app/templates/assiduites/pages/edit_assiduite_etud.j2 new file mode 100644 index 000000000..8b0930e87 --- /dev/null +++ b/app/templates/assiduites/pages/edit_assiduite_etud.j2 @@ -0,0 +1,165 @@ +{# Ajout d'une "assiduité" sur un étudiant #} + +{% extends "sco_page.j2" %} +{% import 'wtf.j2' as wtf %} + + +{% block styles %} +{{super()}} + + + +{% endblock %} + +{% block app_content %} +
    +

    Détails Assiduité concernant {{etud.html_link_fiche()|safe}}

    + +
    +
    + Saisie par {{objet.saisie_par}} le {{objet.entry_date}} +
    +
    + Période : du {{objet.date_debut}} au {{objet.date_fin}} +
    +
    + Module : {{objet.module}} +
    +
    + État de l'assiduité :{{objet.etat}} +
    +
    + Description: + {% if objet.description != "" and objet.description is not None %} + {{objet.description}} + {% else %} + Pas de description + {% endif %} + +
    + {# Affichage des justificatifs si assiduité justifiée #} + {% if objet.etat != "Présence" %} +
    + Justifiée: + {% if objet.justification.est_just %} + Oui + {% else %} + Non + {% if not objet.justification.justificatifs %} + Justifier l'assiduité + {% endif %} + {% endif %} +
    +
    + {% if not objet.justification.justificatifs %} + Pas de justificatif associé + {% else %} + Justificatifs associés: + + {% endif %} +
    + {% endif %} +
    + + {% if readonly != True %} +

    Modification de l'assiduité

    + {% for err_msg in form.error_messages %} +
    + {{ err_msg }} +
    + {% endfor %} + +
    + {{ form.hidden_tag() }} + {# Type d'évènement #} +
    + {{ form.assi_etat.label }} + {{ form.assi_etat() }} +
    + {# Menu module #} +
    + {{ form.modimpl.label }} : + {{ form.modimpl }} + {{ render_field_errors(form, 'modimpl') }} +
    + {# Description #} +
    +
    {{ form.description.label }}
    + {{ form.description() }} + {{ render_field_errors(form, 'description') }} +
    + {# Submit #} +
    + {{ form.submit }} {{ form.cancel }} +
    + + +
    + + {% else %} +

    Vous n'avez pas la permission de modifier cette assiduité

    + {% endif %} + +
    + +{% endblock app_content %} + +{% block scripts %} +{{ super() }} + +{% include "sco_timepicker.j2" %} +{% endblock scripts %} \ No newline at end of file diff --git a/app/views/assiduites.py b/app/views/assiduites.py index e71a32469..b98d61ecd 100644 --- a/app/views/assiduites.py +++ b/app/views/assiduites.py @@ -36,6 +36,7 @@ from flask_login import current_user from flask_sqlalchemy.query import Query from markupsafe import Markup +from werkzeug.exceptions import HTTPException from app import db, log from app.comp import res_sem @@ -48,8 +49,8 @@ from app.forms.assiduite.ajout_assiduite_etud import ( AjoutAssiOrJustForm, AjoutAssiduiteEtudForm, AjoutJustificatifEtudForm, - ChoixDateForm, ) +from app.forms.assiduite.edit_assiduite_etud import EditAssiForm from app.models import ( Assiduite, Departement, @@ -538,10 +539,8 @@ def _record_assiduite_etud( assi: Assiduite = conflits.first() lien: str = url_for( - "assiduites.tableau_assiduite_actions", - type="assiduite", - action="details", - obj_id=assi.assiduite_id, + "assiduites.edit_assiduite_etud", + assiuite_id=assi.assiduite_id, scodoc_dept=g.scodoc_dept, ) @@ -612,7 +611,7 @@ def bilan_etud(): @bp.route("/edit_justificatif_etud/", methods=["GET", "POST"]) @scodoc -@permission_required(Permission.AbsChange) +@permission_required(Permission.ScoView) def edit_justificatif_etud(justif_id: int): """ Edition d'un justificatif. @@ -624,8 +623,19 @@ def edit_justificatif_etud(justif_id: int): Returns: str: l'html généré """ - justif = Justificatif.get_justificatif(justif_id) + try: + justif = Justificatif.get_justificatif(justif_id) + except HTTPException: + flash("Justificatif invalide") + return redirect(url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)) + + readonly = not current_user.has_permission(Permission.AbsChange) + form = AjoutJustificatifEtudForm(obj=justif) + + if readonly: + form.disable_all() + # Set the default value for the etat field if request.method == "GET": form.date_debut.data = justif.date_debut.strftime(scu.DATE_FMT) @@ -652,7 +662,9 @@ def edit_justificatif_etud(justif_id: int): ) if form.validate_on_submit(): - if form.cancel.data: # cancel button + if form.cancel.data or not current_user.has_permission( + Permission.AbsChange + ): # cancel button return redirect(redirect_url) if _record_justificatif_etud(justif.etudiant, form, justif): return redirect(redirect_url) @@ -667,12 +679,13 @@ def edit_justificatif_etud(justif_id: int): etud=justif.etudiant, filenames=filenames, form=form, - justif=justif, + justif=_preparer_objet("justificatif", justif), nb_files=nb_files, title=f"Modification justificatif absence de {justif.etudiant.html_link_fiche()}", redirect_url=redirect_url, sco=ScoData(justif.etudiant), scu=scu, + readonly=not current_user.has_permission(Permission.AbsChange), ) @@ -1672,20 +1685,18 @@ def _preparer_objet( # Gestion justification - if not objet.est_just: - objet_prepare["justification"] = {"est_just": False} - else: - objet_prepare["justification"] = {"est_just": True, "justificatifs": []} + objet_prepare["justification"] = { + "est_just": objet.est_just, + "justificatifs": [], + } - if not sans_gros_objet: - justificatifs: list[int] = get_assiduites_justif( - objet.assiduite_id, False + if not sans_gros_objet: + justificatifs: list[int] = get_assiduites_justif(objet.assiduite_id, False) + for justi_id in justificatifs: + justi: Justificatif = Justificatif.query.get(justi_id) + objet_prepare["justification"]["justificatifs"].append( + _preparer_objet("justificatif", justi, sans_gros_objet=True) ) - for justi_id in justificatifs: - justi: Justificatif = Justificatif.query.get(justi_id) - objet_prepare["justification"]["justificatifs"].append( - _preparer_objet("justificatif", justi, sans_gros_objet=True) - ) else: # objet == "justificatif" justif: Justificatif = objet @@ -1698,9 +1709,8 @@ def _preparer_objet( objet_prepare["justification"] = {"assiduites": [], "fichiers": {}} if not sans_gros_objet: - assiduites: list[int] = scass.justifies(justif) - for assi_id in assiduites: - assi: Assiduite = Assiduite.query.get(assi_id) + assiduites: list[Assiduite] = justif.get_assiduites() + for assi in assiduites: objet_prepare["justification"]["assiduites"].append( _preparer_objet("assiduite", assi, sans_gros_objet=True) ) @@ -2152,6 +2162,106 @@ def signal_assiduites_hebdo(): ) +@bp.route("edit_assiduite_etud/", methods=["GET", "POST"]) +@scodoc +@permission_required(Permission.ScoView) +def edit_assiduite_etud(assiduite_id: int): + """ + Page affichant les détails d'une assiduité + Si le current_user alors la page propose un formulaire de modification + """ + try: + assi: Assiduite = Assiduite.get_assiduite(assiduite_id=assiduite_id) + except HTTPException: + flash("Assiduité invalide") + return redirect(url_for("assiduites.bilan_dept", scodoc_dept=g.scodoc_dept)) + + etud: Identite = assi.etudiant + formsemestre: FormSemestre = assi.get_formsemestre() + + readonly: bool = not current_user.has_permission(Permission.AbsChange) + + form: EditAssiForm = EditAssiForm(request.form) + if readonly: + form.disable_all() + + # peuplement moduleimpl_select + modimpls_by_formsemestre = etud.get_modimpls_by_formsemestre(scu.annee_scolaire()) + choices: OrderedDict = OrderedDict() + choices[""] = [("", "Non spécifié"), ("autre", "Autre module (pas dans la liste)")] + + # indique le nom du semestre dans le menu (optgroup) + group_name: str = formsemestre.titre_annee() + choices[group_name] = [ + (m.id, f"{m.module.code} {m.module.abbrev or m.module.titre or ''}") + for m in modimpls_by_formsemestre[formsemestre.id] + if m.module.ue.type == UE_STANDARD + ] + + choices.move_to_end("", last=False) + form.modimpl.choices = choices + + # Vérification formulaire + if form.validate_on_submit(): + if form.cancel.data: # cancel button + return redirect(request.referrer) + + # vérification des valeurs + + # Gestion de l'état + etat = form.assi_etat.data + try: + etat = int(etat) + etat = scu.EtatAssiduite.inverse().get(etat, None) + except ValueError: + etat = None + + if etat is None: + form.error_messages.append("État invalide") + form.ok = False + + description = form.description.data or "" + description = description.strip() + moduleimpl_id = form.modimpl.data or -1 + if isinstance(moduleimpl_id, int): + try: + ModuleImpl.get_moduleimpl(moduleimpl_id) + except ValueError: + form.error_messages.append("Module invalide") + moduleimpl_id = -1 + form.ok = False + + if form.ok: + assi.etat = etat + assi.description = description + if moduleimpl_id != -1: + assi.set_moduleimpl(moduleimpl_id) + + db.session.add(assi) + db.session.commit() + + scass.simple_invalidate_cache(assi.to_dict(format_api=True), assi.etudid) + + flash("enregistré") + return redirect(request.referrer) + + # Remplissage du formulaire + form.assi_etat.data = str(assi.etat) + form.description.data = assi.description + moduleimpl_id: int | str | None = assi.get_moduleimpl_id() or "" + form.modimpl.data = str(moduleimpl_id) + + return render_template( + "assiduites/pages/edit_assiduite_etud.j2", + etud=etud, + sco=ScoData(etud, formsemestre=formsemestre), + form=form, + readonly=readonly, + objet=_preparer_objet("assiduite", assi), + title=f"Assiduité {etud.nom_short}", + ) + + def generate_bul_list(etud: Identite, semestre: FormSemestre) -> str: """Génère la liste des assiduités d'un étudiant pour le bulletin mail"""