diff --git a/app/__init__.py b/app/__init__.py index 0ea6bb52c..0a5891447 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -190,6 +190,7 @@ def create_app(config_class=DevConfig): app.register_error_handler(ScoGenError, handle_sco_value_error) app.register_error_handler(ScoValueError, handle_sco_value_error) + app.register_error_handler(AccessDenied, handle_access_denied) app.register_error_handler(500, internal_server_error) app.register_error_handler(503, postgresql_server_error) diff --git a/app/auth/models.py b/app/auth/models.py index bc4edb65f..8f187b7e9 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -11,7 +11,7 @@ from time import time from typing import Optional import cracklib # pylint: disable=import-error -from flask import current_app, url_for, g +from flask import current_app, g from flask_login import UserMixin, AnonymousUserMixin from werkzeug.security import generate_password_hash, check_password_hash @@ -136,6 +136,7 @@ class User(UserMixin, db.Model): return check_password_hash(self.password_hash, password) def get_reset_password_token(self, expires_in=600): + "Un token pour réinitialiser son mot de passe" return jwt.encode( {"reset_password": self.id, "exp": time() + expires_in}, current_app.config["SECRET_KEY"], @@ -144,15 +145,17 @@ class User(UserMixin, db.Model): @staticmethod def verify_reset_password_token(token): + "Vérification du token de reéinitialisation du mot de passe" try: - id = jwt.decode( + user_id = jwt.decode( token, current_app.config["SECRET_KEY"], algorithms=["HS256"] )["reset_password"] except: return - return User.query.get(id) + return User.query.get(user_id) def to_dict(self, include_email=True): + """l'utilisateur comme un dict, avec des champs supplémentaires""" data = { "date_expiration": self.date_expiration.isoformat() + "Z" if self.date_expiration @@ -472,5 +475,5 @@ def get_super_admin(): @login.user_loader -def load_user(id): - return User.query.get(int(id)) +def load_user(uid): + return User.query.get(int(uid)) diff --git a/app/but/bulletin_but_xml_compat.py b/app/but/bulletin_but_xml_compat.py index 9743c2181..69fc4ef16 100644 --- a/app/but/bulletin_but_xml_compat.py +++ b/app/but/bulletin_but_xml_compat.py @@ -72,7 +72,7 @@ def bulletin_but_xml_compat( etud = Identite.query.get_or_404(etudid) results = bulletin_but.ResultatsSemestreBUT(sem) nb_inscrits = len(results.etuds) - if sem.bul_hide_xml or force_publishing: + if (not sem.bul_hide_xml) or force_publishing: published = "1" else: published = "0" diff --git a/app/forms/main/config_forms.py b/app/forms/main/config_forms.py index 035897950..16be84518 100644 --- a/app/forms/main/config_forms.py +++ b/app/forms/main/config_forms.py @@ -245,7 +245,7 @@ class DeptForm(FlaskForm): def _make_dept_id_name(): - """Cette section assute que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ) + """Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ) et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département) -> [ (None, None), (dept_id, dept_name)... ]""" depts = [(None, GLOBAL)] diff --git a/app/forms/main/create_dept.py b/app/forms/main/create_dept.py index 7bc26b42a..d1e143bed 100644 --- a/app/forms/main/create_dept.py +++ b/app/forms/main/create_dept.py @@ -31,16 +31,10 @@ Formulaires création département from flask import flash, url_for, redirect, render_template from flask_wtf import FlaskForm -from wtforms import SelectField, SubmitField, FormField, validators, FieldList -from wtforms.fields.simple import StringField, HiddenField +from wtforms import SubmitField, validators +from wtforms.fields.simple import StringField, BooleanField -from app import AccessDenied -from app.models import Departement -from app.models import ScoPreference from app.models import SHORT_STR_LEN -from app.scodoc import sco_utils as scu - -from flask_login import current_user class CreateDeptForm(FlaskForm): @@ -60,5 +54,9 @@ class CreateDeptForm(FlaskForm): validators.DataRequired("acronyme du département requis"), ], ) + visible = BooleanField( + "Visible sur page d'accueil", + default=True, + ) submit = SubmitField("Valider") cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/models/departements.py b/app/models/departements.py index 7ed2f4b56..a8c1bb1e2 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -49,11 +49,11 @@ class Departement(db.Model): return dept -def create_dept(acronym: str) -> Departement: +def create_dept(acronym: str, visible=True) -> Departement: "Create new departement" from app.models import ScoPreference - departement = Departement(acronym=acronym) + departement = Departement(acronym=acronym, visible=visible) p1 = ScoPreference(name="DeptName", value=acronym, departement=departement) db.session.add(p1) db.session.add(departement) diff --git a/app/scodoc/sco_edit_apc.py b/app/scodoc/sco_edit_apc.py index 773cf81d1..05b72b011 100644 --- a/app/scodoc/sco_edit_apc.py +++ b/app/scodoc/sco_edit_apc.py @@ -78,7 +78,8 @@ def html_edit_formation_apc( alt="supprimer", ), "delete_disabled": scu.icontag( - "delete_small_dis_img", title="Suppression impossible (module utilisé)" + "delete_small_dis_img", + title="Suppression impossible (utilisé dans des semestres)", ), } diff --git a/app/scodoc/sco_edit_matiere.py b/app/scodoc/sco_edit_matiere.py index 62a7e1d20..13ddb2a99 100644 --- a/app/scodoc/sco_edit_matiere.py +++ b/app/scodoc/sco_edit_matiere.py @@ -30,13 +30,18 @@ """ import flask from flask import g, url_for, request +from app.models.formations import Matiere import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu from app import log from app.models import Formation from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message -from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError +from app.scodoc.sco_exceptions import ( + ScoValueError, + ScoLockedFormError, + ScoNonEmptyFormationObject, +) from app.scodoc import html_sco_header _matiereEditor = ndb.EditableTable( @@ -156,6 +161,16 @@ associé. return flask.redirect(dest_url) +def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]: + "True si la matiere n'est pas utilisée dans des formsemestre" + locked = matiere_is_locked(matiere.id) + if locked: + return False + if any(m.modimpls.all() for m in matiere.modules): + return False + return True + + def do_matiere_delete(oid): "delete matiere and attached modules" from app.scodoc import sco_formations @@ -165,17 +180,16 @@ def do_matiere_delete(oid): cnx = ndb.GetDBConnexion() # check - mat = matiere_list({"matiere_id": oid})[0] + matiere = Matiere.query.get_or_404(oid) + mat = matiere_list({"matiere_id": oid})[0] # compat sco7 ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0] - locked = matiere_is_locked(mat["matiere_id"]) - if locked: - log("do_matiere_delete: mat=%s" % mat) - log("do_matiere_delete: ue=%s" % ue) - log("do_matiere_delete: locked sems: %s" % locked) - raise ScoLockedFormError() - log("do_matiere_delete: matiere_id=%s" % oid) + if not can_delete_matiere(matiere): + # il y a au moins un modimpl dans un module de cette matière + raise ScoNonEmptyFormationObject("Matière", matiere.titre) + + log("do_matiere_delete: matiere_id=%s" % matiere.id) # delete all modules in this matiere - mods = sco_edit_module.module_list({"matiere_id": oid}) + mods = sco_edit_module.module_list({"matiere_id": matiere.id}) for mod in mods: sco_edit_module.do_module_delete(mod["module_id"]) _matiereEditor.delete(cnx, oid) @@ -194,11 +208,25 @@ def matiere_delete(matiere_id=None): """Delete matière""" from app.scodoc import sco_edit_ue - M = matiere_list(args={"matiere_id": matiere_id})[0] - UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0] + matiere = Matiere.query.get_or_404(matiere_id) + if not can_delete_matiere(matiere): + # il y a au moins un modimpl dans un module de cette matière + raise ScoNonEmptyFormationObject( + "Matière", + matiere.titre, + dest_url=url_for( + "notes.ue_table", + formation_id=matiere.ue.formation_id, + semestre_idx=matiere.ue.semestre_idx, + scodoc_dept=g.scodoc_dept, + ), + ) + + mat = matiere_list(args={"matiere_id": matiere_id})[0] + UE = sco_edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0] H = [ html_sco_header.sco_header(page_title="Suppression d'une matière"), - "
Il faut d'abord supprimer le semestre. Mais il est peut être préférable de - laisser ce programme intact et d'en créer une nouvelle version pour la modifier. +
Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de + laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place.
reprendre @@ -365,12 +387,21 @@ def do_module_delete(oid): def module_delete(module_id=None): """Delete a module""" - if not module_id: - raise ScoValueError("invalid module !") - modules = module_list(args={"module_id": module_id}) - if not modules: - raise ScoValueError("Module inexistant !") - mod = modules[0] + module = Module.query.get_or_404(module_id) + mod = module_list(args={"module_id": module_id})[0] # sco7 + + if not can_delete_module(module): + raise ScoNonEmptyFormationObject( + "Module", + msg=module.titre, + dest_url=url_for( + "notes.ue_table", + scodoc_dept=g.scodoc_dept, + formation_id=module.formation_id, + semestre_idx=module.ue.semestre_idx, + ), + ) + H = [ html_sco_header.sco_header(page_title="Suppression d'un module"), """%d étudiants ont validé l'UE %s (%s)
Si vous supprimez cette UE, ces validations vont être supprimées !
" - % (len(validations), ue["acronyme"], ue["titre"]), + % (len(validations), ue.acronyme, ue.titre), dest_url="", target_variable="delete_validations", cancel_url=url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=str(ue["formation_id"]), + formation_id=ue.formation_id, + semestre_idx=ue.semestre_idx, ), - parameters={"ue_id": ue_id, "dialog_confirmed": 1}, + parameters={"ue_id": ue.id, "dialog_confirmed": 1}, ) if delete_validations: - log("deleting all validations of UE %s" % ue_id) + log("deleting all validations of UE %s" % ue.id) ndb.SimpleQuery( "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", - {"ue_id": ue_id}, + {"ue_id": ue.id}, ) # delete all matiere in this UE - mats = sco_edit_matiere.matiere_list({"ue_id": ue_id}) + mats = sco_edit_matiere.matiere_list({"ue_id": ue.id}) for mat in mats: sco_edit_matiere.do_matiere_delete(mat["matiere_id"]) # delete uecoef and events ndb.SimpleQuery( "DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s", - {"ue_id": ue_id}, + {"ue_id": ue.id}, ) - ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue_id}) + ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue.id}) cnx = ndb.GetDBConnexion() - _ueEditor.delete(cnx, ue_id) - # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?): + _ueEditor.delete(cnx, ue.id) + # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement + # utilisé: acceptable de tout invalider): sco_cache.invalidate_formsemestre() # news - F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0] + F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0] sco_news.add( typ=sco_news.NEWS_FORM, - object=ue["formation_id"], + object=ue.formation_id, text="Modification de la formation %(acronyme)s" % F, max_frequency=3, ) @@ -197,11 +221,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False): url_for( "notes.ue_table", scodoc_dept=g.scodoc_dept, - formation_id=ue["formation_id"], + formation_id=ue.formation_id, + semestre_idx=ue.semestre_idx, ) ) - else: - return None + return None def ue_create(formation_id=None): @@ -211,8 +235,6 @@ def ue_create(formation_id=None): def ue_edit(ue_id=None, create=False, formation_id=None): """Modification ou création d'une UE""" - from app.scodoc import sco_formations - create = int(create) if not create: U = ue_list(args={"ue_id": ue_id}) @@ -444,24 +466,38 @@ def next_ue_numero(formation_id, semestre_id=None): def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False): """Delete an UE""" - ues = ue_list(args={"ue_id": ue_id}) - if not ues: - raise ScoValueError("UE inexistante !") - ue = ues[0] - - if not dialog_confirmed: - return scu.confirm_dialog( - "Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}). + Mais il est peut-être préférable de laisser ce programme intact et d'en créer une + nouvelle version pour la modifier sans affecter les semestres déjà en place. +
+ """ + super().__init__(msg=msg, dest_url=dest_url) class ScoGenError(ScoException): "exception avec affichage d'une page explicative ad-hoc" def __init__(self, msg=""): - ScoException.__init__(self, msg) + super().__init__(msg) class AccessDenied(ScoGenError): @@ -101,7 +116,7 @@ class APIInvalidParams(Exception): status_code = 400 def __init__(self, message, status_code=None, payload=None): - Exception.__init__(self) + super().__init__() self.message = message if status_code is not None: self.status_code = status_code diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 6c889c54a..060289e95 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -198,10 +198,13 @@ def do_formsemestre_createwithmodules(edit=False): NB_SEM = parcours.NB_SEM else: NB_SEM = 10 # fallback, max 10 semestres - semestre_id_list = [-1] + list(range(1, NB_SEM + 1)) + if NB_SEM == 1: + semestre_id_list = [-1] + else: + semestre_id_list = [-1] + list(range(1, NB_SEM + 1)) semestre_id_labels = [] for sid in semestre_id_list: - if sid == "-1": + if sid == -1: semestre_id_labels.append("pas de semestres") else: semestre_id_labels.append(f"S{sid}") @@ -329,6 +332,8 @@ def do_formsemestre_createwithmodules(edit=False): "labels": modalites_titles, }, ), + ] + modform.append( ( "semestre_id", { @@ -338,7 +343,7 @@ def do_formsemestre_createwithmodules(edit=False): "labels": semestre_id_labels, }, ), - ] + ) etapes = sco_portal_apogee.get_etapes_apogee_dept() # Propose les etapes renvoyées par le portail # et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement) diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 8cf8c9bec..4c4734910 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -93,6 +93,7 @@ MODULE_TYPE_NAMES = { ModuleType.MALUS: "Malus", ModuleType.RESSOURCE: "Ressource", ModuleType.SAE: "SAÉ", + None: "Module", } MALUS_MAX = 20.0 diff --git a/app/templates/pn/form_ues.html b/app/templates/pn/form_ues.html index cc9812dc5..500ee42aa 100644 --- a/app/templates/pn/form_ues.html +++ b/app/templates/pn/form_ues.html @@ -24,14 +24,11 @@ {{icons.arrow_none|safe}} {% endif %} - {% if editable and not ue.modules.count() %} + {{icons.delete|safe}} - {% else %} - {{icons.delete_disabled|safe}} - {% endif %} - + }}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %} + {{ue.acronyme}} {{ue.titre}} diff --git a/app/templates/sco_value_error.html b/app/templates/sco_value_error.html index 36ed8d206..8a54f6192 100644 --- a/app/templates/sco_value_error.html +++ b/app/templates/sco_value_error.html @@ -8,10 +8,9 @@ {{ exc | safe }} -{% if g.scodoc_dept %} - retour page d'accueil - departement {{ g.scodoc_dept }} + continuer {% else %} retour page d'accueil {% endif %} diff --git a/app/templates/scodoc.html b/app/templates/scodoc.html index 2b8255e51..6ac10dbd6 100644 --- a/app/templates/scodoc.html +++ b/app/templates/scodoc.html @@ -13,11 +13,21 @@