From 486f20d7f787f6b97f1d96e55780e3229497a92d Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Fri, 13 Aug 2021 00:34:58 +0200 Subject: [PATCH] =?UTF-8?q?Base=20multi-d=C3=A9partements.=20En=20cours?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/__init__.py | 21 ++++++- app/decorators.py | 36 +++++++++--- app/models/__init__.py | 5 +- app/models/departements.py | 36 ++++++++++++ app/models/entreprises.py | 2 +- app/models/etudiants.py | 1 + app/models/formations.py | 6 ++ app/models/formsemestre.py | 5 ++ app/models/preferences.py | 9 +-- app/scodoc/notesdb.py | 36 +++++------- app/scodoc/sco_abs_notification.py | 5 +- app/scodoc/sco_cache.py | 21 ++++++- app/scodoc/sco_dump_db.py | 4 +- app/scodoc/sco_entreprises.py | 1 + app/scodoc/sco_etud.py | 1 + app/scodoc/sco_find_etud.py | 5 +- app/scodoc/sco_formations.py | 1 + app/scodoc/sco_formsemestre.py | 1 + app/scodoc/sco_preferences.py | 46 +++++++++++---- app/scodoc/sco_semset.py | 22 +++++-- app/scodoc/sco_tag_module.py | 8 +-- app/scodoc/sco_utils.py | 13 +---- app/views/__init__.py | 21 ++++--- app/views/absences.py | 17 +++++- app/views/essais.py | 8 ++- app/views/notes.py | 45 ++++++++++++++ app/views/scodoc.py | 2 +- app/views/scolar.py | 51 +++++++++++++++- app/views/users.py | 9 +++ scodoc.py | 28 ++++++--- tests/conftest.py | 9 ++- tests/unit/test_caches.py | 5 +- tests/unit/test_departements.py | 94 ++++++++++++++++++++++++++++++ tests/unit/test_sco_basic.py | 7 ++- 34 files changed, 471 insertions(+), 110 deletions(-) create mode 100644 app/models/departements.py create mode 100644 tests/unit/test_departements.py diff --git a/app/__init__.py b/app/__init__.py index 41d2b38f..754cfa88 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -23,9 +23,6 @@ from flask_caching import Cache from config import DevConfig -from app.scodoc import notesdb as ndb -from app.scodoc import sco_cache - db = SQLAlchemy() migrate = Migrate() login = LoginManager() @@ -131,6 +128,18 @@ def create_app(config_class=DevConfig): return app +def set_sco_dept(scodoc_dept: str): + """Set global g object to given dept and open db connection if needed""" + # Check that dept exists + dept = Departement.query.filter_by(acronym=scodoc_dept).first() + if not dept: + raise ScoValueError(f"Invalid dept: {scodoc_dept}") + g.scodoc_dept = scodoc_dept # l'acronyme + g.scodoc_dept_id = dept.id # l'id + if not hasattr(g, "db_conn"): + ndb.open_db_connection() + + def user_db_init(): """Initialize the users database.""" from app.auth.models import User, Role @@ -214,8 +223,14 @@ def clear_scodoc_cache(): r = redis.Redis() r.flushall() + # Also clear local caches: + sco_preferences.clear_base_preferences() +from app.models import Departement +from app.scodoc import notesdb as ndb, sco_preferences +from app.scodoc import sco_cache + # admin_role = Role.query.filter_by(name="SuperAdmin").first() # if admin_role: # admin = ( diff --git a/app/decorators.py b/app/decorators.py index 6944b28e..38bc9a6b 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -98,10 +98,37 @@ class ZResponse(object): self.headers[header.lower()] = value +def scodoc(func): + """Décorateur pour toutes les fonctions ScoDoc + Affecte le département à g + et ouvre la connexion à la base + + Set `g.scodoc_dept` and `g.scodoc_dept_id` if `scodoc_dept` is present + in the argument (for routes like + `//Scolarite/sco_exemple`). + """ + + @wraps(func) + def scodoc_function(*args, **kwargs): + if "scodoc_dept" in kwargs: + dept_acronym = kwargs["scodoc_dept"] + current_app.logger.info("setting dept to " + dept_acronym) + app.set_sco_dept(dept_acronym) + del kwargs["scodoc_dept"] + elif not hasattr(g, "scodoc_dept"): + current_app.logger.info("setting dept to None") + g.scodoc_dept = None + g.scodoc_dept_id = -1 # invalide + return func(*args, **kwargs) + + return scodoc_function + + def permission_required(permission): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): + # current_app.logger.info("PERMISSION; kwargs=%s" % str(kwargs)) if "scodoc_dept" in kwargs: g.scodoc_dept = kwargs["scodoc_dept"] del kwargs["scodoc_dept"] @@ -142,7 +169,6 @@ def scodoc7func(context): 2. or be called directly from Python. If called via a route, this decorator setups a REQUEST object (emulating Zope2 REQUEST) - and `g.scodoc_dept` if present in the argument (for routes like `//Scolarite/sco_exemple`). """ # Détermine si on est appelé via une route ("toplevel") # ou par un appel de fonction python normal. @@ -150,14 +176,6 @@ def scodoc7func(context): if not top_level: # ne "redécore" pas return func(*args, **kwargs) - # - if "scodoc_dept" in kwargs: - g.scodoc_dept = kwargs["scodoc_dept"] - del kwargs["scodoc_dept"] - elif not hasattr(g, "scodoc_dept"): - g.scodoc_dept = None - # --- Open DB connection - app.views.open_dept_db_connection() # --- Emulate Zope's REQUEST REQUEST = ZRequest() g.zrequest = REQUEST diff --git a/app/models/__init__.py b/app/models/__init__.py index 40f9ec92..88fa3774 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -11,6 +11,9 @@ APO_CODE_STR_LEN = 16 # nb de car max d'un code Apogée from app.models.raw_sql_init import create_database_functions from app.models.absences import Absence, AbsenceNotification, BilletAbsence + +from app.models.departements import Departement + from app.models.entreprises import ( Entreprise, EntrepriseCorrespondant, @@ -56,4 +59,4 @@ from app.models.notes import ( NotesNotes, NotesNotesLog, ) -from app.models.preferences import ScoPreferences +from app.models.preferences import ScoPreference diff --git a/app/models/departements.py b/app/models/departements.py new file mode 100644 index 00000000..50ec0a9f --- /dev/null +++ b/app/models/departements.py @@ -0,0 +1,36 @@ +# -*- coding: UTF-8 -* + +"""ScoDoc8 models : departements +""" +from typing import Any + +from app import db +from app.models import SHORT_STR_LEN + + +class Departement(db.Model): + """Un département ScoDoc""" + + id = db.Column(db.Integer, primary_key=True) + acronym = db.Column(db.String(SHORT_STR_LEN), nullable=False, index=True) + description = db.Column(db.Text()) + date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) + visible = db.Column( + db.Boolean(), nullable=False, default=True, server_default="true" + ) # sur page d'accueil + + entreprises = db.relationship("Entreprise", lazy="dynamic", backref="departement") + etudiants = db.relationship("Identite", lazy="dynamic", backref="departement") + formations = db.relationship( + "NotesFormation", lazy="dynamic", backref="departement" + ) + formsemestres = db.relationship( + "FormSemestre", lazy="dynamic", backref="departement" + ) + preferences = db.relationship( + "ScoPreference", lazy="dynamic", backref="departement" + ) + semsets = db.relationship("NotesSemSet", lazy="dynamic", backref="departement") + + def __repr__(self): + return f"" diff --git a/app/models/entreprises.py b/app/models/entreprises.py index 0e114bac..bdb5672a 100644 --- a/app/models/entreprises.py +++ b/app/models/entreprises.py @@ -15,7 +15,7 @@ class Entreprise(db.Model): __tablename__ = "entreprises" id = db.Column(db.Integer, primary_key=True) entreprise_id = db.synonym("id") - + dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) nom = db.Column(db.Text) adresse = db.Column(db.Text) ville = db.Column(db.Text) diff --git a/app/models/etudiants.py b/app/models/etudiants.py index e189ab59..fe179503 100644 --- a/app/models/etudiants.py +++ b/app/models/etudiants.py @@ -17,6 +17,7 @@ class Identite(db.Model): id = db.Column(db.Integer, primary_key=True) etudid = db.synonym("id") + dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) nom = db.Column(db.Text()) prenom = db.Column(db.Text()) diff --git a/app/models/formations.py b/app/models/formations.py index 65c7d06a..f40085b6 100644 --- a/app/models/formations.py +++ b/app/models/formations.py @@ -15,6 +15,8 @@ class NotesFormation(db.Model): id = db.Column(db.Integer, primary_key=True) formation_id = db.synonym("id") + dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) + acronyme = db.Column(db.String(SHORT_STR_LEN), nullable=False) titre = db.Column(db.Text(), nullable=False) titre_officiel = db.Column(db.Text(), nullable=False) @@ -28,6 +30,8 @@ class NotesFormation(db.Model): type_parcours = db.Column(db.Integer, default=0, server_default="0") code_specialite = db.Column(db.String(SHORT_STR_LEN)) + formsemestres = db.relationship("FormSemestre", lazy="dynamic", backref="formation") + class NotesUE(db.Model): """Unité d'Enseignement""" @@ -106,6 +110,8 @@ class NotesTag(db.Model): __tablename__ = "notes_tags" id = db.Column(db.Integer, primary_key=True) + dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) + tag_id = db.synonym("id") title = db.Column(db.String(SHORT_STR_LEN), nullable=False, unique=True) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index 4b881ac2..0aa3a5b0 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -19,6 +19,9 @@ class FormSemestre(db.Model): id = db.Column(db.Integer, primary_key=True) formsemestre_id = db.synonym("id") + # dept_id est aussi dans la formation, ajpouté ici pour + # simplifier et accélérer les selects dans notesdb + dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) formation_id = db.Column(db.Integer, db.ForeignKey("notes_formations.id")) semestre_id = db.Column(db.Integer, nullable=False, default=1, server_default="1") titre = db.Column(db.Text()) @@ -309,6 +312,8 @@ class NotesSemSet(db.Model): id = db.Column(db.Integer, primary_key=True) semset_id = db.synonym("id") + dept_id = db.Column(db.Integer, db.ForeignKey("departement.id")) + title = db.Column(db.Text) annee_scolaire = db.Column(db.Integer, nullable=True, default=None) # periode: 0 (année), 1 (Simpair), 2 (Spair) diff --git a/app/models/preferences.py b/app/models/preferences.py index 7d4f80b0..15a2ea5f 100644 --- a/app/models/preferences.py +++ b/app/models/preferences.py @@ -2,17 +2,18 @@ """Model : preferences """ - from app import db -class ScoPreferences(db.Model): +class ScoPreference(db.Model): """ScoDoc preferences""" __tablename__ = "sco_prefs" id = db.Column(db.Integer, primary_key=True) pref_id = db.synonym("id") - dept = db.Column(db.String(16), index=True) - name = db.Column(db.String(128), nullable=False) + + dept_id = db.Column(db.Integer, db.ForeignKey("departement.id")) + + name = db.Column(db.String(128), nullable=False, index=True) value = db.Column(db.Text()) formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id")) diff --git a/app/scodoc/notesdb.py b/app/scodoc/notesdb.py index 18ae6517..a1c8a54c 100644 --- a/app/scodoc/notesdb.py +++ b/app/scodoc/notesdb.py @@ -7,8 +7,9 @@ import psycopg2 import psycopg2.pool import psycopg2.extras -from flask import g +from flask import g, current_app +import app import app.scodoc.sco_utils as scu from app.scodoc.notes_log import log from app.scodoc.sco_exceptions import ScoException, ScoValueError, NoteProcessError @@ -33,28 +34,17 @@ def unquote(s): return s.replace("&", "&") -def open_dept_connection(scodoc_dept=None): - """Open a connection to the current dept db (g.scodoc_dept) - or to the argument scodoc_dept - """ - return psycopg2.connect(scu.get_db_cnx_string(scodoc_dept)) +def open_db_connection(): + """Open a connection to the database""" + g.db_conn = psycopg2.connect(current_app.config["SQLALCHEMY_DATABASE_URI"]) -def close_dept_connection(): - """Commit and close dept db.""" - # log("close_dept_connection to " + scu.get_db_cnx_string()) - g.db_conn.commit() - g.db_conn.close() - - -def set_sco_dept(scodoc_dept): - """Set "context" to given dept - open db connection - """ - g.scodoc_dept = scodoc_dept +def close_db_connection(): + """Commit and close database.""" if hasattr(g, "db_conn"): - close_dept_connection() - g.db_conn = open_dept_connection() + g.db_conn.commit() + g.db_conn.close() + del g.db_conn def GetDBConnexion(autocommit=True): # on n'utilise plus autocommit @@ -277,6 +267,7 @@ class EditableTable(object): html_quote=True, fields_creators={}, # { field : [ sql_command_to_create_it ] } filter_nulls=True, # dont allow to set fields to null + filter_dept=False, # ajoute selection sur g.scodoc_dept_id ): self.table_name = table_name self.id_name = id_name @@ -295,6 +286,7 @@ class EditableTable(object): self.html_quote = html_quote self.fields_creators = fields_creators self.filter_nulls = filter_nulls + self.filter_dept = filter_dept self.sql_default_values = None def create(self, cnx, args): @@ -304,6 +296,8 @@ class EditableTable(object): del vals[self.id_name] if "id" in vals: del vals["id"] + if self.filter_dept: + vals["dept_id"] = g.scodoc_dept_id if self.html_quote: quote_dict(vals) # quote all HTML markup # format value @@ -334,6 +328,8 @@ class EditableTable(object): vals = dictfilter(args, self.dbfields, self.filter_nulls) if (id_value is not None) and (not "id" in vals): vals["id"] = id_value + if self.filter_dept: + vals["dept_id"] = g.scodoc_dept_id if not sortkey: sortkey = self.sortkey res = DBSelectArgs( diff --git a/app/scodoc/sco_abs_notification.py b/app/scodoc/sco_abs_notification.py index 7e74c191..d583a627 100644 --- a/app/scodoc/sco_abs_notification.py +++ b/app/scodoc/sco_abs_notification.py @@ -118,7 +118,10 @@ def abs_notify_send( msg["To"] = email sco_emails.sendEmail(context, msg) ndb.SimpleQuery( - """insert into absences_notifications (etudid, email, nbabs, nbabsjust, formsemestre_id) values (%(etudid)s, %(email)s, %(nbabs)s, %(nbabsjust)s, %(formsemestre_id)s)""", + """insert into absences_notifications + (etudid, email, nbabs, nbabsjust, formsemestre_id) + VALUES (%(etudid)s, %(email)s, %(nbabs)s, %(nbabsjust)s, %(formsemestre_id)s) + """, vars(), cursor=cursor, ) diff --git a/app/scodoc/sco_cache.py b/app/scodoc/sco_cache.py index d65482a1..a494bf8f 100644 --- a/app/scodoc/sco_cache.py +++ b/app/scodoc/sco_cache.py @@ -138,9 +138,18 @@ class EvaluationCache(ScoDocCache): @classmethod def invalidate_all_sems(cls): - "delete all evaluations from cache" + "delete all evaluations in current dept from cache" evaluation_ids = [ - x[0] for x in ndb.SimpleQuery("SELECT id FROM notes_evaluation", "") + x[0] + for x in ndb.SimpleQuery( + """SELECT e.id + FROM notes_evaluation e, notes_moduleimpl mi, notes_formsemestre s + WHERE s.dept_id=%(dept_id)s + AND s.id = mi.formsemestre_id + AND mi.id = e.moduleimpl_id; + """, + {"dept_id": g.scodoc_dept_id}, + ) ] cls.delete_many(evaluation_ids) @@ -243,7 +252,13 @@ def invalidate_formsemestre( # was inval_cache( context, formsemestre_id=None, # clear all caches log("----- invalidate_formsemestre: clearing all caches -----") formsemestre_ids = [ - x[0] for x in ndb.SimpleQuery("SELECT id FROM notes_formsemestre", "") + x[0] + for x in ndb.SimpleQuery( + """SELECT id FROM notes_formsemestre s + WHERE s.dept_id=%(dept_id)s + """, + {"dept_id": g.scodoc_dept_id}, + ) ] else: formsemestre_ids = [ diff --git a/app/scodoc/sco_dump_db.py b/app/scodoc/sco_dump_db.py index f938fb7d..529afca0 100644 --- a/app/scodoc/sco_dump_db.py +++ b/app/scodoc/sco_dump_db.py @@ -65,7 +65,7 @@ SCO_DUMP_LOCK = "/tmp/scodump.lock" def sco_dump_and_send_db(context, REQUEST=None): - """Dump base de données du département courant et l'envoie anonymisée pour debug""" + """Dump base de données et l'envoie anonymisée pour debug""" H = [html_sco_header.sco_header(page_title="Assistance technique")] # get currect (dept) DB name: cursor = ndb.SimpleQuery("SELECT current_database()", {}) @@ -75,7 +75,7 @@ def sco_dump_and_send_db(context, REQUEST=None): try: x = open(SCO_DUMP_LOCK, "w+") fcntl.flock(x, fcntl.LOCK_EX | fcntl.LOCK_NB) - except (IOError, OSError): # exception changed from Python 2 to 3 + except (IOError, OSError): raise ScoValueError( "Un envoi de la base " + db_name diff --git a/app/scodoc/sco_entreprises.py b/app/scodoc/sco_entreprises.py index 02b4f336..a7d4ffdf 100644 --- a/app/scodoc/sco_entreprises.py +++ b/app/scodoc/sco_entreprises.py @@ -148,6 +148,7 @@ _entreprisesEditor = EntreprisesEditor( "note", "date_creation", ), + filter_dept=True, sortkey="nom", input_formators={ "nom": _format_nom, diff --git a/app/scodoc/sco_etud.py b/app/scodoc/sco_etud.py index 03e9654e..f2a5a423 100644 --- a/app/scodoc/sco_etud.py +++ b/app/scodoc/sco_etud.py @@ -258,6 +258,7 @@ _identiteEditor = ndb.EditableTable( "code_ine", "code_nip", ), + filter_dept=True, sortkey="nom", input_formators={ "nom": force_uppercase, diff --git a/app/scodoc/sco_find_etud.py b/app/scodoc/sco_find_etud.py index 9176fda0..5847ba51 100644 --- a/app/scodoc/sco_find_etud.py +++ b/app/scodoc/sco_find_etud.py @@ -32,6 +32,7 @@ from flask import url_for, g from flask_login import current_user from scodoc_manager import sco_mgr +import app import app.scodoc.sco_utils as scu import app.scodoc.notesdb as ndb from app.scodoc.gen_tables import GenTable @@ -48,7 +49,7 @@ def form_search_etud( dest_url=None, parameters=None, parameters_keys=None, - title="Rechercher un étudiant par nom : ", + title="Rechercher un étudiant par nom : ", add_headers=False, # complete page ): "form recherche par nom" @@ -274,7 +275,7 @@ def search_etud_in_accessible_depts(expnom=None, code_nip=None): if current_user.has_permission(Permission.ScoView, dept=dept): if expnom or code_nip: accessible_depts.append(dept) - ndb.set_sco_dept(dept) + app.set_sco_dept(dept) etuds = search_etuds_infos(expnom=expnom, code_nip=code_nip) else: etuds = [] diff --git a/app/scodoc/sco_formations.py b/app/scodoc/sco_formations.py index 043a4bec..162900e7 100644 --- a/app/scodoc/sco_formations.py +++ b/app/scodoc/sco_formations.py @@ -64,6 +64,7 @@ _formationEditor = ndb.EditableTable( "type_parcours", "code_specialite", ), + filter_dept=True, sortkey="acronyme", ) diff --git a/app/scodoc/sco_formsemestre.py b/app/scodoc/sco_formsemestre.py index 5a71da4e..af6f77d2 100644 --- a/app/scodoc/sco_formsemestre.py +++ b/app/scodoc/sco_formsemestre.py @@ -65,6 +65,7 @@ _formsemestreEditor = ndb.EditableTable( "elt_sem_apo", "elt_annee_apo", ), + filter_dept=True, sortkey="date_debut", output_formators={ "date_debut": ndb.DateISOtoDMY, diff --git a/app/scodoc/sco_preferences.py b/app/scodoc/sco_preferences.py index bf87dac0..0559a53a 100644 --- a/app/scodoc/sco_preferences.py +++ b/app/scodoc/sco_preferences.py @@ -113,6 +113,7 @@ get_base_preferences(formsemestre_id) import flask from flask import g +from app.models import Departement from app.scodoc import sco_cache from app.scodoc.notes_log import log from app.scodoc.sco_exceptions import ScoValueError, ScoException @@ -120,15 +121,22 @@ from app.scodoc.TrivialFormulator import TrivialFormulator import app.scodoc.notesdb as ndb import app.scodoc.sco_utils as scu -_SCO_BASE_PREFERENCES = {} # { URL: BasePreferences instance } +_SCO_BASE_PREFERENCES = {} # { dept_acronym: BasePreferences instance } + + +def clear_base_preferences(): + """Clear cached preferences""" + # usefull only for tests, where the same process may run + # successively on several databases + _SCO_BASE_PREFERENCES.clear() def get_base_preferences(): """Return global preferences for the current department""" - dept = g.scodoc_dept - if not dept in _SCO_BASE_PREFERENCES: - _SCO_BASE_PREFERENCES[dept] = BasePreferences() - return _SCO_BASE_PREFERENCES[dept] + dept_acronym = g.scodoc_dept + if not dept_acronym in _SCO_BASE_PREFERENCES: + _SCO_BASE_PREFERENCES[dept_acronym] = BasePreferences(dept_acronym) + return _SCO_BASE_PREFERENCES[dept_acronym] def get_preference(name, formsemestre_id=None): @@ -198,7 +206,8 @@ class BasePreferences(object): _editor = ndb.EditableTable( "sco_prefs", "pref_id", - ("pref_id", "name", "value", "formsemestre_id"), + ("pref_id", "dept_id", "name", "value", "formsemestre_id"), + filter_dept=True, sortkey="name", convert_null_outputs_to_empty=False, # allow_set_id=True, #sco8 @@ -206,7 +215,11 @@ class BasePreferences(object): filter_nulls=False, ) - def __init__(self): + def __init__(self, dept_acronym: str): + dept = Departement.query.filter_by(acronym=dept_acronym).first() + if not dept: + raise ScoValueError(f"Invalid departement: {dept_acronym}") + self.dept_id = dept.id self.init() self.load() @@ -1785,11 +1798,11 @@ class BasePreferences(object): def load(self): """Load all preferences from db""" - log("loading preferences") + log(f"loading preferences for dept_id={self.dept_id}") try: scu.GSL.acquire() cnx = ndb.GetDBConnexion() - preflist = self._editor.list(cnx) + preflist = self._editor.list(cnx, {"dept_id": self.dept_id}) self.prefs = {None: {}} # { formsemestre_id (or None) : { name : value } } self.default = {} # { name : default_value } for p in preflist: @@ -1850,7 +1863,9 @@ class BasePreferences(object): self.prefs[None][name] = value log("creating missing preference for %s=%s" % (name, value)) # add to db table - self._editor.create(cnx, {"name": name, "value": value}) + self._editor.create( + cnx, {"dept_id": self.dept_id, "name": name, "value": value} + ) finally: scu.GSL.release() @@ -1898,14 +1913,20 @@ class BasePreferences(object): value = "1" if value else "0" # existe deja ? pdb = self._editor.list( - cnx, args={"formsemestre_id": formsemestre_id, "name": name} + cnx, + args={ + "dept_id": self.dept_id, + "formsemestre_id": formsemestre_id, + "name": name, + }, ) if not pdb: - # cree preference + # crée préférence log("create pref sem=%s %s=%s" % (formsemestre_id, name, value)) self._editor.create( cnx, { + "dept_id": self.dept_id, "name": name, "value": value, "formsemestre_id": formsemestre_id, @@ -1959,6 +1980,7 @@ class BasePreferences(object): ) if pdb: log("deleting pref sem=%s %s" % (formsemestre_id, name)) + assert pdb[0]["dept_id"] == self.dept_id self._editor.delete(cnx, pdb[0]["pref_id"]) sco_cache.invalidate_formsemestre() # > modif preferences finally: diff --git a/app/scodoc/sco_semset.py b/app/scodoc/sco_semset.py index acd3b143..cc22bc01 100644 --- a/app/scodoc/sco_semset.py +++ b/app/scodoc/sco_semset.py @@ -40,6 +40,7 @@ sem_set_list(context) """ import flask +from flask import g from app.scodoc import html_sco_header from app.scodoc import sco_cache @@ -58,7 +59,10 @@ import app.scodoc.sco_utils as scu _semset_editor = ndb.EditableTable( - "notes_semset", "semset_id", ("semset_id", "title", "annee_scolaire", "sem_id") + "notes_semset", + "semset_id", + ("semset_id", "title", "annee_scolaire", "sem_id"), + filter_dept=True, ) semset_create = _semset_editor.create @@ -156,14 +160,24 @@ class SemSet(dict): ) ndb.SimpleQuery( - "INSERT INTO notes_semset_formsemestre (formsemestre_id, semset_id) VALUES (%(formsemestre_id)s, %(semset_id)s)", - {"formsemestre_id": formsemestre_id, "semset_id": self.semset_id}, + """INSERT INTO notes_semset_formsemestre + (dept_id, id, semset_id) + VALUES (%(dept_id)s, %(formsemestre_id)s, %(semset_id)s) + """, + { + "dept_id": g.scodoc_dept_id, + "formsemestre_id": formsemestre_id, + "semset_id": self.semset_id, + }, ) self.load_sems() # update our list def remove(self, formsemestre_id): ndb.SimpleQuery( - "DELETE FROM notes_semset_formsemestre WHERE semset_id=%(semset_id)s AND formsemestre_id=%(formsemestre_id)s", + """DELETE FROM notes_semset_formsemestre + WHERE id=%(semset_id)s + AND formsemestre_id=%(formsemestre_id)s + """, {"formsemestre_id": formsemestre_id, "semset_id": self.semset_id}, ) self.load_sems() # update our list diff --git a/app/scodoc/sco_tag_module.py b/app/scodoc/sco_tag_module.py index 4e74cfc4..93786f16 100644 --- a/app/scodoc/sco_tag_module.py +++ b/app/scodoc/sco_tag_module.py @@ -75,8 +75,8 @@ class ScoTag(object): r = ndb.SimpleDictFetch( "SELECT id as tag_id, * FROM " + self.tag_table - + " WHERE title = %(title)s", - {"title": self.title}, + + " WHERE dept_id=%(dept_id) AND title = %(title)s", + {"dept_id": g.scodoc_dept_id, "title": self.title}, ) if r: self.tag_id = r[0]["tag_id"] @@ -182,8 +182,8 @@ class ModuleTag(ScoTag): r = ndb.SimpleDictFetch( """SELECT mt.module_id FROM notes_modules_tags mt, notes_modules m, notes_formations f - WHERE mt.tag_id = %(tag_id)s - AND m.id = mt.module_id + WHERE mt.tag_id = %(tag_id)s + AND m.id = mt.module_id AND m.formation_id = f.id AND f.formation_code = %(formation_code)s """, diff --git a/app/scodoc/sco_utils.py b/app/scodoc/sco_utils.py index 3093f871..963202da 100644 --- a/app/scodoc/sco_utils.py +++ b/app/scodoc/sco_utils.py @@ -52,7 +52,7 @@ STRING_TYPES = six.string_types from PIL import Image as PILImage -from flask import g, url_for, request, current_app +from flask import g, url_for, request from scodoc_manager import sco_mgr @@ -319,19 +319,10 @@ BULLETINS_VERSIONS = ("short", "selectedevals", "long") # Support for ScoDoc7 compatibility def get_dept_id(): + "acronyme du dept courant" return g.scodoc_dept # en scodoc 8.1 #sco8 -# if g.scodoc_dept in sco_mgr.get_dept_ids(): -# return g.scodoc_dept -# raise sco_exceptions.ScoInvalidDept("département invalide: %s" % g.scodoc_dept) - - -def get_db_cnx_string(scodoc_dept=None): - return current_app.config["SQLALCHEMY_DATABASE_URI"] - # return "dbname=SCO" + (scodoc_dept or g.scodoc_dept) - - def ScoURL(): """base URL for this sco instance. e.g. https://scodoc.xxx.fr/ScoDoc/DEPT/Scolarite diff --git a/app/views/__init__.py b/app/views/__init__.py index 24972e5a..fb520825 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -2,7 +2,7 @@ """ScoDoc Flask views """ from flask import Blueprint -from flask import g +from flask import g, current_app from app.scodoc import notesdb as ndb scodoc_bp = Blueprint("scodoc", __name__) @@ -12,19 +12,18 @@ users_bp = Blueprint("users", __name__) absences_bp = Blueprint("absences", __name__) essais_bp = Blueprint("essais", __name__) +from app.views import scodoc, notes, scolar, absences, users, essais +# Cette fonction est bien appelée avant toutes les requêtes +# de tous les blueprints +# mais apparemment elle n'a pas acces aux arguments @scodoc_bp.before_app_request -def open_dept_db_connection(): - # current_app.logger.info("open_dept_db_connection") - if hasattr(g, "scodoc_dept") and not hasattr(g, "db_conn") and g.scodoc_dept: - g.db_conn = ndb.open_dept_connection() +def start_scodoc_request(): + """Affecte toutes les requêtes, de tous les blueprints""" + ndb.open_db_connection() @scodoc_bp.teardown_app_request def close_dept_db_connection(arg): - # current_app.logger.info("close_dept_db_connection") - if hasattr(g, "db_conn"): - ndb.close_dept_connection() - - -from app.views import scodoc, notes, scolar, absences, users, essais + # current_app.logger.info("close_db_connection") + ndb.close_db_connection() diff --git a/app/views/absences.py b/app/views/absences.py index 2f6f201d..0f5d0dcf 100644 --- a/app/views/absences.py +++ b/app/views/absences.py @@ -63,6 +63,7 @@ from flask import url_for from flask import current_app from app.decorators import ( + scodoc, scodoc7func, ScoDoc7Context, permission_required, @@ -109,7 +110,7 @@ def sco_publish(route, function, permission, methods=["GET"]): protected by permission and called following ScoDoc 7 Zope standards. """ return bp.route(route, methods=methods)( - permission_required(permission)(scodoc7func(context)(function)) + permission_required(permission)(scodoc7func(context)(scodoc(function))) ) @@ -127,6 +128,7 @@ def _toboolean(x): @bp.route("/") @bp.route("/index_html") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def index_html(context, REQUEST=None): @@ -267,6 +269,7 @@ sco_publish("/CountAbsJust", sco_abs.count_abs_just, Permission.ScoView) @bp.route("/doSignaleAbsenceGrSemestre", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoAbsChange) @scodoc7func(context) def doSignaleAbsenceGrSemestre( @@ -310,6 +313,7 @@ def doSignaleAbsenceGrSemestre( # ------------ HTML Interfaces @bp.route("/SignaleAbsenceGrHebdo", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoAbsChange) @scodoc7func(context) def SignaleAbsenceGrHebdo( @@ -473,6 +477,7 @@ def SignaleAbsenceGrHebdo( @bp.route("/SignaleAbsenceGrSemestre", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoAbsChange) @scodoc7func(context) def SignaleAbsenceGrSemestre( @@ -846,6 +851,7 @@ def _gen_form_saisie_groupe( @bp.route("/EtatAbsencesGr") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) # ported from dtml def EtatAbsencesGr( @@ -984,6 +990,7 @@ ou entrez une date pour visualiser les absents un jour donné : @bp.route("/EtatAbsencesDate") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def EtatAbsencesDate( @@ -1068,6 +1075,7 @@ def EtatAbsencesDate( # ----- Gestion des "billets d'absence": signalement par les etudiants eux mêmes (à travers le portail) @bp.route("/AddBilletAbsence") +@scodoc @permission_required(Permission.ScoAbsAddBillet) @scodoc7func(context) def AddBilletAbsence( @@ -1127,6 +1135,7 @@ def AddBilletAbsence( @bp.route("/AddBilletAbsenceForm") +@scodoc @permission_required(Permission.ScoAbsAddBillet) @scodoc7func(context) def AddBilletAbsenceForm(context, etudid, REQUEST=None): @@ -1239,6 +1248,7 @@ def _tableBillets(context, billets, etud=None, title=""): @bp.route("/listeBilletsEtud") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def listeBilletsEtud(context, etudid=False, REQUEST=None, format="html"): @@ -1255,6 +1265,7 @@ def listeBilletsEtud(context, etudid=False, REQUEST=None, format="html"): @bp.route("/XMLgetBilletsEtud") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def XMLgetBilletsEtud(context, etudid=False, REQUEST=None): @@ -1268,6 +1279,7 @@ def XMLgetBilletsEtud(context, etudid=False, REQUEST=None): @bp.route("/listeBillets") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def listeBillets(context, REQUEST=None): @@ -1296,6 +1308,7 @@ def listeBillets(context, REQUEST=None): @bp.route("/deleteBilletAbsence") +@scodoc @permission_required(Permission.ScoAbsChange) @scodoc7func(context) def deleteBilletAbsence(context, billet_id, REQUEST=None, dialog_confirmed=False): @@ -1390,6 +1403,7 @@ def _ProcessBilletAbsence(context, billet, estjust, description, REQUEST): @bp.route("/ProcessBilletAbsenceForm") +@scodoc @permission_required(Permission.ScoAbsChange) @scodoc7func(context) def ProcessBilletAbsenceForm(context, billet_id, REQUEST=None): @@ -1480,6 +1494,7 @@ def ProcessBilletAbsenceForm(context, billet_id, REQUEST=None): @bp.route("/XMLgetAbsEtud") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def XMLgetAbsEtud(context, beg_date="", end_date="", REQUEST=None): diff --git a/app/views/essais.py b/app/views/essais.py index 67e49da7..50a40f93 100644 --- a/app/views/essais.py +++ b/app/views/essais.py @@ -14,6 +14,7 @@ from flask import request from flask import url_for from app.decorators import ( + scodoc, scodoc7func, ScoDoc7Context, permission_required, @@ -31,6 +32,7 @@ context = ScoDoc7Context(globals()) @bp.route("//Scolarite/sco_exemple") +@scodoc @scodoc7func(context) def sco_exemple(etudid="NON"): """Un exemple de fonction ScoDoc 7""" @@ -56,6 +58,7 @@ def sco_exemple(etudid="NON"): @bp.route("//Scolarite/sco_exemple3") +@scodoc @login_required @scodoc7func(context) def sco_exemple3(toto): @@ -63,6 +66,7 @@ def sco_exemple3(toto): @bp.route("//Scolarite/sco_exemple4") +@scodoc @login_required @scodoc7func(context) def sco_exemple4(toto): @@ -71,6 +75,7 @@ def sco_exemple4(toto): # Test avec un seul argument REQUEST positionnel @bp.route("//Scolarite/sco_get_version") +@scodoc @scodoc7func(context) def sco_get_version(REQUEST): return "ok" @@ -78,8 +83,9 @@ def sco_get_version(REQUEST): # Fonction ressemblant à une méthode Zope protégée @bp.route("//Scolarite/sco_test_view") -@scodoc7func(context) +@scodoc @permission_required(Permission.ScoView) +@scodoc7func(context) def sco_test_view(REQUEST=None): return """Vous avez vu sco_test_view !""" diff --git a/app/views/notes.py b/app/views/notes.py index 9edc332d..5bca5326 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -48,6 +48,7 @@ import scodoc_manager from app.auth.models import User from app.decorators import ( + scodoc, scodoc7func, ScoDoc7Context, permission_required, @@ -150,6 +151,7 @@ def sco_publish(route, function, permission, methods=["GET"]): # --------------------- Quelques essais élémentaires: @bp.route("/essai") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def essai(context, REQUEST=None): @@ -380,6 +382,7 @@ sco_publish( # @bp.route("/") @bp.route("/index_html") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def index_html(context, REQUEST=None): @@ -432,6 +435,7 @@ sco_publish( @bp.route("/formation_list") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formation_list(context, format=None, REQUEST=None, formation_id=None, args={}): @@ -443,6 +447,7 @@ def formation_list(context, format=None, REQUEST=None, formation_id=None, args={ @bp.route("/formation_export") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formation_export( @@ -455,6 +460,7 @@ def formation_export( @bp.route("/formation_import_xml") +@scodoc @permission_required(Permission.ScoChangeFormation) @scodoc7func(context) def formation_import_xml(context, file): @@ -465,6 +471,7 @@ def formation_import_xml(context, file): @bp.route("/formation_import_xml_form", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoChangeFormation) @scodoc7func(context) def formation_import_xml_form(context, REQUEST): @@ -546,6 +553,7 @@ sco_publish( @bp.route("/formation_count_sems") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formation_count_sems(context, formation_id): @@ -578,6 +586,7 @@ sco_publish("/ue_move", sco_edit_formation.ue_move, Permission.ScoChangeFormatio @bp.route("/formsemestre_list") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_list( @@ -604,6 +613,7 @@ def formsemestre_list( @bp.route("/XMLgetFormsemestres") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def XMLgetFormsemestres(context, etape_apo=None, formsemestre_id=None, REQUEST=None): @@ -656,6 +666,7 @@ sco_publish( @bp.route("/formsemestre_custommenu_edit", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_custommenu_edit(context, REQUEST, formsemestre_id): @@ -668,6 +679,7 @@ def formsemestre_custommenu_edit(context, REQUEST, formsemestre_id): # --- dialogue modif enseignants/moduleimpl @bp.route("/edit_enseignants_form", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def edit_enseignants_form(context, REQUEST, moduleimpl_id): @@ -778,6 +790,7 @@ def edit_enseignants_form(context, REQUEST, moduleimpl_id): @bp.route("/edit_moduleimpl_resp", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def edit_moduleimpl_resp(context, REQUEST, moduleimpl_id): @@ -885,6 +898,7 @@ _EXPR_HELP = """

