From 12f646547e6009585bdbb1fd1d6dfbec816424be Mon Sep 17 00:00:00 2001 From: Jean-Marie PLACE Date: Tue, 21 Dec 2021 18:49:33 +0100 Subject: [PATCH 01/15] api list logos --- app/api/sco_api.py | 81 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/app/api/sco_api.py b/app/api/sco_api.py index 969753d1..dd08c744 100644 --- a/app/api/sco_api.py +++ b/app/api/sco_api.py @@ -38,18 +38,18 @@ # Scolarite/Notes/groups_view # Scolarite/Notes/moduleimpl_status # Scolarite/setGroups +from datetime import datetime -from flask import jsonify, request, url_for, abort, g -from flask_login import current_user +from flask import jsonify, request, g, send_file from sqlalchemy.sql import func from app import db, log from app.api import bp from app.api.auth import token_auth -from app.api.errors import bad_request, error_response -from app.decorators import permission_required +from app.api.errors import error_response from app import models from app.models import FormSemestre, FormSemestreInscription, Identite +from app.scodoc.sco_logos import list_logos, find_logo from app.scodoc.sco_permissions import Permission @@ -79,3 +79,76 @@ def etudiants(): FormSemestre.date_fin >= func.now(), ) return jsonify([e.to_dict_bul(include_urls=False) for e in query]) + + +def format_required(default_format="json", allowed_formats=None): + """Extract required format from a request. + * default value is json. a list of allowed formats may be provided + (['json'] considered if not provided). + * if the required format is not in allowed list, returns None. + NB: if json in not in allowed_formats, format specification is mandatory.""" + format_type = request.args.get("format", default_format) + if format_type in (allowed_formats or ["json"]): + return format_type + return None + + +def get_dept_id(dept_name=None): + dept = models.Departement.query.filter_by(acronym=dept_name).first_or_404() + return dept.id + + +@bp.route("/logos", methods=["GET"]) +@token_auth.login_required +def api_get_glob_logos(): + if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): + return error_response(401, message="accès interdit") + required_format = format_required() # json only + if required_format is None: + return error_response(400, "Illegal format") + logos = list_logos()[None] + return jsonify(list(logos.keys())) + + +@bp.route("/logos/", methods=["GET"]) +@token_auth.login_required +def api_get_glob_logo(logoname): + if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): + return error_response(401, message="accès interdit") + logo = find_logo(logoname=logoname) + if logo is None: + return error_response(404, message="logo not found") + logo.select() + return send_file( + logo.filepath, + mimetype=f"image/{logo.suffix}", + last_modified=datetime.now(), + ) + + +@bp.route("/departements//logos", methods=["GET"]) +@token_auth.login_required +def api_get_local_logos(departement): + dept_id = get_dept_id(departement) + if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): + return error_response(401, message="accès interdit") + logos = list_logos().get(dept_id, dict()) + return jsonify(list(logos.keys())) + + +@bp.route("/departements//logos/", methods=["GET"]) +@token_auth.login_required +def api_get_local_logo(departement, logoname): + # format = format_required("jpg", ['png', 'jpg']) + dept_id = get_dept_id(departement) + if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): + return error_response(401, message="accès interdit") + logo = find_logo(logoname=logoname, dept_id=dept_id) + if logo is None: + return error_response(404, message="logo not found") + logo.select() + return send_file( + logo.filepath, + mimetype=f"image/{logo.suffix}", + last_modified=datetime.now(), + ) From 3631719f54eb373944cc99d54e1096a7f77fe75a Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Tue, 21 Dec 2021 23:05:49 +0100 Subject: [PATCH 02/15] fix #238 --- app/scodoc/sco_abs_views.py | 15 ++++++++++----- app/scodoc/sco_evaluations.py | 2 +- app/views/absences.py | 6 ++++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/scodoc/sco_abs_views.py b/app/scodoc/sco_abs_views.py index b21cb8c0..4e5aaa46 100644 --- a/app/scodoc/sco_abs_views.py +++ b/app/scodoc/sco_abs_views.py @@ -30,7 +30,7 @@ """ import datetime -from flask import url_for, g, request +from flask import url_for, g, request, abort import app.scodoc.sco_utils as scu from app.scodoc import notesdb as ndb @@ -773,7 +773,8 @@ def CalAbs(etudid, sco_year=None): def ListeAbsEtud( - etudid, + etudid=None, + code_nip=None, with_evals=True, format="html", absjust_only=0, @@ -793,9 +794,13 @@ def ListeAbsEtud( # si absjust_only, table absjust seule (export xls ou pdf) absjust_only = ndb.bool_or_str(absjust_only) datedebut = "%s-08-01" % scu.AnneeScolaire(sco_year=sco_year) - - etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] - + etudid = etudid or False + etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True) + if not etuds: + log(f"ListeAbsEtud: no etuds with etudid={etudid} or nip={code_nip}") + abort(404) + etud = etuds[0] + etudid = etud["etudid"] # Liste des absences et titres colonnes tables: titles, columns_ids, absnonjust, absjust = _tables_abs_etud( etudid, datedebut, with_evals=with_evals, format=format diff --git a/app/scodoc/sco_evaluations.py b/app/scodoc/sco_evaluations.py index 7d6bb82b..e06e6b54 100644 --- a/app/scodoc/sco_evaluations.py +++ b/app/scodoc/sco_evaluations.py @@ -98,7 +98,7 @@ def ListMedian(L): def do_evaluation_etat(evaluation_id, partition_id=None, select_first_partition=False): - """donne infos sur l'etat du evaluation + """donne infos sur l'état de l'évaluation { nb_inscrits, nb_notes, nb_abs, nb_neutre, nb_att, moyenne, mediane, mini, maxi, date_last_modif, gr_complets, gr_incomplets, evalcomplete } diff --git a/app/views/absences.py b/app/views/absences.py index a475988b..a3f89351 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -251,14 +251,16 @@ sco_publish( @permission_required_compat_scodoc7(Permission.ScoView) @scodoc7func def ListeAbsEtud( - etudid, + etudid=None, + code_nip=None, with_evals=True, format="html", absjust_only=0, sco_year=None, ): return sco_abs_views.ListeAbsEtud( - etudid, + etudid=etudid, + code_nip=str(code_nip), with_evals=with_evals, format=format, absjust_only=absjust_only, From 8db9a027cbdfcc60cd2dc5799e3ce0e2d70ae85c Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 22 Dec 2021 00:35:58 +0100 Subject: [PATCH 03/15] API logos / reorganise code --- app/api/__init__.py | 19 +++++++++- app/api/sco_api.py | 76 +------------------------------------- app/models/departements.py | 5 +++ 3 files changed, 24 insertions(+), 76 deletions(-) diff --git a/app/api/__init__.py b/app/api/__init__.py index 956c1b46..d397a93d 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -2,8 +2,25 @@ """ from flask import Blueprint +from flask import request bp = Blueprint("api", __name__) -from app.api import sco_api + +def requested_format(default_format="json", allowed_formats=None): + """Extract required format from query string. + * default value is json. A list of allowed formats may be provided + (['json'] considered if not provided). + * if the required format is not in allowed list, returns None. + + NB: if json in not in allowed_formats, format specification is mandatory. + """ + format_type = request.args.get("format", default_format) + if format_type in (allowed_formats or ["json"]): + return format_type + return None + + from app.api import tokens +from app.api import sco_api +from app.api import logos diff --git a/app/api/sco_api.py b/app/api/sco_api.py index dd08c744..6aa488c2 100644 --- a/app/api/sco_api.py +++ b/app/api/sco_api.py @@ -44,12 +44,11 @@ from flask import jsonify, request, g, send_file from sqlalchemy.sql import func from app import db, log -from app.api import bp +from app.api import bp, requested_format from app.api.auth import token_auth from app.api.errors import error_response from app import models from app.models import FormSemestre, FormSemestreInscription, Identite -from app.scodoc.sco_logos import list_logos, find_logo from app.scodoc.sco_permissions import Permission @@ -79,76 +78,3 @@ def etudiants(): FormSemestre.date_fin >= func.now(), ) return jsonify([e.to_dict_bul(include_urls=False) for e in query]) - - -def format_required(default_format="json", allowed_formats=None): - """Extract required format from a request. - * default value is json. a list of allowed formats may be provided - (['json'] considered if not provided). - * if the required format is not in allowed list, returns None. - NB: if json in not in allowed_formats, format specification is mandatory.""" - format_type = request.args.get("format", default_format) - if format_type in (allowed_formats or ["json"]): - return format_type - return None - - -def get_dept_id(dept_name=None): - dept = models.Departement.query.filter_by(acronym=dept_name).first_or_404() - return dept.id - - -@bp.route("/logos", methods=["GET"]) -@token_auth.login_required -def api_get_glob_logos(): - if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): - return error_response(401, message="accès interdit") - required_format = format_required() # json only - if required_format is None: - return error_response(400, "Illegal format") - logos = list_logos()[None] - return jsonify(list(logos.keys())) - - -@bp.route("/logos/", methods=["GET"]) -@token_auth.login_required -def api_get_glob_logo(logoname): - if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): - return error_response(401, message="accès interdit") - logo = find_logo(logoname=logoname) - if logo is None: - return error_response(404, message="logo not found") - logo.select() - return send_file( - logo.filepath, - mimetype=f"image/{logo.suffix}", - last_modified=datetime.now(), - ) - - -@bp.route("/departements//logos", methods=["GET"]) -@token_auth.login_required -def api_get_local_logos(departement): - dept_id = get_dept_id(departement) - if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): - return error_response(401, message="accès interdit") - logos = list_logos().get(dept_id, dict()) - return jsonify(list(logos.keys())) - - -@bp.route("/departements//logos/", methods=["GET"]) -@token_auth.login_required -def api_get_local_logo(departement, logoname): - # format = format_required("jpg", ['png', 'jpg']) - dept_id = get_dept_id(departement) - if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): - return error_response(401, message="accès interdit") - logo = find_logo(logoname=logoname, dept_id=dept_id) - if logo is None: - return error_response(404, message="logo not found") - logo.select() - return send_file( - logo.filepath, - mimetype=f"image/{logo.suffix}", - last_modified=datetime.now(), - ) diff --git a/app/models/departements.py b/app/models/departements.py index aa9c1006..95167383 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -42,3 +42,8 @@ class Departement(db.Model): "date_creation": self.date_creation, } return data + + @classmethod + def from_acronym(cls, acronym): + dept = cls.query.filter_by(acronym=acronym).first_or_404() + return dept From d86bb3e9b70cdaa4ec65015139882b987d84c25f Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 22 Dec 2021 10:23:17 +0100 Subject: [PATCH 04/15] =?UTF-8?q?le=20fichier=20oubli=C3=A9=20en=209.1.13?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/logos.py | 97 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 app/api/logos.py diff --git a/app/api/logos.py b/app/api/logos.py new file mode 100644 index 00000000..4fdb1099 --- /dev/null +++ b/app/api/logos.py @@ -0,0 +1,97 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# Gestion scolarite IUT +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +"""API: gestion des logos +Contrib @jmp +""" + +from datetime import datetime +from flask import jsonify, g, send_file + +from app.api import bp +from app.api import requested_format +from app.api.auth import token_auth +from app.api.errors import error_response +from app.models import Departement +from app.scodoc.sco_logos import list_logos, find_logo +from app.scodoc.sco_permissions import Permission + + +@bp.route("/logos", methods=["GET"]) +@token_auth.login_required +def api_get_glob_logos(): + if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): + return error_response(401, message="accès interdit") + required_format = requested_format() # json only + if required_format is None: + return error_response(400, "Illegal format") + logos = list_logos()[None] + return jsonify(list(logos.keys())) + + +@bp.route("/logos/", methods=["GET"]) +@token_auth.login_required +def api_get_glob_logo(logoname): + if not g.current_user.has_permission(Permission.ScoSuperAdmin, None): + return error_response(401, message="accès interdit") + logo = find_logo(logoname=logoname) + if logo is None: + return error_response(404, message="logo not found") + logo.select() + return send_file( + logo.filepath, + mimetype=f"image/{logo.suffix}", + last_modified=datetime.now(), + ) + + +@bp.route("/departements//logos", methods=["GET"]) +@token_auth.login_required +def api_get_local_logos(departement): + dept_id = Departement.from_acronym(departement).id + if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): + return error_response(401, message="accès interdit") + logos = list_logos().get(dept_id, dict()) + return jsonify(list(logos.keys())) + + +@bp.route("/departements//logos/", methods=["GET"]) +@token_auth.login_required +def api_get_local_logo(departement, logoname): + # format = requested_format("jpg", ['png', 'jpg']) XXX ? + dept_id = Departement.from_acronym(departement).id + if not g.current_user.has_permission(Permission.ScoChangePreferences, departement): + return error_response(401, message="accès interdit") + logo = find_logo(logoname=logoname, dept_id=dept_id) + if logo is None: + return error_response(404, message="logo not found") + logo.select() + return send_file( + logo.filepath, + mimetype=f"image/{logo.suffix}", + last_modified=datetime.now(), + ) From 799245b265ae5212685f4a0a5f61088811261a2b Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 22 Dec 2021 10:30:35 +0100 Subject: [PATCH 05/15] version 9.1.13 --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index 8baaca5e..811be249 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.12" +SCOVERSION = "9.1.13" SCONAME = "ScoDoc" From 9eb2c2462b5af2b3bd540c84c686dc35dc7833fe Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 22 Dec 2021 13:13:01 +0100 Subject: [PATCH 06/15] liens marge gauche --- app/scodoc/html_sidebar.py | 4 +++- app/templates/sidebar_dept.html | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/scodoc/html_sidebar.py b/app/scodoc/html_sidebar.py index a96da1a3..c35f3042 100644 --- a/app/scodoc/html_sidebar.py +++ b/app/scodoc/html_sidebar.py @@ -39,8 +39,10 @@ from app.scodoc.sco_permissions import Permission def sidebar_common(): "partie commune à toutes les sidebar" + home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept) H = [ - f"""ScoDoc 9.1 + f"""ScoDoc 9.1
+ Accueil
Dépt. {{ prefs["DeptName"] }} -Accueil
+