Expérimental: formule de calcul de la moyenne % @bp.route("/edit_moduleimpl_expr", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def edit_moduleimpl_expr(context, REQUEST, moduleimpl_id): @@ -953,6 +967,7 @@ def edit_moduleimpl_expr(context, REQUEST, moduleimpl_id): @bp.route("/view_module_abs") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def view_module_abs(context, REQUEST, moduleimpl_id, format="html"): @@ -1033,6 +1048,7 @@ def view_module_abs(context, REQUEST, moduleimpl_id, format="html"): @bp.route("/edit_ue_expr", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def edit_ue_expr(context, REQUEST, formsemestre_id, ue_id): @@ -1107,6 +1123,7 @@ def edit_ue_expr(context, REQUEST, formsemestre_id, ue_id): @bp.route("/formsemestre_enseignants_list") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_enseignants_list(context, REQUEST, formsemestre_id, format="html"): @@ -1191,6 +1208,7 @@ def formsemestre_enseignants_list(context, REQUEST, formsemestre_id, format="htm @bp.route("/edit_enseignants_form_delete", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def edit_enseignants_form_delete(context, REQUEST, moduleimpl_id, ens_id): @@ -1230,6 +1248,7 @@ sco_publish( @bp.route("/do_formsemestre_inscription_listinscrits") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def do_formsemestre_inscription_listinscrits( @@ -1243,6 +1262,7 @@ def do_formsemestre_inscription_listinscrits( @bp.route("/formsemestre_desinscription") +@scodoc @permission_required(Permission.ScoImplement) @scodoc7func(context) def formsemestre_desinscription( @@ -1329,6 +1349,7 @@ sco_publish( @bp.route("/etud_desinscrit_ue") +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def etud_desinscrit_ue(context, etudid, formsemestre_id, ue_id, REQUEST=None): @@ -1344,6 +1365,7 @@ def etud_desinscrit_ue(context, etudid, formsemestre_id, ue_id, REQUEST=None): @bp.route("/etud_inscrit_ue") +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def etud_inscrit_ue(context, etudid, formsemestre_id, ue_id, REQUEST=None): @@ -1407,6 +1429,7 @@ sco_publish( @bp.route("/evaluation_delete", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEnsView) @scodoc7func(context) def evaluation_delete(context, REQUEST, evaluation_id): @@ -1490,6 +1513,7 @@ sco_publish( @bp.route("/evaluation_edit", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEnsView) @scodoc7func(context) def evaluation_edit(evaluation_id, REQUEST): @@ -1500,6 +1524,7 @@ def evaluation_edit(evaluation_id, REQUEST): @bp.route("/evaluation_create", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEnsView) @scodoc7func(context) def evaluation_create(moduleimpl_id, REQUEST): @@ -1510,6 +1535,7 @@ def evaluation_create(moduleimpl_id, REQUEST): @bp.route("/evaluation_listenotes") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def evaluation_listenotes(context, REQUEST=None): @@ -1591,6 +1617,7 @@ sco_publish( # --- Bulletins @bp.route("/formsemestre_bulletins_pdf") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_bulletins_pdf( @@ -1607,6 +1634,7 @@ _EXPL_BULL = """Versions des bulletins:

  • courte: moyennes des mod @bp.route("/formsemestre_bulletins_pdf_choice") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_bulletins_pdf_choice(context, REQUEST, formsemestre_id, version=None): @@ -1626,6 +1654,7 @@ def formsemestre_bulletins_pdf_choice(context, REQUEST, formsemestre_id, version @bp.route("/etud_bulletins_pdf") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def etud_bulletins_pdf(context, etudid, REQUEST, version="selectedevals"): @@ -1637,6 +1666,7 @@ def etud_bulletins_pdf(context, etudid, REQUEST, version="selectedevals"): @bp.route("/formsemestre_bulletins_mailetuds_choice") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_bulletins_mailetuds_choice( @@ -1703,6 +1733,7 @@ def formsemestre_bulletins_choice( @bp.route("/formsemestre_bulletins_mailetuds") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_bulletins_mailetuds( @@ -1766,6 +1797,7 @@ sco_publish( @bp.route("/appreciation_add_form", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEnsView) @scodoc7func(context) def appreciation_add_form( @@ -1875,6 +1907,7 @@ def appreciation_add_form( @bp.route("/formsemestre_validation_etud_form") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_validation_etud_form( @@ -1903,6 +1936,7 @@ def formsemestre_validation_etud_form( @bp.route("/formsemestre_validation_etud") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_validation_etud( @@ -1934,6 +1968,7 @@ def formsemestre_validation_etud( @bp.route("/formsemestre_validation_etud_manu") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_validation_etud_manu( @@ -1971,6 +2006,7 @@ def formsemestre_validation_etud_manu( @bp.route("/formsemestre_validate_previous_ue") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_validate_previous_ue( @@ -1997,6 +2033,7 @@ sco_publish( @bp.route("/formsemestre_ext_edit_ue_validations", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_ext_edit_ue_validations( @@ -2022,6 +2059,7 @@ sco_publish( @bp.route("/etud_ue_suppress_validation") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def etud_ue_suppress_validation(context, etudid, formsemestre_id, ue_id, REQUEST=None): @@ -2038,6 +2076,7 @@ def etud_ue_suppress_validation(context, etudid, formsemestre_id, ue_id, REQUEST @bp.route("/formsemestre_validation_auto") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_validation_auto(context, formsemestre_id, REQUEST): @@ -2055,6 +2094,7 @@ def formsemestre_validation_auto(context, formsemestre_id, REQUEST): @bp.route("/do_formsemestre_validation_auto") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def do_formsemestre_validation_auto(context, formsemestre_id, REQUEST): @@ -2072,6 +2112,7 @@ def do_formsemestre_validation_auto(context, formsemestre_id, REQUEST): @bp.route("/formsemestre_validation_suppress_etud", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_validation_suppress_etud( @@ -2312,6 +2353,7 @@ sco_publish( @bp.route("/check_sem_integrity") +@scodoc @permission_required(Permission.ScoImplement) @scodoc7func(context) def check_sem_integrity(context, formsemestre_id, REQUEST, fix=False): @@ -2384,6 +2426,7 @@ def check_sem_integrity(context, formsemestre_id, REQUEST, fix=False): @bp.route("/check_form_integrity") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def check_form_integrity(context, formation_id, fix=False, REQUEST=None): @@ -2423,6 +2466,7 @@ def check_form_integrity(context, formation_id, fix=False, REQUEST=None): @bp.route("/check_formsemestre_integrity") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def check_formsemestre_integrity(context, formsemestre_id, REQUEST=None): @@ -2473,6 +2517,7 @@ def check_formsemestre_integrity(context, formsemestre_id, REQUEST=None): @bp.route("/check_integrity_all") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def check_integrity_all(context, REQUEST=None): diff --git a/app/views/scodoc.py b/app/views/scodoc.py index 54ddc8c7..855a418f 100644 --- a/app/views/scodoc.py +++ b/app/views/scodoc.py @@ -45,7 +45,7 @@ from app.scodoc.sco_permissions import Permission @bp.route("/ScoDoc") @bp.route("/ScoDoc/index") -def index(): +def index(): # XXX TODO A REECRIRE dept_ids = sco_mgr.get_dept_ids() return render_template( "scodoc.html", diff --git a/app/views/scolar.py b/app/views/scolar.py index c919de88..82ecc27c 100644 --- a/app/views/scolar.py +++ b/app/views/scolar.py @@ -48,6 +48,7 @@ from flask_login import current_user from config import Config import scodoc_manager from app.decorators import ( + scodoc, scodoc7func, ScoDoc7Context, permission_required, @@ -120,7 +121,7 @@ def sco_publish(route, function, permission, methods=("GET",)): protected by permission and called following ScoDoc 7 Zope standards. """ return bp.route(route, methods=methods)( - permission_required(permission)(scodoc7func(context)(function)) + permission_required(permission)(scodoc7func(context)(scodoc(function))) ) @@ -135,6 +136,7 @@ log("ScoDoc8 restarting...") @bp.route("/about") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def about(context, REQUEST): @@ -169,6 +171,7 @@ def about(context, REQUEST): @bp.route("/edit_preferences", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoChangePreferences) @scodoc7func(context) def edit_preferences(context, REQUEST): @@ -177,6 +180,7 @@ def edit_preferences(context, REQUEST): @bp.route("/formsemestre_edit_preferences", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def formsemestre_edit_preferences(context, formsemestre_id, REQUEST): @@ -196,6 +200,7 @@ def formsemestre_edit_preferences(context, formsemestre_id, REQUEST): @bp.route("/doc_preferences") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def doc_preferences(REQUEST): @@ -212,6 +217,7 @@ def doc_preferences(REQUEST): @bp.route("/showEtudLog") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def showEtudLog(context, etudid, format="html", REQUEST=None): @@ -248,8 +254,22 @@ def showEtudLog(context, etudid, format="html", REQUEST=None): # ---------- PAGE ACCUEIL (listes) -------------- -@bp.route("/") -@bp.route("/index_html") +# @bp.route("/") +@bp.route("/kimo") +@scodoc +@scodoc +@permission_required(Permission.ScoView) +@scodoc7func(context) +def kimo(context, REQUEST=None, showcodes=0, showsemtable=0): + import time + + return f"{time.time()} := {g.scodoc_dept}" + + +# @bp.route("/") +@bp.route("/index_html2") +@scodoc +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def index_html(context, REQUEST=None, showcodes=0, showsemtable=0): @@ -287,6 +307,7 @@ sco_publish( # -------------------------- INFOS SUR ETUDIANTS -------------------------- @bp.route("/getEtudInfo") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def getEtudInfo( @@ -321,6 +342,7 @@ sco_publish( # XMLgetEtudInfos était le nom dans l'ancienne API ScoDoc 6 @bp.route("/etud_info") @bp.route("/XMLgetEtudInfos") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def etud_info(context, etudid=None, format="xml", REQUEST=None): @@ -494,6 +516,7 @@ sco_publish( @bp.route("/doAddAnnotation", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEtudAddAnnotations) @scodoc7func(context) def doAddAnnotation(etudid, comment): @@ -514,6 +537,7 @@ def doAddAnnotation(etudid, comment): @bp.route("/doSuppressAnnotation", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def doSuppressAnnotation(context, etudid, annotation_id, REQUEST): @@ -541,6 +565,7 @@ def doSuppressAnnotation(context, etudid, annotation_id, REQUEST): @bp.route("/formChangeCoordonnees", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEtudChangeAdr) @scodoc7func(context) def formChangeCoordonnees(context, etudid, REQUEST): @@ -704,6 +729,7 @@ sco_publish( methods=["GET", "POST"], ) # @bp.route("/partition_create", methods=["GET", "POST"]) +# @scodoc # @permission_required(Permission.ScoView) # @scodoc7func(context) # def partition_create( @@ -725,6 +751,7 @@ sco_publish("/etud_photo_html", sco_photos.etud_photo_html, Permission.ScoView) @bp.route("/etud_photo_orig_page") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def etud_photo_orig_page(context, etudid=None, REQUEST=None): @@ -743,6 +770,7 @@ def etud_photo_orig_page(context, etudid=None, REQUEST=None): @bp.route("/formChangePhoto", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEtudChangeAdr) @scodoc7func(context) def formChangePhoto(context, etudid=None, REQUEST=None): @@ -802,6 +830,7 @@ def formChangePhoto(context, etudid=None, REQUEST=None): @bp.route("/formSuppressPhoto") +@scodoc @permission_required(Permission.ScoEtudChangeAdr) @scodoc7func(context) def formSuppressPhoto(context, etudid=None, REQUEST=None, dialog_confirmed=False): @@ -826,6 +855,7 @@ def formSuppressPhoto(context, etudid=None, REQUEST=None, dialog_confirmed=False # @bp.route("/formDem") +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def formDem(context, etudid, formsemestre_id, REQUEST): @@ -841,6 +871,7 @@ def formDem(context, etudid, formsemestre_id, REQUEST): @bp.route("/formDef") +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def formDef(context, etudid, formsemestre_id, REQUEST): @@ -902,6 +933,7 @@ def _formDem_of_Def( @bp.route("/doDemEtudiant") +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def doDemEtudiant(context, etudid, formsemestre_id, event_date=None, REQUEST=None): @@ -919,6 +951,7 @@ def doDemEtudiant(context, etudid, formsemestre_id, event_date=None, REQUEST=Non @bp.route("/doDefEtudiant") +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def doDefEtudiant(context, etudid, formsemestre_id, event_date=None, REQUEST=None): @@ -980,6 +1013,7 @@ def _do_dem_or_def_etud( @bp.route("/doCancelDem", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def doCancelDem( @@ -1002,6 +1036,7 @@ def doCancelDem( @bp.route("/doCancelDef", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def doCancelDef( @@ -1087,6 +1122,7 @@ def _do_cancel_dem_or_def( @bp.route("/etudident_create_form", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def etudident_create_form(context, REQUEST=None): @@ -1095,6 +1131,7 @@ def etudident_create_form(context, REQUEST=None): @bp.route("/etudident_edit_form", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def etudident_edit_form(context, REQUEST=None): @@ -1501,6 +1538,7 @@ def _etudident_create_or_edit_form(context, REQUEST, edit): @bp.route("/etudident_delete", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def etudident_delete(context, etudid, dialog_confirmed=False, REQUEST=None): @@ -1573,6 +1611,7 @@ def etudident_delete(context, etudid, dialog_confirmed=False, REQUEST=None): @bp.route("/check_group_apogee") +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def check_group_apogee( @@ -1724,6 +1763,7 @@ def check_group_apogee( @bp.route("/form_students_import_excel") +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def form_students_import_excel(context, REQUEST, formsemestre_id=None): @@ -1864,6 +1904,7 @@ Les champs avec un astérisque (*) doivent être présents (nulls non autorisés @bp.route("/import_generate_excel_sample") +@scodoc @permission_required(Permission.ScoEtudInscrit) @scodoc7func(context) def import_generate_excel_sample(context, REQUEST, with_codesemestre="1"): @@ -1881,6 +1922,7 @@ def import_generate_excel_sample(context, REQUEST, with_codesemestre="1"): # --- Données admission @bp.route("/import_generate_admission_sample") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def import_generate_admission_sample(context, REQUEST, formsemestre_id): @@ -1902,6 +1944,7 @@ def import_generate_admission_sample(context, REQUEST, formsemestre_id): # --- Données admission depuis fichier excel (version nov 2016) @bp.route("/form_students_import_infos_admissions", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def form_students_import_infos_admissions(context, REQUEST, formsemestre_id=None): @@ -2009,6 +2052,7 @@ def form_students_import_infos_admissions(context, REQUEST, formsemestre_id=None @bp.route("/formsemestre_import_etud_admission") +@scodoc @permission_required(Permission.ScoEtudChangeAdr) @scodoc7func(context) def formsemestre_import_etud_admission( @@ -2057,6 +2101,7 @@ sco_publish( # --- Statistiques @bp.route("/stat_bac") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def stat_bac(context, formsemestre_id): diff --git a/app/views/users.py b/app/views/users.py index 69eac225..17c68411 100644 --- a/app/views/users.py +++ b/app/views/users.py @@ -48,6 +48,7 @@ from app.auth.models import User from app.auth.models import Role from app.auth.models import UserRole from app.decorators import ( + scodoc, scodoc7func, ScoDoc7Context, permission_required, @@ -71,6 +72,7 @@ context = ScoDoc7Context("users") # sco8 @bp.route("/") @bp.route("/index_html") +@scodoc @permission_required(Permission.ScoUsersView) @scodoc7func(context) def index_html(context, REQUEST, all_depts=False, with_inactives=False, format="html"): @@ -84,6 +86,7 @@ def index_html(context, REQUEST, all_depts=False, with_inactives=False, format=" @bp.route("/user_info") +@scodoc @permission_required(Permission.ScoUsersView) @scodoc7func(context) def user_info(user_name, format="json", REQUEST=None): @@ -92,6 +95,7 @@ def user_info(user_name, format="json", REQUEST=None): @bp.route("/create_user_form", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoUsersAdmin) @scodoc7func(context) def create_user_form(context, REQUEST, user_name=None, edit=0): @@ -464,6 +468,7 @@ def import_users_form(): @bp.route("/user_info_page") +@scodoc @permission_required(Permission.ScoUsersView) @scodoc7func(context) def user_info_page(user_name, REQUEST=None): @@ -471,6 +476,7 @@ def user_info_page(user_name, REQUEST=None): @bp.route("/get_user_list_xml") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def get_user_list_xml(dept=None, start="", limit=25, REQUEST=None): @@ -496,6 +502,7 @@ def get_user_list_xml(dept=None, start="", limit=25, REQUEST=None): @bp.route("/form_change_password") +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def form_change_password(REQUEST, user_name=None): @@ -534,6 +541,7 @@ def form_change_password(REQUEST, user_name=None): @bp.route("/change_password", methods=["POST"]) +@scodoc @permission_required(Permission.ScoView) @scodoc7func(context) def change_password(user_name, password, password2, REQUEST): @@ -593,6 +601,7 @@ def change_password(user_name, password, password2, REQUEST): @bp.route("/delete_user_form", methods=["GET", "POST"]) +@scodoc @permission_required(Permission.ScoUsersAdmin) @scodoc7func(context) def delete_user_form(REQUEST, user_name, dialog_confirmed=False): diff --git a/scodoc.py b/scodoc.py index 48bfd3f2..a0fe96da 100755 --- a/scodoc.py +++ b/scodoc.py @@ -146,11 +146,22 @@ def user_password(username, password=None): # user-password @app.cli.command() @click.argument("dept") def sco_delete_dept(dept): # sco-delete-dept - "Delete existing departement" - raise NotImplementedError() - if os.system('tools/delete_dept.sh -n "{}"'.format(dept)): - sys.stderr.write("error deleting dept " + dept) - return 1 + """Delete existing departement""" + click.confirm( + f"""Attention: Cela va effacer toutes les données du département {dept} + (étudiants, notes, formations, etc) + Voulez-vous vraiment continuer ? + """, + abort=True, + ) + db.reflect() + d = models.Departement.query.filter_by(acronym=dept).first() + if d is None: + sys.stderr.write(f"Erreur: le departement {dept} n'existe pas !") + return 2 + # XXX TODO: détruire les objets du département ! + db.session.delete(d) + db.session.commit() return 0 @@ -158,10 +169,9 @@ def sco_delete_dept(dept): # sco-delete-dept @click.argument("dept") def sco_create_dept(dept): # sco-create-dept "Create new departement" - raise NotImplementedError() - if os.system(f'tools/create_dept.sh -n "{dept}"'): - sys.stderr.write(f"error creating dept {dept}\n") - return 1 + d = models.Departement(acronym=dept) + db.session.add(d) + db.session.commit() return 0 diff --git a/tests/conftest.py b/tests/conftest.py index 8f3a853e..f2de2820 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ from flask import g from flask_login import login_user, logout_user, current_user from config import TestConfig +import app from app import db, create_app from app import initialize_scodoc_database, clear_scodoc_cache from app import models @@ -44,9 +45,13 @@ def test_client(): u.add_role(admin_role, TestConfig.DEPT_TEST) db.session.add(u) db.session.commit() - ndb.set_sco_dept(TestConfig.DEPT_TEST) # set db connection + # Creation département de Test + d = models.Departement(acronym=TestConfig.DEPT_TEST) + db.session.add(d) + db.session.commit() + app.set_sco_dept(TestConfig.DEPT_TEST) # set db connection yield client - # ndb.close_dept_connection() + ndb.close_db_connection() # Teardown: db.session.commit() db.session.remove() diff --git a/tests/unit/test_caches.py b/tests/unit/test_caches.py index 37392fee..d6ecb0bf 100644 --- a/tests/unit/test_caches.py +++ b/tests/unit/test_caches.py @@ -11,6 +11,7 @@ Usage: pytest tests/unit/test_caches.py from flask import current_app, g +import app from app import db from app.scodoc import sco_cache from app.scodoc import sco_evaluations @@ -25,7 +26,7 @@ context = None # #context def test_notes_table(test_client): """Test construction et cache de NotesTable.""" - ndb.set_sco_dept(DEPT) + app.set_sco_dept(DEPT) assert g.scodoc_dept == DEPT # prépare le département avec quelques semestres: run_sco_basic() @@ -49,7 +50,7 @@ def test_notes_table(test_client): def test_cache_evaluations(test_client): """""" # cherche un semestre ayant des evaluations - ndb.set_sco_dept(DEPT) + app.set_sco_dept(DEPT) # prépare le département avec quelques semestres: run_sco_basic() # diff --git a/tests/unit/test_departements.py b/tests/unit/test_departements.py new file mode 100644 index 00000000..58687c4a --- /dev/null +++ b/tests/unit/test_departements.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- + +"""Test ORM departement/formation/preferences + + +Utiliser comme: + pytest tests/unit/test_departements.py + +""" +from flask import g +import app +from app import db +from app.models import Departement, ScoPreference, FormSemestre, formsemestre +from app.scodoc import notesdb as ndb +from app.scodoc import sco_formsemestre +from app.scodoc import sco_preferences +from tests.unit import test_sco_basic + + +def test_preferences_orm(test_client): + """preferences, via ORM and legacy ScoDoc""" + d = Departement(acronym="TT") + p1 = ScoPreference(name="temperature", value="24", departement=d) + p2 = ScoPreference(name="couleur", value="bleue", departement=d) + db.session.add(d) + db.session.add(p1) + db.session.add(p2) + db.session.commit() + prefs = d.preferences.all() + assert isinstance(prefs, list) + assert len(prefs) == 2 + + +def test_preferences(test_client): + """ScoDoc preferences""" + # preferences "globales" d'un département: + current_dept = Departement.query.filter_by(acronym=g.scodoc_dept).first() + prefs = sco_preferences.get_base_preferences() + assert isinstance(prefs, sco_preferences.BasePreferences) + assert prefs.dept_id == current_dept.id + # Compare nombre de d'items + assert len(ScoPreference.query.filter_by(dept_id=current_dept.id).all()) == len( + prefs + ) + # Accès à une valeur via ORM + assert ( + len( + ScoPreference.query.filter_by( + dept_id=current_dept.id, name="abs_notification_mail_tmpl" + ).all() + ) + == 1 + ) + orm_val = ( + ScoPreference.query.filter_by( + dept_id=current_dept.id, name="abs_notification_mail_tmpl" + ) + .first() + .value + ) + # Compare valeurs + sco_val = prefs.get(None, "abs_notification_mail_tmpl") + assert orm_val.strip() == sco_val.strip() + # nb: I don't understand why SQLAlchemy strips the string ?! + + # --- Charge dans un autre département + # departement fictif créé ici: + d = Departement(acronym="D2") + db.session.add(d) + db.session.commit() + app.set_sco_dept("D2") + prefs2 = sco_preferences.get_base_preferences() + assert len(prefs2) == len(prefs) + prefs2.set(None, "abs_notification_mail_tmpl", "toto") + assert prefs2.get(None, "abs_notification_mail_tmpl") == "toto" + assert prefs.get(None, "abs_notification_mail_tmpl") != "toto" + orm_val = ( + ScoPreference.query.filter_by(dept_id=d.id, name="abs_notification_mail_tmpl") + .first() + .value + ) + assert orm_val == "toto" + # --- Preferences d'un semestre + # rejoure ce test pour avoir un semestre créé + test_sco_basic.run_sco_basic() + sem = sco_formsemestre.do_formsemestre_list(None)[0] + formsemestre_id = sem["formsemestre_id"] + semp = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id) + assert semp["abs_notification_mail_tmpl"] == "toto" + assert semp.is_global("abs_notification_mail_tmpl") + # donne une valeur pour le semestre: + prefs2.set(formsemestre_id, "abs_notification_mail_tmpl", "foo") + assert not semp.is_global("abs_notification_mail_tmpl") + assert semp["abs_notification_mail_tmpl"] == "foo" diff --git a/tests/unit/test_sco_basic.py b/tests/unit/test_sco_basic.py index 9817d3ce..e4b3be72 100644 --- a/tests/unit/test_sco_basic.py +++ b/tests/unit/test_sco_basic.py @@ -7,8 +7,8 @@ Utiliser comme: pytest tests/unit/test_sco_basic.py -Au préalable, créer un département de test neuf: - flask sco-delete-dept TEST00 && flask sco-create-dept TEST00 +Au besoin, créer un base de test neuve: + ./tools/create_database.sh SCODOC_TEST """ import random @@ -18,6 +18,7 @@ from flask import g from config import TestConfig from tests.unit import sco_fake_gen +import app from app.scodoc import notesdb as ndb from app.scodoc import sco_abs from app.scodoc import sco_abs_views @@ -38,7 +39,7 @@ def test_sco_basic(test_client): Création 10 étudiants, formation, semestre, inscription etudiant, creation 1 evaluation, saisie 10 notes. """ - ndb.set_sco_dept(DEPT) + app.set_sco_dept(DEPT) run_sco_basic()