Dépt. {{ prefs["DeptName"] }} +

{% if prefs["DeptIntranetURL"] %} - + {{ prefs["DeptIntranetTitle"] }} {% endif %}
From d12db963895cf274d8b97d0b6b2e4b854199e2be Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 22 Dec 2021 14:31:44 +0100 Subject: [PATCH 07/15] typo --- app/views/scodoc.py | 2 +- sco_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/scodoc.py b/app/views/scodoc.py index c098f034..dfd8bcfe 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -134,7 +134,7 @@ def get_etud_dept(): last_etud = None last_date = None for etud in etuds: - inscriptions = FormsemestreInscription.query.filter_by(etudid=etud.id).all() + inscriptions = FormSemestreInscription.query.filter_by(etudid=etud.id).all() for ins in inscriptions: date_fin = FormSemestre.query.get(ins.formsemestre_id).date_fin if (last_date is None) or date_fin > last_date: diff --git a/sco_version.py b/sco_version.py index 811be249..e2e05e13 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.13" +SCOVERSION = "9.1.14" SCONAME = "ScoDoc" From 429820b7865f4747476f089a1ef13d4f11324a62 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Dec 2021 00:18:05 +0100 Subject: [PATCH 08/15] 9.1.15 --- sco_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sco_version.py b/sco_version.py index e2e05e13..3de53222 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.14" +SCOVERSION = "9.1.15" SCONAME = "ScoDoc" From 2d2b2b2f39e29662acc1d3ebe3e41e1486c39ea3 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Thu, 23 Dec 2021 16:03:30 +0100 Subject: [PATCH 09/15] =?UTF-8?q?Form=20cr=C3=A9ation=20dept=20+=20d=C3=A9?= =?UTF-8?q?place=20form=20logos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/config_forms.py} | 3 +- app/forms/main/create_dept.py | 64 +++++++++++++++++++ app/models/departements.py | 12 ++++ app/templates/create_dept.html | 14 ++++ app/templates/scodoc.html | 3 + app/views/scodoc.py | 27 ++++++-- sco_version.py | 2 +- scodoc.py | 7 +- 8 files changed, 121 insertions(+), 11 deletions(-) rename app/{scodoc/sco_config_form.py => forms/main/config_forms.py} (99%) create mode 100644 app/forms/main/create_dept.py create mode 100644 app/templates/create_dept.html diff --git a/app/scodoc/sco_config_form.py b/app/forms/main/config_forms.py similarity index 99% rename from app/scodoc/sco_config_form.py rename to app/forms/main/config_forms.py index 993382de..609066c5 100644 --- a/app/scodoc/sco_config_form.py +++ b/app/forms/main/config_forms.py @@ -89,7 +89,8 @@ CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS # - only one operation found: execute and go to main page # - more than 1 operation found. asked form confirmation (and execution if confirmed) # -# Someday we'll have time to refactor as abstract classes but Abstract FieldList makes this a bit complicated +# Someday we'll have time to refactor as abstract classes but Abstract FieldList makes this +# a bit complicated # """ # Terminology: diff --git a/app/forms/main/create_dept.py b/app/forms/main/create_dept.py new file mode 100644 index 00000000..11b36129 --- /dev/null +++ b/app/forms/main/create_dept.py @@ -0,0 +1,64 @@ +# -*- mode: python -*- +# -*- coding: utf-8 -*- + +############################################################################## +# +# ScoDoc +# +# Copyright (c) 1999 - 2021 Emmanuel Viennet. All rights reserved. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Emmanuel Viennet emmanuel.viennet@viennet.net +# +############################################################################## + +""" +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 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): + """Formulaire permettant l'ajout d'un département""" + + acronym = StringField( + label="Acronyme", + validators=[ + validators.regexp( + r"^[a-zA-Z0-9_\-]*$", + message="Ne doit comporter que lettres, chiffres ou -", + ), + validators.Length( + max=SHORT_STR_LEN, + message=f"L'acronyme ne doit pas dépasser {SHORT_STR_LEN} caractères", + ), + validators.DataRequired("acronyme du département requis"), + ], + ) + submit = SubmitField("Valider") + cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) diff --git a/app/models/departements.py b/app/models/departements.py index 95167383..0734e35b 100644 --- a/app/models/departements.py +++ b/app/models/departements.py @@ -47,3 +47,15 @@ class Departement(db.Model): def from_acronym(cls, acronym): dept = cls.query.filter_by(acronym=acronym).first_or_404() return dept + + +def create_dept(acronym: str) -> Departement: + "Create new departement" + from app.models import ScoPreference + + departement = Departement(acronym=acronym) + p1 = ScoPreference(name="DeptName", value=acronym, departement=departement) + db.session.add(p1) + db.session.add(departement) + db.session.commit() + return departement diff --git a/app/templates/create_dept.html b/app/templates/create_dept.html new file mode 100644 index 00000000..4e3f8a46 --- /dev/null +++ b/app/templates/create_dept.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% import 'bootstrap/wtf.html' as wtf %} + +{% block app_content %} +

Créer un département

+ +
+
+ {{ wtf.quick_form(form) }} +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/app/templates/scodoc.html b/app/templates/scodoc.html index c0e1eddd..2aaf9e22 100644 --- a/app/templates/scodoc.html +++ b/app/templates/scodoc.html @@ -22,6 +22,9 @@ Aucun département défini ! {% endfor %} + {% if current_user.is_administrator() %} +
  • créer un nouveau département
  • + {% endif %} diff --git a/app/views/scodoc.py b/app/views/scodoc.py index dfd8bcfe..815de175 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -53,11 +53,13 @@ from wtforms.fields.simple import BooleanField, StringField, TextAreaField, Hidd from wtforms.validators import ValidationError, DataRequired, Email, EqualTo import app +from app.forms.main import config_forms +from app.forms.main.create_dept import CreateDeptForm from app.models import Departement, Identite +from app.models import departements from app.models import FormSemestre, FormSemestreInscription -from app.models import ScoDocSiteConfig import sco_version -from app.scodoc import sco_logos, sco_config_form +from app.scodoc import sco_logos from app.scodoc import sco_find_etud from app.scodoc import sco_utils as scu from app.decorators import ( @@ -67,7 +69,6 @@ from app.decorators import ( permission_required_compat_scodoc7, permission_required, ) -from app.scodoc.sco_config_form import configuration from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_logos import find_logo from app.scodoc.sco_permissions import Permission @@ -99,6 +100,24 @@ def index_dept(scodoc_dept): return redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept)) +@bp.route("/ScoDoc/create_dept", methods=["GET", "POST"]) +@admin_required +def create_dept(): + """Form création département""" + form = CreateDeptForm() + if request.method == "POST" and form.cancel.data: # cancel button + return redirect(url_for("scodoc.index")) + if form.validate_on_submit(): + departements.create_dept(form.acronym.data) + flash(f"Département {form.acronym.data} créé.") + return redirect(url_for("scodoc.index")) + return render_template( + "create_dept.html", + form=form, + title="Création d'un nouveau département", + ) + + @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"]) @login_required def table_etud_in_accessible_depts(): @@ -207,7 +226,7 @@ def configuration(): auth_name = str(current_user) if not current_user.is_administrator(): raise AccessDenied("invalid user (%s) must be SuperAdmin" % auth_name) - return sco_config_form.configuration() + return config_forms.configuration() SMALL_SIZE = (200, 200) diff --git a/sco_version.py b/sco_version.py index 3de53222..bc906576 100644 --- a/sco_version.py +++ b/sco_version.py @@ -1,7 +1,7 @@ # -*- mode: python -*- # -*- coding: utf-8 -*- -SCOVERSION = "9.1.15" +SCOVERSION = "9.1.16" SCONAME = "ScoDoc" diff --git a/scodoc.py b/scodoc.py index 482501e0..94c8a7f0 100755 --- a/scodoc.py +++ b/scodoc.py @@ -27,6 +27,7 @@ from app.models import Formation, UniteEns, Module from app.models import FormSemestre, FormSemestreInscription from app.models import ModuleImpl, ModuleImplInscription from app.models import Identite +from app.models import departements from app.models.evaluations import Evaluation from app.scodoc.sco_etud import identite_create from app.scodoc.sco_permissions import Permission @@ -306,11 +307,7 @@ def delete_dept(dept): # delete-dept @click.argument("dept") def create_dept(dept): # create-dept "Create new departement" - d = models.Departement(acronym=dept) - p1 = ScoPreference(name="DeptName", value=dept, departement=d) - db.session.add(p1) - db.session.add(d) - db.session.commit() + _ = departements.create_dept(dept) return 0 From 9cd31e66f05ecfd922db503935f34633ae073059 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 29 Dec 2021 11:26:54 +0100 Subject: [PATCH 10/15] =?UTF-8?q?Edition/cr=C3=A9eation=20utilisateurs:=20?= =?UTF-8?q?choix=20d=C3=A9partement=20selon=20les=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/auth/models.py | 18 +++++++- app/views/users.py | 110 ++++++++++++++++++++++++++++++--------------- 2 files changed, 90 insertions(+), 38 deletions(-) diff --git a/app/auth/models.py b/app/auth/models.py index d9c5455b..bc4edb65 100644 --- a/app/auth/models.py +++ b/app/auth/models.py @@ -19,7 +19,8 @@ from werkzeug.security import generate_password_hash, check_password_hash import jwt from app import db, login - +from app.models import Departement +from app.models import SHORT_STR_LEN from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_permissions import Permission from app.scodoc.sco_roles_default import SCO_ROLES_DEFAULTS @@ -55,7 +56,7 @@ class User(UserMixin, db.Model): nom = db.Column(db.String(64)) prenom = db.Column(db.String(64)) - dept = db.Column(db.String(32), index=True) + dept = db.Column(db.String(SHORT_STR_LEN), index=True) active = db.Column(db.Boolean, default=True, index=True) password_hash = db.Column(db.String(128)) @@ -71,6 +72,13 @@ class User(UserMixin, db.Model): roles = db.relationship("Role", secondary="user_role", viewonly=True) Permission = Permission + _departement = db.relationship( + "Departement", + foreign_keys=[Departement.acronym], + primaryjoin=(dept == Departement.acronym), + lazy="dynamic", + ) + def __init__(self, **kwargs): self.roles = [] self.user_roles = [] @@ -221,6 +229,12 @@ class User(UserMixin, db.Model): return None return user + def get_dept_id(self) -> int: + "returns user's department id, or None" + if self.dept: + return self._departement.first().id + return None + # Permissions management: def has_permission(self, perm: int, dept=False): """Check if user has permission `perm` in given `dept`. diff --git a/app/views/users.py b/app/views/users.py index b54cbf0c..f14ecf2a 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -167,10 +167,10 @@ def create_user_form(user_name=None, edit=0, all_roles=1): if edit: if not user_name: raise ValueError("missing argument: user_name") - u = User.query.filter_by(user_name=user_name).first() - if not u: + the_user = User.query.filter_by(user_name=user_name).first() + if not the_user: raise ScoValueError("utilisateur inexistant") - initvalues = u.to_dict() + initvalues = the_user.to_dict() H.append("

    Modification de l'utilisateur %s

    " % user_name) else: H.append("

    Création d'un utilisateur

    ") @@ -197,11 +197,11 @@ def create_user_form(user_name=None, edit=0, all_roles=1): # sinon, les départements dans lesquels l'utilisateur a le droit if is_super_admin: log("create_user_form called by %s (super admin)" % (current_user.user_name,)) - dept_ids = [d.acronym for d in Departement.query.all()] + administrable_dept_acronyms = [d.acronym for d in Departement.query.all()] else: # Si on n'est pas SuperAdmin, liste les départements dans lesquels on a la # permission ScoUsersAdmin - dept_ids = sorted( + administrable_dept_acronyms = sorted( set( [ x.dept @@ -211,7 +211,9 @@ def create_user_form(user_name=None, edit=0, all_roles=1): ) ) - editable_roles_set = {(r, dept) for r in standard_roles for dept in dept_ids} + editable_roles_set = { + (r, dept) for r in standard_roles for dept in administrable_dept_acronyms + } # if not edit: submitlabel = "Créer utilisateur" @@ -224,9 +226,11 @@ def create_user_form(user_name=None, edit=0, all_roles=1): initvalues["roles"] = [] if "date_expiration" in initvalues: initvalues["date_expiration"] = ( - u.date_expiration.strftime("%d/%m/%Y") if u.date_expiration else "" + the_user.date_expiration.strftime("%d/%m/%Y") + if the_user.date_expiration + else "" ) - initvalues["status"] = "" if u.active else "old" + initvalues["status"] = "" if the_user.active else "old" orig_roles = { # set des roles existants avant édition UserRole.role_dept_from_string(role_dept) for role_dept in initvalues["roles"] @@ -347,11 +351,12 @@ def create_user_form(user_name=None, edit=0, all_roles=1): }, ), ] - + # Si auth n'a pas de departement (admin global) + # propose de choisir librement le dept du nouvel utilisateur + # sinon, menu proposant l'ensembe des départements dans lesquels + # nous avons la permission ScoUserAdmin + le dept actuel de l'utilisateur + # modifié. if not auth_dept: - # si auth n'a pas de departement (admin global) - # propose de choisir le dept du nouvel utilisateur - # sinon, il sera créé dans le même département que auth descr.append( ( "dept", @@ -360,35 +365,57 @@ def create_user_form(user_name=None, edit=0, all_roles=1): "input_type": "text", "size": 12, "allow_null": True, - "explanation": """département d\'appartenance de l\'utilisateur (s'il s'agit d'un administrateur, laisser vide si vous voulez qu'il puisse créer des utilisateurs dans d'autres départements)""", + "explanation": """département de rattachement de l'utilisateur + (s'il s'agit d'un administrateur, laisser vide si vous voulez + qu'il puisse créer des utilisateurs dans d'autres départements) + """, }, ) ) can_choose_dept = True else: - can_choose_dept = False - if edit: + selectable_dept_acronyms = set(administrable_dept_acronyms) + if edit: # ajoute dept actuel de l'utilisateur + selectable_dept_acronyms |= {the_user.dept} + if len(selectable_dept_acronyms) > 1: + can_choose_dept = True + selectable_dept_acronyms = sorted(list(selectable_dept_acronyms)) descr.append( ( - "d", + "dept", { - "input_type": "separator", - "title": "L'utilisateur appartient au département %s" - % auth_dept, + "title": "Département", + "input_type": "menu", + "explanation": """département de rattachement de l'utilisateur""", + "labels": selectable_dept_acronyms, + "allowed_values": selectable_dept_acronyms, }, ) ) - else: - descr.append( - ( - "d", - { - "input_type": "separator", - "title": "L'utilisateur sera crée dans le département %s" - % auth_dept, - }, + else: # pas de choix de département + can_choose_dept = False + if edit: + descr.append( + ( + "d", + { + "input_type": "separator", + "title": "L'utilisateur appartient au département %s" + % auth_dept, + }, + ) + ) + else: + descr.append( + ( + "d", + { + "input_type": "separator", + "title": "L'utilisateur sera crée dans le département %s" + % auth_dept, + }, + ) ) - ) descr += [ ( @@ -512,6 +539,10 @@ def create_user_form(user_name=None, edit=0, all_roles=1): del vals["status"] # no one can't change its own status if "status" in vals: vals["active"] = vals["status"] == "" + # Département: + if auth_dept: # pas super-admin + if vals["dept"] not in selectable_dept_acronyms: + del vals["dept"] # ne change pas de dept # traitement des roles: ne doit pas affecter les roles # que l'on en controle pas: for role in orig_roles_strings: # { "Ens_RT", "Secr_CJ", ... } @@ -565,30 +596,37 @@ def create_user_form(user_name=None, edit=0, all_roles=1): """Mot de passe trop simple, recommencez !""" ) return "\n".join(H) + msg + "\n" + tf[1] + F + # Département: if not can_choose_dept: vals["dept"] = auth_dept + else: + if auth_dept: # pas super-admin + if vals["dept"] not in selectable_dept_acronyms: + raise ScoValueError("département invalide") # ok, go log( "sco_users: new_user %s by %s" % (vals["user_name"], current_user.user_name) ) - u = User() - u.from_dict(vals, new_user=True) - db.session.add(u) + the_user = User() + the_user.from_dict(vals, new_user=True) + db.session.add(the_user) db.session.commit() # envoi éventuel d'un message if mode == Mode.WELCOME_AND_CHANGE_PASSWORD or mode == Mode.WELCOME_ONLY: if mode == Mode.WELCOME_AND_CHANGE_PASSWORD: - token = u.get_reset_password_token() + token = the_user.get_reset_password_token() else: token = None send_email( "[ScoDoc] Création de votre compte", sender=from_mail, # current_app.config["ADMINS"][0], - recipients=[u.email], - text_body=render_template("email/welcome.txt", user=u, token=token), + recipients=[the_user.email], + text_body=render_template( + "email/welcome.txt", user=the_user, token=token + ), html_body=render_template( - "email/welcome.html", user=u, token=token + "email/welcome.html", user=the_user, token=token ), ) From 9b5b4777e25a2d02f9c4460b9af005452f3c2591 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Wed, 29 Dec 2021 19:30:49 +0100 Subject: [PATCH 11/15] Messages erreurs quand saisie champs trop longs --- app/models/absences.py | 3 --- app/models/entreprises.py | 3 --- app/models/etudiants.py | 3 --- app/models/evaluations.py | 3 --- app/models/events.py | 2 -- app/models/groups.py | 2 -- app/models/notes.py | 1 - app/scodoc/notesdb.py | 5 ++++- app/scodoc/sco_edit_formation.py | 2 ++ app/scodoc/sco_edit_module.py | 12 +++++++----- app/scodoc/sco_edit_ue.py | 2 ++ app/scodoc/sco_formsemestre_edit.py | 3 +++ app/scodoc/sco_groups.py | 6 +++++- app/static/js/groupmgr.js | 4 ++++ app/templates/scolar/affect_groups.html | 2 +- 15 files changed, 28 insertions(+), 25 deletions(-) diff --git a/app/models/absences.py b/app/models/absences.py index 658a390d..bbff2c5c 100644 --- a/app/models/absences.py +++ b/app/models/absences.py @@ -4,9 +4,6 @@ """ from app import db -from app.models import APO_CODE_STR_LEN -from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN class Absence(db.Model): diff --git a/app/models/entreprises.py b/app/models/entreprises.py index bdb5672a..c5d05e93 100644 --- a/app/models/entreprises.py +++ b/app/models/entreprises.py @@ -4,9 +4,6 @@ """ from app import db -from app.models import APO_CODE_STR_LEN -from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN class Entreprise(db.Model): diff --git a/app/models/etudiants.py b/app/models/etudiants.py index 0ae36bd2..9c8457fa 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -8,9 +8,6 @@ from flask import g, url_for from app import db from app import models -from app.models import APO_CODE_STR_LEN -from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN class Identite(db.Model): diff --git a/app/models/evaluations.py b/app/models/evaluations.py index e0733fb7..4f06fb75 100644 --- a/app/models/evaluations.py +++ b/app/models/evaluations.py @@ -4,9 +4,6 @@ """ from app import db -from app.models import APO_CODE_STR_LEN -from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN from app.models import UniteEns import app.scodoc.notesdb as ndb diff --git a/app/models/events.py b/app/models/events.py index 0cdd7e93..55b34d38 100644 --- a/app/models/events.py +++ b/app/models/events.py @@ -4,9 +4,7 @@ """ from app import db -from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN class Scolog(db.Model): diff --git a/app/models/groups.py b/app/models/groups.py index 688744b0..902298cc 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -5,9 +5,7 @@ from typing import Any from app import db -from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN -from app.models import CODE_STR_LEN from app.models import GROUPNAME_STR_LEN diff --git a/app/models/notes.py b/app/models/notes.py index df2766d0..fa8dc8d1 100644 --- a/app/models/notes.py +++ b/app/models/notes.py @@ -4,7 +4,6 @@ """ from app import db -from app.models import APO_CODE_STR_LEN from app.models import SHORT_STR_LEN from app.models import CODE_STR_LEN diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py index 5418c3ce..3de5c281 100644 --- a/app/scodoc/notesdb.py +++ b/app/scodoc/notesdb.py @@ -265,8 +265,11 @@ def DBUpdateArgs(cnx, table, vals, where=None, commit=False, convert_empty_to_nu cursor.execute(req, vals) # log('req=%s\n'%req) # log('vals=%s\n'%vals) + except psycopg2.errors.StringDataRightTruncation: + cnx.rollback() + raise ScoValueError("champs de texte trop long !") except: - cnx.commit() # get rid of this transaction + cnx.rollback() # get rid of this transaction log('Exception in DBUpdateArgs:\n\treq="%s"\n\tvals="%s"\n' % (req, vals)) raise # and re-raise exception if commit: diff --git a/app/scodoc/sco_edit_formation.py b/app/scodoc/sco_edit_formation.py index bd01abae..4901f494 100644 --- a/app/scodoc/sco_edit_formation.py +++ b/app/scodoc/sco_edit_formation.py @@ -33,6 +33,7 @@ from flask import g, url_for, request from app import db from app import log +from app.models import SHORT_STR_LEN from app.models.formations import Formation from app.models.modules import Module @@ -209,6 +210,7 @@ def formation_edit(formation_id=None, create=False): "size": 12, "title": "Code formation", "explanation": "code interne. Toutes les formations partageant le même code sont compatibles (compensation de semestres, capitalisation d'UE). Laisser vide si vous ne savez pas, ou entrer le code d'une formation existante.", + "validator": lambda val, _: len(val) < SHORT_STR_LEN, }, ), ( diff --git a/app/scodoc/sco_edit_module.py b/app/scodoc/sco_edit_module.py index 24232164..ee87f7e7 100644 --- a/app/scodoc/sco_edit_module.py +++ b/app/scodoc/sco_edit_module.py @@ -32,6 +32,7 @@ import flask from flask import url_for, render_template from flask import g, request from flask_login import current_user +from app.models import APO_CODE_STR_LEN from app.models import Matiere, Module, UniteEns import app.scodoc.notesdb as ndb @@ -397,21 +398,21 @@ def module_delete(module_id=None): return flask.redirect(dest_url) -def do_module_edit(val): +def do_module_edit(vals: dict) -> None: "edit a module" from app.scodoc import sco_edit_formation # check - mod = module_list({"module_id": val["module_id"]})[0] + mod = module_list({"module_id": vals["module_id"]})[0] if module_is_locked(mod["module_id"]): # formation verrouillée: empeche de modifier certains champs: protected_fields = ("coefficient", "ue_id", "matiere_id", "semestre_id") for f in protected_fields: - if f in val: - del val[f] + if f in vals: + del vals[f] # edit cnx = ndb.GetDBConnexion() - _moduleEditor.edit(cnx, val) + _moduleEditor.edit(cnx, vals) Formation.query.get(mod["formation_id"]).invalidate_cached_sems() @@ -604,6 +605,7 @@ def module_edit(module_id=None): "title": "Code Apogée", "size": 25, "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", + "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, }, ), ( diff --git a/app/scodoc/sco_edit_ue.py b/app/scodoc/sco_edit_ue.py index cdc83a04..192ed8d3 100644 --- a/app/scodoc/sco_edit_ue.py +++ b/app/scodoc/sco_edit_ue.py @@ -33,6 +33,7 @@ from flask import url_for, render_template from flask import g, request from flask_login import current_user +from app.models import APO_CODE_STR_LEN from app.models import Formation, UniteEns, ModuleImpl, Module import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -324,6 +325,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None): "title": "Code Apogée", "size": 25, "explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", + "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, }, ), ( diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py index 2f9308f7..bf90fe26 100644 --- a/app/scodoc/sco_formsemestre_edit.py +++ b/app/scodoc/sco_formsemestre_edit.py @@ -33,6 +33,7 @@ from flask_login import current_user from app import db from app.auth.models import User +from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN from app.models import ModuleImpl, Evaluation, EvaluationUEPoids import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu @@ -359,6 +360,7 @@ def do_formsemestre_createwithmodules(edit=False): mf_manual = { "size": 12, "template": '%(label)s%(elem)s', + "validator": lambda val, _: len(val) < APO_CODE_STR_LEN, } if etapes: mf = { @@ -495,6 +497,7 @@ def do_formsemestre_createwithmodules(edit=False): "size": 8, "title": "Couleur fond des bulletins", "explanation": "version web seulement (ex: #ffeeee)", + "validator": lambda val, _: len(val) < SHORT_STR_LEN, }, ), ( diff --git a/app/scodoc/sco_groups.py b/app/scodoc/sco_groups.py index da46270d..44aa7797 100644 --- a/app/scodoc/sco_groups.py +++ b/app/scodoc/sco_groups.py @@ -45,6 +45,7 @@ from flask import g, request from flask import url_for, make_response from app import db +from app.models import GROUPNAME_STR_LEN, SHORT_STR_LEN from app.models.groups import Partition import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb @@ -870,10 +871,11 @@ def editPartitionForm(formsemestre_id=None): page_title="Partitions...", javascripts=["js/editPartitionForm.js"], ), + # limite à SHORT_STR_LEN r"""