From 369b45a8c4e534a1677f7044bc603d5161698f33 Mon Sep 17 00:00:00 2001 From: Emmanuel Viennet Date: Mon, 31 May 2021 00:14:15 +0200 Subject: [PATCH] WIP: migration de ZNotes, decorateurs, etc. --- README.md | 7 + app/__init__.py | 17 +- app/decorators.py | 195 +- app/main/routes.py | 10 +- app/scodoc/ZNotes.py | 3316 ------------------------------ app/scodoc/ZScolar.py | 9 +- app/scodoc/sco_exceptions.py | 6 + app/scodoc/sco_permissions.py | 2 + app/templates/main/index.html | 2 +- app/views/__init__.py | 5 +- app/views/absences.py | 37 + app/views/essais.py | 65 + app/views/notes.py | 3548 ++++++++++++++++++++++++++++++++- app/views/scolar.py | 37 + config.py | 20 +- requirements.txt | 5 + scodoc_manager.py | 70 + 17 files changed, 3886 insertions(+), 3465 deletions(-) mode change 100755 => 100644 app/__init__.py delete mode 100644 app/scodoc/ZNotes.py create mode 100644 app/views/absences.py create mode 100644 app/views/essais.py create mode 100644 app/views/scolar.py mode change 100644 => 100755 config.py create mode 100644 scodoc_manager.py diff --git a/README.md b/README.md index e9c058b1..828b5ec9 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,13 @@ pour régénerer ce fichier: pip freeze > requirements.txt +### Bidouilles temporaires + +Installer le bon vieux `pyExcelerator` dans l'environnement: + + (cd /tmp; tar xfz /opt/scodoc/Products/ScoDoc/config/softs/pyExcelerator-0.6.3a.patched.tgz ) + (cd /tmp/pyExcelerator-0.6.3a.patched/; python setup.py install) + ## Lancement serveur (développement, sur VM Linux) export FLASK_APP=scodoc.py diff --git a/app/__init__.py b/app/__init__.py old mode 100755 new mode 100644 index 61fa4045..9d57e9e4 --- a/app/__init__.py +++ b/app/__init__.py @@ -43,9 +43,22 @@ def create_app(config_class=Config): app.register_blueprint(auth_bp, url_prefix="/auth") - from app.views import notes_bp + from app.views import essais_bp - app.register_blueprint(notes_bp, url_prefix="/ScoDoc") + app.register_blueprint(essais_bp, url_prefix="/Essais") + + from app.views import scolar_bp + from app.views import notes_bp + from app.views import absences_bp + + # https://scodoc.fr/ScoDoc/RT/Scolarite/... + app.register_blueprint(scolar_bp, url_prefix="/ScoDoc//Scolarite") + # https://scodoc.fr/ScoDoc/RT/Scolarite/Notes/... + app.register_blueprint(notes_bp, url_prefix="/ScoDoc//Scolarite/Notes") + # https://scodoc.fr/ScoDoc/RT/Scolarite/Absences/... + app.register_blueprint( + absences_bp, url_prefix="/ScoDoc//Scolarite/Absences" + ) from app.main import bp as main_bp diff --git a/app/decorators.py b/app/decorators.py index 9816e53a..b8bc694b 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -16,26 +16,6 @@ from werkzeug.exceptions import BadRequest from app.auth.models import Permission -def permission_required(permission): - def decorator(f): - @wraps(f) - def decorated_function(*args, **kwargs): - current_app.logger.info( - "permission_required: %s in %s" % (permission, g.scodoc_dept) - ) - if not current_user.has_permission(permission, g.scodoc_dept): - abort(403) - return f(*args, **kwargs) - - return decorated_function - - return decorator - - -def admin_required(f): - return permission_required(Permission.ScoSuperAdmin)(f) - - class ZUser(object): "Emulating Zope User" @@ -99,90 +79,118 @@ class ZResponse(object): self.headers[header.tolower()] = value -def scodoc7func(func): +def permission_required(permission): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if "scodoc_dept" in kwargs: + g.scodoc_dept = kwargs["scodoc_dept"] + del kwargs["scodoc_dept"] + current_app.logger.info( + "permission_required: %s in %s" % (permission, g.scodoc_dept) + ) + if not current_user.has_permission(permission, g.scodoc_dept): + abort(403) + return f(*args, **kwargs) + + return decorated_function + + return decorator + + +def admin_required(f): + return permission_required(Permission.ScoSuperAdmin)(f) + + +def scodoc7func(context): """Décorateur pour intégrer les fonctions Zope 2 de ScoDoc 7. Si on a un kwarg `scodoc_dept`(venant de la route), le stocke dans `g.scodoc_dept`. Ajoute l'argument REQUEST s'il est dans la signature de la fonction. Les paramètres de la query string deviennent des (keywords) paramètres de la fonction. """ - @wraps(func) - def scodoc7func_decorator(*args, **kwargs): - """Decorator allowing legacy Zope published methods to be called via Flask - routes without modification. + def s7_decorator(func): + @wraps(func) + def scodoc7func_decorator(*args, **kwargs): + """Decorator allowing legacy Zope published methods to be called via Flask + routes without modification. - There are two cases: the function can be called - 1. via a Flask route ("top level call") - 2. or be called directly from Python. + There are two cases: the function can be called + 1. via a Flask route ("top level call") + 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`). - """ - assert not args - if hasattr(g, "zrequest"): - top_level = False - else: - g.zrequest = None - top_level = True - # - if "scodoc_dept" in kwargs: - g.scodoc_dept = kwargs["scodoc_dept"] - del kwargs["scodoc_dept"] - elif not hasattr(g, "scodoc_dept"): # if toplevel call - g.scodoc_dept = None - # --- Emulate Zope's REQUEST - REQUEST = ZRequest() - g.zrequest = REQUEST - req_args = REQUEST.form # args from query string (get) or form (post) - # --- Add positional arguments - pos_arg_values = [] - # PY3 à remplacer par inspect.getfullargspec en py3: - argspec = inspect.getargspec(func) - current_app.logger.info("argspec=%s" % str(argspec)) - nb_default_args = len(argspec.defaults) if argspec.defaults else 0 - if nb_default_args: - arg_names = argspec.args[:-nb_default_args] - else: - arg_names = argspec.args - for arg_name in arg_names: - if arg_name == "REQUEST": # special case - pos_arg_values.append(REQUEST) + 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`). + """ + assert not args + # Détermine si on est appelé via une route ("toplevel") + # ou par un appel de fonction python normal. + top_level = not hasattr(g, "zrequest") + if top_level: + g.zrequest = None + # + if "scodoc_dept" in kwargs: + g.scodoc_dept = kwargs["scodoc_dept"] + del kwargs["scodoc_dept"] + elif not hasattr(g, "scodoc_dept"): # if toplevel call + g.scodoc_dept = None + # --- Emulate Zope's REQUEST + REQUEST = ZRequest() + g.zrequest = REQUEST + req_args = REQUEST.form # args from query string (get) or form (post) + # --- Add positional arguments + pos_arg_values = [] + # PY3 à remplacer par inspect.getfullargspec en py3: + argspec = inspect.getargspec(func) + current_app.logger.info("argspec=%s" % str(argspec)) + nb_default_args = len(argspec.defaults) if argspec.defaults else 0 + if nb_default_args: + arg_names = argspec.args[:-nb_default_args] else: - pos_arg_values.append(req_args[arg_name]) - current_app.logger.info("pos_arg_values=%s" % pos_arg_values) - # Add keyword arguments - if nb_default_args: - for arg_name in argspec.args[-nb_default_args:]: + arg_names = argspec.args + for arg_name in arg_names: if arg_name == "REQUEST": # special case - kwargs[arg_name] = REQUEST - elif arg_name in req_args: - # set argument kw optionnel - kwargs[arg_name] = req_args[arg_name] - current_app.logger.info( - "scodoc7func_decorator: top_level=%s, pos_arg_values=%s, kwargs=%s" - % (top_level, pos_arg_values, kwargs) - ) - value = func(*pos_arg_values, **kwargs) + pos_arg_values.append(REQUEST) + elif arg_name == "context": + pos_arg_values.append(context) + else: + pos_arg_values.append(req_args[arg_name]) + current_app.logger.info("pos_arg_values=%s" % pos_arg_values) + # Add keyword arguments + if nb_default_args: + for arg_name in argspec.args[-nb_default_args:]: + if arg_name == "REQUEST": # special case + kwargs[arg_name] = REQUEST + elif arg_name in req_args: + # set argument kw optionnel + kwargs[arg_name] = req_args[arg_name] + current_app.logger.info( + "scodoc7func_decorator: top_level=%s, pos_arg_values=%s, kwargs=%s" + % (top_level, pos_arg_values, kwargs) + ) + value = func(*pos_arg_values, **kwargs) - if not top_level: - return value - else: - # Build response, adding collected http headers: - headers = [] - kw = {"response": value, "status": 200} - if g.zrequest: - headers = g.zrequest.RESPONSE.headers - if not headers: - # no customized header, speedup: - return value - if "content-type" in headers: - kw["mimetype"] = headers["content-type"] - r = flask.Response(**kw) - for h in headers: - r.headers[h] = headers[h] - return r + if not top_level: + return value + else: + # Build response, adding collected http headers: + headers = [] + kw = {"response": value, "status": 200} + if g.zrequest: + headers = g.zrequest.RESPONSE.headers + if not headers: + # no customized header, speedup: + return value + if "content-type" in headers: + kw["mimetype"] = headers["content-type"] + r = flask.Response(**kw) + for h in headers: + r.headers[h] = headers[h] + return r - return scodoc7func_decorator + return scodoc7func_decorator + + return s7_decorator # Le "context" de ScoDoc7 @@ -193,3 +201,6 @@ class ScoDoc7Context(object): def __init__(self, globals_dict): self.__dict__ = globals_dict + + def __repr__(self): + return "ScoDoc7Context()" diff --git a/app/main/routes.py b/app/main/routes.py index d36545b9..a0235849 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -14,6 +14,8 @@ from app.main import bp from app.decorators import scodoc7func, admin_required +context = None + @bp.route("/") @bp.route("/index") @@ -47,7 +49,7 @@ D = {"count": 0} @bp.route("/zopefunction", methods=["POST", "GET"]) @login_required -@scodoc7func +@scodoc7func(context) def a_zope_function(y, x="defaut", REQUEST=None): """Une fonction typique de ScoDoc7""" H = get_request_infos() + [ @@ -64,7 +66,7 @@ def a_zope_function(y, x="defaut", REQUEST=None): @bp.route("/zopeform_get") -@scodoc7func +@scodoc7func(context) def a_zope_form_get(REQUEST=None): H = [ """

Formulaire GET

@@ -81,7 +83,7 @@ def a_zope_form_get(REQUEST=None): @bp.route("/zopeform_post") -@scodoc7func +@scodoc7func(context) def a_zope_form_post(REQUEST=None): H = [ """

Formulaire POST

@@ -98,7 +100,7 @@ def a_zope_form_post(REQUEST=None): @bp.route("/ScoDoc//Scolarite/Notes/formsemestre_status") -@scodoc7func +@scodoc7func(context) def formsemestre_status(dept_id=None, formsemestre_id=None, REQUEST=None): """Essai méthode de département Le contrôle d'accès doit vérifier les bons rôles : ici Ens diff --git a/app/scodoc/ZNotes.py b/app/scodoc/ZNotes.py deleted file mode 100644 index 9cc2e6dd..00000000 --- a/app/scodoc/ZNotes.py +++ /dev/null @@ -1,3316 +0,0 @@ -# -*- 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 -# -############################################################################## - -"""Interface Zope <-> Notes -""" -import time -import datetime -import jaxml -import pprint - -from sco_zope import * # pylint: disable=unused-wildcard-import - -# --------------- -import sco_utils as scu -import notesdb as ndb -from notes_log import log, sendAlarm -import scolog -from scolog import logdb -from sco_permissions import ( - ScoView, - ScoEnsView, - ScoImplement, - ScoChangeFormation, - ScoObservateur, - ScoEtudInscrit, - ScoEtudChangeGroups, - ScoEtudChangeAdr, - ScoEtudSupprAnnotations, - ScoEditAllEvals, - ScoEditAllNotes, - ScoEditFormationTags, - ScoEditApo, - ScoSuperAdmin, -) -from sco_exceptions import ScoValueError, ScoLockedFormError, ScoGenError, AccessDenied - -from TrivialFormulator import TrivialFormulator -import htmlutils -import sco_excel - -# import notes_users -from gen_tables import GenTable -import sco_cache -import scolars -import sco_news -from sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC - -import sco_formsemestre -import sco_formsemestre_edit -import sco_formsemestre_status -import sco_formsemestre_inscriptions -import sco_formsemestre_custommenu -import sco_moduleimpl -import sco_moduleimpl_status -import sco_moduleimpl_inscriptions -import sco_evaluations -import sco_groups -import sco_edit_ue -import sco_edit_formation -import sco_edit_matiere -import sco_edit_module -import sco_tag_module -import sco_bulletins -import sco_bulletins_pdf -import sco_compute_moy -import sco_recapcomplet -import sco_liste_notes -import sco_saisie_notes -import sco_placement -import sco_undo_notes -import sco_formations -import sco_report -import sco_lycee -import sco_poursuite_dut -import pe_view -import sco_debouche -import sco_ue_external -import sco_cost_formation -import sco_formsemestre_validation -import sco_parcours_dut -import sco_codes_parcours -import sco_pvjury -import sco_pvpdf -import sco_prepajury -import sco_inscr_passage -import sco_synchro_etuds -import sco_archives -import sco_apogee_csv -import sco_etape_apogee_view -import sco_apogee_compare -import sco_semset -import sco_export_results -import sco_formsemestre_exterieurs - -from sco_pdf import PDFLOCK -import notes_table -from notes_table import NOTES_CACHE_INST, CacheNotesTable -import VERSION - -# -# Cache global: chaque instance, repérée par sa connexion db, a un cache -# qui est recréé à la demande -# -CACHE_formsemestre_inscription = {} -CACHE_evaluations = {} - -# --------------- - - -class ZNotes(ObjectManager, PropertyManager, RoleManager, Item, Persistent, Implicit): - - "ZNotes object" - - meta_type = "ZNotes" - security = ClassSecurityInfo() - - # This is the list of the methods associated to 'tabs' in the ZMI - # Be aware that The first in the list is the one shown by default, so if - # the 'View' tab is the first, you will never see your tabs by cliquing - # on the object. - manage_options = ( - ({"label": "Contents", "action": "manage_main"},) - + PropertyManager.manage_options # add the 'Properties' tab - + ( - # this line is kept as an example with the files : - # dtml/manage_editZScolarForm.dtml - # html/ZScolar-edit.stx - # {'label': 'Properties', 'action': 'manage_editForm',}, - {"label": "View", "action": "index_html"}, - ) - + Item.manage_options # add the 'Undo' & 'Owner' tab - + RoleManager.manage_options # add the 'Security' tab - ) - - # no permissions, only called from python - def __init__(self, id, title): - "initialise a new instance of ZNotes" - self.id = id - self.title = title - - # The form used to edit this object - security.declareProtected(ScoView, "manage_editZNotes") - - def manage_editZNotes(self, title, RESPONSE=None): - "Changes the instance values" - self.title = title - self._p_changed = 1 - return RESPONSE.redirect("manage_editForm") - - def _getNotesCache(self): - "returns CacheNotesTable instance for us" - u = self.GetDBConnexionString() # identifie le dept de facon fiable - if not NOTES_CACHE_INST.has_key(u): - log("getNotesCache: creating cache for %s" % u) - NOTES_CACHE_INST[u] = CacheNotesTable() - return NOTES_CACHE_INST[u] - - def _inval_cache( - self, formsemestre_id=None, pdfonly=False, formsemestre_id_list=None - ): # > - "expire cache pour un semestre (ou tous si pas d'argument)" - if formsemestre_id_list: - for formsemestre_id in formsemestre_id_list: - self._getNotesCache().inval_cache( - self, formsemestre_id=formsemestre_id, pdfonly=pdfonly - ) - # Affecte aussi cache inscriptions - self.get_formsemestre_inscription_cache().inval_cache( - key=formsemestre_id - ) - else: - self._getNotesCache().inval_cache( - self, formsemestre_id=formsemestre_id, pdfonly=pdfonly - ) - # Affecte aussi cache inscriptions - self.get_formsemestre_inscription_cache().inval_cache(key=formsemestre_id) - - security.declareProtected(ScoView, "clearcache") - - def clearcache(self, REQUEST=None): - "Efface les caches de notes (utile pendant developpement slt)" - log("*** clearcache request") - # Debugging code: compare results before and after cache reconstruction - # (_should_ be identicals !) - # Compare XML representation - cache = self._getNotesCache() - formsemestre_ids = cache.get_cached_formsemestre_ids() - docs_before = [] - for formsemestre_id in formsemestre_ids: - docs_before.append( - sco_recapcomplet.do_formsemestre_recapcomplet( - self, REQUEST, formsemestre_id, format="xml", xml_nodate=True - ) - ) - # - cache.inval_cache(self) # > - # Rebuild cache (useful only to debug) - docs_after = [] - for formsemestre_id in formsemestre_ids: - docs_after.append( - sco_recapcomplet.do_formsemestre_recapcomplet( - self, REQUEST, formsemestre_id, format="xml", xml_nodate=True - ) - ) - if docs_before != docs_after: - log("clearcache: inconsistency !") - txt = "before=" + repr(docs_before) + "\n\nafter=" + repr(docs_after) + "\n" - log(txt) - sendAlarm(self, "clearcache: inconsistency !", txt) - - # -------------------------------------------------------------------- - # - # NOTES (top level) - # - # -------------------------------------------------------------------- - # XXX essai - security.declareProtected(ScoView, "gloups") - - def gloups(self, REQUEST): - "essai gloups" - H = [ - "

REQUEST.URL =%s

" % REQUEST.URL, - "

REQUEST.URL0=%s

" % REQUEST.URL0, - "

REQUEST.URL1=%s

" % REQUEST.URL1, - "

REQUEST.BASE0=%s

" % REQUEST.BASE0, - "

REQUEST.QUERY_STRING=%s

" % REQUEST.QUERY_STRING, - "

REQUEST.REQUEST_METHOD=%s (%s)

" - % (REQUEST.REQUEST_METHOD, type(REQUEST.REQUEST_METHOD)), - ] - return "\n".join(H) - # return pdfbulletins.essaipdf(REQUEST) - # return scu.sendPDFFile(REQUEST, pdfbulletins.pdftrombino(0,0), 'toto.pdf' ) - - # Python methods: - security.declareProtected(ScoView, "formsemestre_status") - formsemestre_status = sco_formsemestre_status.formsemestre_status - - security.declareProtected(ScoImplement, "formsemestre_createwithmodules") - formsemestre_createwithmodules = ( - sco_formsemestre_edit.formsemestre_createwithmodules - ) - - security.declareProtected( - ScoView, "formsemestre_editwithmodules" - ) # controle d'acces specifique pour dir. etud - formsemestre_editwithmodules = sco_formsemestre_edit.formsemestre_editwithmodules - - security.declareProtected(ScoImplement, "formsemestre_clone") - formsemestre_clone = sco_formsemestre_edit.formsemestre_clone - - security.declareProtected(ScoChangeFormation, "formsemestre_associate_new_version") - formsemestre_associate_new_version = ( - sco_formsemestre_edit.formsemestre_associate_new_version - ) - - security.declareProtected(ScoImplement, "formsemestre_delete") - formsemestre_delete = sco_formsemestre_edit.formsemestre_delete - security.declareProtected(ScoImplement, "formsemestre_delete2") - formsemestre_delete2 = sco_formsemestre_edit.formsemestre_delete2 - - security.declareProtected(ScoView, "formsemestre_recapcomplet") - formsemestre_recapcomplet = sco_recapcomplet.formsemestre_recapcomplet - - security.declareProtected(ScoObservateur, "formsemestres_bulletins") - formsemestres_bulletins = sco_recapcomplet.formsemestres_bulletins - - security.declareProtected(ScoView, "moduleimpl_status") - moduleimpl_status = sco_moduleimpl_status.moduleimpl_status - - security.declareProtected(ScoView, "formsemestre_description") - formsemestre_description = sco_formsemestre_status.formsemestre_description - - security.declareProtected(ScoView, "formsemestre_lists") - formsemestre_lists = sco_formsemestre_status.formsemestre_lists - - security.declareProtected(ScoView, "formsemestre_status_menubar") - formsemestre_status_menubar = sco_formsemestre_status.formsemestre_status_menubar - security.declareProtected(ScoChangeFormation, "formation_create") - formation_create = sco_edit_formation.formation_create - security.declareProtected(ScoChangeFormation, "formation_delete") - formation_delete = sco_edit_formation.formation_delete - security.declareProtected(ScoChangeFormation, "formation_edit") - formation_edit = sco_edit_formation.formation_edit - - security.declareProtected(ScoView, "formsemestre_bulletinetud") - formsemestre_bulletinetud = sco_bulletins.formsemestre_bulletinetud - - security.declareProtected(ScoView, "formsemestre_evaluations_cal") - formsemestre_evaluations_cal = sco_evaluations.formsemestre_evaluations_cal - security.declareProtected(ScoView, "formsemestre_evaluations_delai_correction") - formsemestre_evaluations_delai_correction = ( - sco_evaluations.formsemestre_evaluations_delai_correction - ) - - security.declareProtected(ScoView, "module_evaluation_renumber") - module_evaluation_renumber = sco_evaluations.module_evaluation_renumber - security.declareProtected(ScoView, "module_evaluation_move") - module_evaluation_move = sco_evaluations.module_evaluation_move - - security.declareProtected(ScoView, "formsemestre_list_saisies_notes") - formsemestre_list_saisies_notes = sco_undo_notes.formsemestre_list_saisies_notes - - security.declareProtected(ScoChangeFormation, "ue_create") - ue_create = sco_edit_ue.ue_create - security.declareProtected(ScoChangeFormation, "ue_delete") - ue_delete = sco_edit_ue.ue_delete - security.declareProtected(ScoChangeFormation, "ue_edit") - ue_edit = sco_edit_ue.ue_edit - security.declareProtected(ScoView, "ue_list") - ue_list = sco_edit_ue.ue_list - security.declareProtected(ScoView, "ue_sharing_code") - ue_sharing_code = sco_edit_ue.ue_sharing_code - security.declareProtected(ScoChangeFormation, "edit_ue_set_code_apogee") - edit_ue_set_code_apogee = sco_edit_ue.edit_ue_set_code_apogee - security.declareProtected(ScoView, "formation_table_recap") - formation_table_recap = sco_edit_ue.formation_table_recap - security.declareProtected(ScoChangeFormation, "formation_add_malus_modules") - formation_add_malus_modules = sco_edit_module.formation_add_malus_modules - - security.declareProtected(ScoChangeFormation, "matiere_create") - matiere_create = sco_edit_matiere.matiere_create - security.declareProtected(ScoChangeFormation, "matiere_delete") - matiere_delete = sco_edit_matiere.matiere_delete - security.declareProtected(ScoChangeFormation, "matiere_edit") - matiere_edit = sco_edit_matiere.matiere_edit - - security.declareProtected(ScoChangeFormation, "module_create") - module_create = sco_edit_module.module_create - security.declareProtected(ScoChangeFormation, "module_delete") - module_delete = sco_edit_module.module_delete - security.declareProtected(ScoChangeFormation, "module_edit") - module_edit = sco_edit_module.module_edit - security.declareProtected(ScoChangeFormation, "edit_module_set_code_apogee") - edit_module_set_code_apogee = sco_edit_module.edit_module_set_code_apogee - security.declareProtected(ScoView, "module_list") - module_list = sco_edit_module.module_list - # Tags - security.declareProtected(ScoView, "module_tag_search") - module_tag_search = sco_tag_module.module_tag_search - security.declareProtected( - ScoView, "module_tag_set" - ) # should be ScoEditFormationTags, but not present in old installs => check in method - module_tag_set = sco_tag_module.module_tag_set - - # - security.declareProtected(ScoView, "index_html") - - def index_html(self, REQUEST=None): - "Page accueil formations" - - editable = REQUEST.AUTHENTICATED_USER.has_permission(ScoChangeFormation, self) - - H = [ - self.sco_header(REQUEST, page_title="Programmes formations"), - """

Programmes pédagogiques

- """, - ] - T = sco_formations.formation_list_table(self, REQUEST=REQUEST) - - H.append(T.html()) - - if editable: - H.append( - """

Créer une formation

-

Importer une formation (xml)

-

Une "formation" est un programme pédagogique structuré en UE, matières et modules. Chaque semestre se réfère à une formation. La modification d'une formation affecte tous les semestres qui s'y réfèrent.

- """ - ) - - H.append(self.sco_footer(REQUEST)) - return "\n".join(H) - - # -------------------------------------------------------------------- - # - # Notes Methods - # - # -------------------------------------------------------------------- - - # --- Formations - _formationEditor = ndb.EditableTable( - "notes_formations", - "formation_id", - ( - "formation_id", - "acronyme", - "titre", - "titre_officiel", - "version", - "formation_code", - "type_parcours", - "code_specialite", - ), - sortkey="acronyme", - ) - - security.declareProtected(ScoChangeFormation, "do_formation_create") - - def do_formation_create(self, args, REQUEST): - "create a formation" - cnx = self.GetDBConnexion() - # check unique acronyme/titre/version - a = args.copy() - if a.has_key("formation_id"): - del a["formation_id"] - F = self.formation_list(args=a) - if len(F) > 0: - log( - "do_formation_create: error: %d formations matching args=%s" - % (len(F), a) - ) - raise ScoValueError("Formation non unique (%s) !" % str(a)) - # Si pas de formation_code, l'enleve (default SQL) - if args.has_key("formation_code") and not args["formation_code"]: - del args["formation_code"] - # - r = self._formationEditor.create(cnx, args) - - sco_news.add( - self, - REQUEST, - typ=NEWS_FORM, - text="Création de la formation %(titre)s (%(acronyme)s)" % args, - ) - return r - - security.declareProtected(ScoChangeFormation, "do_formation_delete") - - def do_formation_delete(self, oid, REQUEST): - """delete a formation (and all its UE, matieres, modules) - XXX delete all ues, will break if there are validations ! USE WITH CARE ! - """ - F = self.formation_list(args={"formation_id": oid})[0] - if self.formation_has_locked_sems(oid): - raise ScoLockedFormError() - cnx = self.GetDBConnexion() - # delete all UE in this formation - ues = self.do_ue_list({"formation_id": oid}) - for ue in ues: - self._do_ue_delete(ue["ue_id"], REQUEST=REQUEST, force=True) - - self._formationEditor.delete(cnx, oid) - - # news - sco_news.add( - self, - REQUEST, - typ=NEWS_FORM, - object=oid, - text="Suppression de la formation %(acronyme)s" % F, - ) - - security.declareProtected(ScoView, "formation_list") - - def formation_list(self, format=None, REQUEST=None, formation_id=None, args={}): - """List formation(s) with given id, or matching args - (when args is given, formation_id is ignored). - """ - # logCallStack() - if not args: - if formation_id is None: - args = {} - else: - args = {"formation_id": formation_id} - cnx = self.GetDBConnexion() - r = self._formationEditor.list(cnx, args=args) - # log('%d formations found' % len(r)) - return scu.sendResult(REQUEST, r, name="formation", format=format) - - security.declareProtected(ScoView, "formation_export") - - def formation_export( - self, formation_id, export_ids=False, format=None, REQUEST=None - ): - "Export de la formation au format indiqué (xml ou json)" - return sco_formations.formation_export( - self, formation_id, export_ids=export_ids, format=format, REQUEST=REQUEST - ) - - security.declareProtected(ScoChangeFormation, "formation_import_xml") - - def formation_import_xml(self, file, REQUEST): - "import d'une formation en XML" - log("formation_import_xml") - doc = file.read() - return sco_formations.formation_import_xml(self, REQUEST, doc) - - security.declareProtected(ScoChangeFormation, "formation_import_xml_form") - - def formation_import_xml_form(self, REQUEST): - "form import d'une formation en XML" - H = [ - self.sco_header(page_title="Import d'une formation", REQUEST=REQUEST), - """

Import d'une formation

-

Création d'une formation (avec UE, matières, modules) - à partir un fichier XML (réservé aux utilisateurs avertis)

- """, - ] - footer = self.sco_footer(REQUEST) - tf = TrivialFormulator( - REQUEST.URL0, - REQUEST.form, - (("xmlfile", {"input_type": "file", "title": "Fichier XML", "size": 30}),), - submitlabel="Importer", - cancelbutton="Annuler", - ) - if tf[0] == 0: - return "\n".join(H) + tf[1] + footer - elif tf[0] == -1: - return REQUEST.RESPONSE.redirect(self.NotesURL()) - else: - formation_id, _, _ = self.formation_import_xml(tf[2]["xmlfile"], REQUEST) - - return ( - "\n".join(H) - + """

Import effectué !

-

Voir la formation

""" - % formation_id - + footer - ) - - security.declareProtected(ScoChangeFormation, "formation_create_new_version") - - def formation_create_new_version(self, formation_id, redirect=True, REQUEST=None): - "duplicate formation, with new version number" - xml = sco_formations.formation_export( - self, formation_id, export_ids=True, format="xml" - ) - new_id, modules_old2new, ues_old2new = sco_formations.formation_import_xml( - self, REQUEST, xml - ) - # news - F = self.formation_list(args={"formation_id": new_id})[0] - sco_news.add( - self, - REQUEST, - typ=NEWS_FORM, - object=new_id, - text="Nouvelle version de la formation %(acronyme)s" % F, - ) - if redirect: - return REQUEST.RESPONSE.redirect( - "ue_list?formation_id=" + new_id + "&msg=Nouvelle version !" - ) - else: - return new_id, modules_old2new, ues_old2new - - # --- UE - _ueEditor = ndb.EditableTable( - "notes_ue", - "ue_id", - ( - "ue_id", - "formation_id", - "acronyme", - "numero", - "titre", - "type", - "ue_code", - "ects", - "is_external", - "code_apogee", - "coefficient", - ), - sortkey="numero", - input_formators={"type": ndb.int_null_is_zero}, - output_formators={ - "numero": ndb.int_null_is_zero, - "ects": ndb.float_null_is_null, - "coefficient": ndb.float_null_is_zero, - }, - ) - - security.declareProtected(ScoChangeFormation, "do_ue_create") - - def do_ue_create(self, args, REQUEST): - "create an ue" - cnx = self.GetDBConnexion() - # check duplicates - ues = self.do_ue_list( - {"formation_id": args["formation_id"], "acronyme": args["acronyme"]} - ) - if ues: - raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"]) - # create - r = self._ueEditor.create(cnx, args) - - # news - F = self.formation_list(args={"formation_id": args["formation_id"]})[0] - sco_news.add( - self, - REQUEST, - typ=NEWS_FORM, - object=args["formation_id"], - text="Modification de la formation %(acronyme)s" % F, - ) - return r - - def _do_ue_delete(self, ue_id, delete_validations=False, REQUEST=None, force=False): - "delete UE and attached matieres (but not modules (it should ?))" - cnx = self.GetDBConnexion() - log( - "do_ue_delete: ue_id=%s, delete_validations=%s" - % (ue_id, delete_validations) - ) - # check - ue = self.do_ue_list({"ue_id": ue_id}) - if not ue: - raise ScoValueError("UE inexistante !") - ue = ue[0] - if self.ue_is_locked(ue["ue_id"]): - raise ScoLockedFormError() - # Il y a-t-il des etudiants ayant validé cette UE ? - # si oui, propose de supprimer les validations - validations = sco_parcours_dut.scolar_formsemestre_validation_list( - cnx, args={"ue_id": ue_id} - ) - if validations and not delete_validations and not force: - return self.confirmDialog( - "

%d étudiants ont validé l'UE %s (%s)

Si vous supprimez cette UE, ces validations vont être supprimées !

" - % (len(validations), ue["acronyme"], ue["titre"]), - dest_url="", - REQUEST=REQUEST, - target_variable="delete_validations", - cancel_url="ue_list?formation_id=%s" % ue["formation_id"], - parameters={"ue_id": ue_id, "dialog_confirmed": 1}, - ) - if delete_validations: - log("deleting all validations of UE %s" % ue_id) - ndb.SimpleQuery( - self, - "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", - {"ue_id": ue_id}, - ) - - # delete all matiere in this UE - mats = self.do_matiere_list({"ue_id": ue_id}) - for mat in mats: - self.do_matiere_delete(mat["matiere_id"], REQUEST) - # delete uecoef and events - ndb.SimpleQuery( - self, - "DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s", - {"ue_id": ue_id}, - ) - ndb.SimpleQuery( - self, "DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue_id} - ) - cnx = self.GetDBConnexion() - self._ueEditor.delete(cnx, ue_id) - self._inval_cache() # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?) - # news - F = self.formation_list(args={"formation_id": ue["formation_id"]})[0] - sco_news.add( - self, - REQUEST, - typ=NEWS_FORM, - object=ue["formation_id"], - text="Modification de la formation %(acronyme)s" % F, - ) - # - if not force: - return REQUEST.RESPONSE.redirect( - self.NotesURL() + "/ue_list?formation_id=" + str(ue["formation_id"]) - ) - else: - return None - - security.declareProtected(ScoView, "do_ue_list") - - def do_ue_list(self, *args, **kw): - "list UEs" - cnx = self.GetDBConnexion() - return self._ueEditor.list(cnx, *args, **kw) - - # --- Matieres - _matiereEditor = ndb.EditableTable( - "notes_matieres", - "matiere_id", - ("matiere_id", "ue_id", "numero", "titre"), - sortkey="numero", - output_formators={"numero": ndb.int_null_is_zero}, - ) - - security.declareProtected(ScoChangeFormation, "do_matiere_create") - - def do_matiere_create(self, args, REQUEST): - "create a matiere" - cnx = self.GetDBConnexion() - # check - ue = self.do_ue_list({"ue_id": args["ue_id"]})[0] - # create matiere - r = self._matiereEditor.create(cnx, args) - - # news - F = self.formation_list(args={"formation_id": ue["formation_id"]})[0] - sco_news.add( - self, - REQUEST, - typ=NEWS_FORM, - object=ue["formation_id"], - text="Modification de la formation %(acronyme)s" % F, - ) - return r - - security.declareProtected(ScoChangeFormation, "do_matiere_delete") - - def do_matiere_delete(self, oid, REQUEST): - "delete matiere and attached modules" - cnx = self.GetDBConnexion() - # check - mat = self.do_matiere_list({"matiere_id": oid})[0] - ue = self.do_ue_list({"ue_id": mat["ue_id"]})[0] - locked = self.matiere_is_locked(mat["matiere_id"]) - if locked: - log("do_matiere_delete: mat=%s" % mat) - log("do_matiere_delete: ue=%s" % ue) - log("do_matiere_delete: locked sems: %s" % locked) - raise ScoLockedFormError() - log("do_matiere_delete: matiere_id=%s" % oid) - # delete all modules in this matiere - mods = self.do_module_list({"matiere_id": oid}) - for mod in mods: - self.do_module_delete(mod["module_id"], REQUEST) - self._matiereEditor.delete(cnx, oid) - - # news - F = self.formation_list(args={"formation_id": ue["formation_id"]})[0] - sco_news.add( - self, - REQUEST, - typ=NEWS_FORM, - object=ue["formation_id"], - text="Modification de la formation %(acronyme)s" % F, - ) - - security.declareProtected(ScoView, "do_matiere_list") - - def do_matiere_list(self, *args, **kw): - "list matieres" - cnx = self.GetDBConnexion() - return self._matiereEditor.list(cnx, *args, **kw) - - security.declareProtected(ScoChangeFormation, "do_matiere_edit") - - def do_matiere_edit(self, *args, **kw): - "edit a matiere" - cnx = self.GetDBConnexion() - # check - mat = self.do_matiere_list({"matiere_id": args[0]["matiere_id"]})[0] - if self.matiere_is_locked(mat["matiere_id"]): - raise ScoLockedFormError() - # edit - self._matiereEditor.edit(cnx, *args, **kw) - self._inval_cache() # > modif matiere - - security.declareProtected(ScoView, "do_matiere_formation_id") - - def do_matiere_formation_id(self, matiere_id): - "get formation_id from matiere" - cnx = self.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - "select UE.formation_id from notes_matieres M, notes_ue UE where M.matiere_id = %(matiere_id)s and M.ue_id = UE.ue_id", - {"matiere_id": matiere_id}, - ) - res = cursor.fetchall() - return res[0][0] - - # --- Modules - _moduleEditor = ndb.EditableTable( - "notes_modules", - "module_id", - ( - "module_id", - "titre", - "code", - "abbrev", - "heures_cours", - "heures_td", - "heures_tp", - "coefficient", - "ue_id", - "matiere_id", - "formation_id", - "semestre_id", - "numero", - "code_apogee", - "module_type" - #'ects' - ), - sortkey="numero, code, titre", - output_formators={ - "heures_cours": ndb.float_null_is_zero, - "heures_td": ndb.float_null_is_zero, - "heures_tp": ndb.float_null_is_zero, - "numero": ndb.int_null_is_zero, - "coefficient": ndb.float_null_is_zero, - "module_type": ndb.int_null_is_zero - #'ects' : ndb.float_null_is_null - }, - ) - - security.declareProtected(ScoChangeFormation, "do_module_create") - - def do_module_create(self, args, REQUEST): - "create a module" - # create - cnx = self.GetDBConnexion() - r = self._moduleEditor.create(cnx, args) - - # news - F = self.formation_list(args={"formation_id": args["formation_id"]})[0] - sco_news.add( - self, - REQUEST, - typ=NEWS_FORM, - object=args["formation_id"], - text="Modification de la formation %(acronyme)s" % F, - ) - return r - - security.declareProtected(ScoChangeFormation, "do_module_delete") - - def do_module_delete(self, oid, REQUEST): - "delete module" - mod = self.do_module_list({"module_id": oid})[0] - if self.module_is_locked(mod["module_id"]): - raise ScoLockedFormError() - - # S'il y a des moduleimpls, on ne peut pas detruire le module ! - mods = sco_moduleimpl.do_moduleimpl_list(self, module_id=oid) - if mods: - err_page = self.confirmDialog( - message="""

Destruction du module impossible car il est utilisé dans des semestres existants !

""", - helpmsg="""Il faut d'abord supprimer le semestre. Mais il est peut être préférable de laisser ce programme intact et d'en créer une nouvelle version pour la modifier.""", - dest_url="ue_list", - parameters={"formation_id": mod["formation_id"]}, - REQUEST=REQUEST, - ) - raise ScoGenError(err_page) - # delete - cnx = self.GetDBConnexion() - self._moduleEditor.delete(cnx, oid) - - # news - F = self.formation_list(args={"formation_id": mod["formation_id"]})[0] - sco_news.add( - self, - REQUEST, - typ=NEWS_FORM, - object=mod["formation_id"], - text="Modification de la formation %(acronyme)s" % F, - ) - - security.declareProtected(ScoView, "do_module_list") - - def do_module_list(self, *args, **kw): - "list modules" - cnx = self.GetDBConnexion() - return self._moduleEditor.list(cnx, *args, **kw) - - security.declareProtected(ScoChangeFormation, "do_module_edit") - - def do_module_edit(self, val): - "edit a module" - # check - mod = self.do_module_list({"module_id": val["module_id"]})[0] - if self.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] - # edit - cnx = self.GetDBConnexion() - self._moduleEditor.edit(cnx, val) - - sems = sco_formsemestre.do_formsemestre_list( - self, args={"formation_id": mod["formation_id"]} - ) - if sems: - self._inval_cache( - formsemestre_id_list=[s["formsemestre_id"] for s in sems] - ) # > modif module - - # - security.declareProtected(ScoView, "formation_has_locked_sems") - - def formation_has_locked_sems(self, formation_id): - "True if there is a locked formsemestre in this formation" - sems = sco_formsemestre.do_formsemestre_list( - self, args={"formation_id": formation_id, "etat": "0"} - ) - return sems - - security.declareProtected(ScoView, "formation_count_sems") - - def formation_count_sems(self, formation_id): - "Number of formsemestre in this formation (locked or not)" - sems = sco_formsemestre.do_formsemestre_list( - self, args={"formation_id": formation_id} - ) - return len(sems) - - security.declareProtected(ScoView, "module_count_moduleimpls") - - def module_count_moduleimpls(self, module_id): - "Number of moduleimpls using this module" - mods = sco_moduleimpl.do_moduleimpl_list(self, module_id=module_id) - return len(mods) - - security.declareProtected(ScoView, "module_is_locked") - - def module_is_locked(self, module_id): - """True if UE should not be modified - (used in a locked formsemestre) - """ - r = ndb.SimpleDictFetch( - self, - """SELECT mi.* from notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi - WHERE mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id - AND mi.module_id = %(module_id)s AND sem.etat = 0 - """, - {"module_id": module_id}, - ) - return len(r) > 0 - - security.declareProtected(ScoView, "matiere_is_locked") - - def matiere_is_locked(self, matiere_id): - """True if matiere should not be modified - (contains modules used in a locked formsemestre) - """ - r = ndb.SimpleDictFetch( - self, - """SELECT ma.* from notes_matieres ma, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi - WHERE ma.matiere_id = mod.matiere_id AND mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id - AND ma.matiere_id = %(matiere_id)s AND sem.etat = 0 - """, - {"matiere_id": matiere_id}, - ) - return len(r) > 0 - - security.declareProtected(ScoView, "ue_is_locked") - - def ue_is_locked(self, ue_id): - """True if module should not be modified - (contains modules used in a locked formsemestre) - """ - r = ndb.SimpleDictFetch( - self, - """SELECT ue.* FROM notes_ue ue, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi - WHERE ue.ue_id = mod.ue_id - AND mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id - AND ue.ue_id = %(ue_id)s AND sem.etat = 0 - """, - {"ue_id": ue_id}, - ) - return len(r) > 0 - - security.declareProtected(ScoChangeFormation, "module_move") - - def module_move(self, module_id, after=0, REQUEST=None, redirect=1): - """Move before/after previous one (decrement/increment numero)""" - module = self.do_module_list({"module_id": module_id})[0] - redirect = int(redirect) - after = int(after) # 0: deplace avant, 1 deplace apres - if after not in (0, 1): - raise ValueError('invalid value for "after"') - formation_id = module["formation_id"] - others = self.do_module_list({"matiere_id": module["matiere_id"]}) - # log('others=%s' % others) - if len(others) > 1: - idx = [p["module_id"] for p in others].index(module_id) - # log('module_move: after=%s idx=%s' % (after, idx)) - neigh = None # object to swap with - if after == 0 and idx > 0: - neigh = others[idx - 1] - elif after == 1 and idx < len(others) - 1: - neigh = others[idx + 1] - if neigh: # - # swap numero between partition and its neighbor - # log('moving module %s' % module_id) - cnx = self.GetDBConnexion() - module["numero"], neigh["numero"] = neigh["numero"], module["numero"] - if module["numero"] == neigh["numero"]: - neigh["numero"] -= 2 * after - 1 - self._moduleEditor.edit(cnx, module) - self._moduleEditor.edit(cnx, neigh) - - # redirect to ue_list page: - if redirect: - return REQUEST.RESPONSE.redirect("ue_list?formation_id=" + formation_id) - - security.declareProtected(ScoChangeFormation, "ue_move") - - def ue_move(self, ue_id, after=0, REQUEST=None, redirect=1): - """Move UE before/after previous one (decrement/increment numero)""" - o = self.do_ue_list({"ue_id": ue_id})[0] - # log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after)) - redirect = int(redirect) - after = int(after) # 0: deplace avant, 1 deplace apres - if after not in (0, 1): - raise ValueError('invalid value for "after"') - formation_id = o["formation_id"] - others = self.do_ue_list({"formation_id": formation_id}) - if len(others) > 1: - idx = [p["ue_id"] for p in others].index(ue_id) - neigh = None # object to swap with - if after == 0 and idx > 0: - neigh = others[idx - 1] - elif after == 1 and idx < len(others) - 1: - neigh = others[idx + 1] - if neigh: # - # swap numero between partition and its neighbor - # log('moving ue %s (neigh #%s)' % (ue_id, neigh['numero'])) - cnx = self.GetDBConnexion() - o["numero"], neigh["numero"] = neigh["numero"], o["numero"] - if o["numero"] == neigh["numero"]: - neigh["numero"] -= 2 * after - 1 - self._ueEditor.edit(cnx, o) - self._ueEditor.edit(cnx, neigh) - # redirect to ue_list page - if redirect: - return REQUEST.RESPONSE.redirect( - "ue_list?formation_id=" + o["formation_id"] - ) - - # --- Semestres de formation - - security.declareProtected(ScoImplement, "do_formsemestre_create") - - def do_formsemestre_create(self, args, REQUEST, silent=False): - "create a formsemestre" - cnx = self.GetDBConnexion() - formsemestre_id = sco_formsemestre._formsemestreEditor.create(cnx, args) - if args["etapes"]: - args["formsemestre_id"] = formsemestre_id - sco_formsemestre.write_formsemestre_etapes(self, args) - if args["responsables"]: - args["formsemestre_id"] = formsemestre_id - sco_formsemestre.write_formsemestre_responsables(self, args) - - # create default partition - partition_id = sco_groups.partition_create( - self, formsemestre_id, default=True, redirect=0, REQUEST=REQUEST - ) - _group_id = sco_groups.createGroup( - self, partition_id, default=True, REQUEST=REQUEST - ) - - # news - if not args.has_key("titre"): - args["titre"] = "sans titre" - args["formsemestre_id"] = formsemestre_id - args["url"] = ( - "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args - ) - if not silent: - sco_news.add( - self, - REQUEST, - typ=NEWS_SEM, - text='Création du semestre %(titre)s' % args, - url=args["url"], - ) - return formsemestre_id - - security.declareProtected(ScoView, "formsemestre_list") - - def formsemestre_list( - self, - format=None, - REQUEST=None, - formsemestre_id=None, - formation_id=None, - etape_apo=None, - ): - """List formsemestres in given format. - kw can specify some conditions: examples: - formsemestre_list( format='json', formation_id='F777', REQUEST=REQUEST) - """ - # XAPI: new json api - args = {} - L = locals() - for argname in ("formsemestre_id", "formation_id", "etape_apo"): - if L[argname] is not None: - args[argname] = L[argname] - sems = sco_formsemestre.do_formsemestre_list(self, args=args) - # log('formsemestre_list: format="%s", %s semestres found' % (format,len(sems))) - return scu.sendResult(REQUEST, sems, name="formsemestre", format=format) - - security.declareProtected(ScoView, "XMLgetFormsemestres") - - def XMLgetFormsemestres(self, etape_apo=None, formsemestre_id=None, REQUEST=None): - """List all formsemestres matching etape, XML format - DEPRECATED: use formsemestre_list() - """ - log("Warning: calling deprecated XMLgetFormsemestres") - args = {} - if etape_apo: - args["etape_apo"] = etape_apo - if formsemestre_id: - args["formsemestre_id"] = formsemestre_id - if REQUEST: - REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) - doc = jaxml.XML_document(encoding=scu.SCO_ENCODING) - doc.formsemestrelist() - for sem in sco_formsemestre.do_formsemestre_list(self, args=args): - doc._push() - doc.formsemestre(sem) - doc._pop() - return repr(doc) - - security.declareProtected(ScoImplement, "do_formsemestre_edit") - do_formsemestre_edit = sco_formsemestre.do_formsemestre_edit - - security.declareProtected(ScoView, "formsemestre_edit_options") - formsemestre_edit_options = sco_formsemestre_edit.formsemestre_edit_options - - security.declareProtected(ScoView, "formsemestre_change_lock") - formsemestre_change_lock = sco_formsemestre_edit.formsemestre_change_lock - - security.declareProtected(ScoView, "formsemestre_change_publication_bul") - formsemestre_change_publication_bul = ( - sco_formsemestre_edit.formsemestre_change_publication_bul - ) - - security.declareProtected(ScoView, "view_formsemestre_by_etape") - view_formsemestre_by_etape = sco_formsemestre.view_formsemestre_by_etape - - def _check_access_diretud( - self, formsemestre_id, REQUEST, required_permission=ScoImplement - ): - """Check if access granted: responsable or ScoImplement - Return True|False, HTML_error_page - """ - authuser = REQUEST.AUTHENTICATED_USER - sem = sco_formsemestre.get_formsemestre(self, formsemestre_id) - header = self.sco_header(page_title="Accès interdit", REQUEST=REQUEST) - footer = self.sco_footer(REQUEST) - if (str(authuser) not in sem["responsables"]) and not authuser.has_permission( - required_permission, self - ): - return ( - False, - "\n".join( - [ - header, - "

Opération non autorisée pour %s

" % authuser, - "

Responsable de ce semestre : %s

" - % ", ".join(sem["responsables"]), - footer, - ] - ), - ) - else: - return True, "" - - security.declareProtected(ScoView, "formsemestre_custommenu_edit") - - def formsemestre_custommenu_edit(self, REQUEST, formsemestre_id): - "Dialogue modif menu" - # accessible à tous ! - return sco_formsemestre_custommenu.formsemestre_custommenu_edit( - self, formsemestre_id, REQUEST=REQUEST - ) - - security.declareProtected(ScoView, "formsemestre_custommenu_html") - formsemestre_custommenu_html = ( - sco_formsemestre_custommenu.formsemestre_custommenu_html - ) - - security.declareProtected(ScoView, "html_sem_header") - - def html_sem_header( - self, - REQUEST, - title, - sem=None, - with_page_header=True, - with_h2=True, - page_title=None, - **args - ): - "Titre d'une page semestre avec lien vers tableau de bord" - # sem now unused and thus optional... - if with_page_header: - h = self.sco_header( - REQUEST, page_title="%s" % (page_title or title), **args - ) - else: - h = "" - if with_h2: - return h + """

%s

""" % (title) - else: - return h - - # --- dialogue modif enseignants/moduleimpl - security.declareProtected(ScoView, "edit_enseignants_form") - - def edit_enseignants_form(self, REQUEST, moduleimpl_id): - "modif liste enseignants/moduleimpl" - M, sem = sco_moduleimpl.can_change_ens(self, REQUEST, moduleimpl_id) - # -- - header = self.html_sem_header( - REQUEST, - 'Enseignants du module %s' - % (moduleimpl_id, M["module"]["titre"]), - page_title="Enseignants du module %s" % M["module"]["titre"], - javascripts=["libjs/AutoSuggest.js"], - cssstyles=["css/autosuggest_inquisitor.css"], - bodyOnLoad="init_tf_form('')", - ) - footer = self.sco_footer(REQUEST) - - # Liste des enseignants avec forme pour affichage / saisie avec suggestion - userlist = self.Users.get_userlist() - login2display = {} # user_name : forme pour affichage = "NOM Prenom (login)" - for u in userlist: - login2display[u["user_name"]] = u["nomplogin"] - allowed_user_names = login2display.values() - - H = [ - "
  • %s (responsable)
  • " - % login2display.get(M["responsable_id"], M["responsable_id"]) - ] - for ens in M["ens"]: - H.append( - '
  • %s (supprimer)
  • ' - % ( - login2display.get(ens["ens_id"], ens["ens_id"]), - moduleimpl_id, - ens["ens_id"], - ) - ) - H.append("
") - F = """

Les enseignants d'un module ont le droit de - saisir et modifier toutes les notes des évaluations de ce module. -

-

Pour changer le responsable du module, passez par la - page "Modification du semestre", accessible uniquement au responsable de la formation (chef de département) -

- """ % ( - sem["formation_id"], - M["formsemestre_id"], - ) - - modform = [ - ("moduleimpl_id", {"input_type": "hidden"}), - ( - "ens_id", - { - "input_type": "text_suggest", - "size": 50, - "title": "Ajouter un enseignant", - "allowed_values": allowed_user_names, - "allow_null": False, - "text_suggest_options": { - "script": "Users/get_userlist_xml?", - "varname": "start", - "json": False, - "noresults": "Valeur invalide !", - "timeout": 60000, - }, - }, - ), - ] - tf = TrivialFormulator( - REQUEST.URL0, - REQUEST.form, - modform, - submitlabel="Ajouter enseignant", - cancelbutton="Annuler", - ) - if tf[0] == 0: - return header + "\n".join(H) + tf[1] + F + footer - elif tf[0] == -1: - return REQUEST.RESPONSE.redirect( - "moduleimpl_status?moduleimpl_id=" + moduleimpl_id - ) - else: - ens_id = self.Users.get_user_name_from_nomplogin(tf[2]["ens_id"]) - if not ens_id: - H.append( - '

Pour ajouter un enseignant, choisissez un nom dans le menu

' - ) - else: - # et qu'il n'est pas deja: - if ( - ens_id in [x["ens_id"] for x in M["ens"]] - or ens_id == M["responsable_id"] - ): - H.append( - '

Enseignant %s déjà dans la liste !

' - % ens_id - ) - else: - sco_moduleimpl.do_ens_create( - self, {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id} - ) - return REQUEST.RESPONSE.redirect( - "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id - ) - return header + "\n".join(H) + tf[1] + F + footer - - security.declareProtected(ScoView, "edit_moduleimpl_resp") - - def edit_moduleimpl_resp(self, REQUEST, moduleimpl_id): - """Changement d'un enseignant responsable de module - Accessible par Admin et dir des etud si flag resp_can_change_ens - """ - M, sem = sco_moduleimpl.can_change_module_resp(self, REQUEST, moduleimpl_id) - H = [ - self.html_sem_header( - REQUEST, - 'Modification du responsable du module %s' - % (moduleimpl_id, M["module"]["titre"]), - sem, - javascripts=["libjs/AutoSuggest.js"], - cssstyles=["css/autosuggest_inquisitor.css"], - bodyOnLoad="init_tf_form('')", - ) - ] - help = """

Taper le début du nom de l'enseignant.

""" - # Liste des enseignants avec forme pour affichage / saisie avec suggestion - userlist = self.Users.get_userlist() - login2display = {} # user_name : forme pour affichage = "NOM Prenom (login)" - for u in userlist: - login2display[u["user_name"]] = u["nomplogin"] - allowed_user_names = login2display.values() - - initvalues = M - initvalues["responsable_id"] = login2display.get( - M["responsable_id"], M["responsable_id"] - ) - form = [ - ("moduleimpl_id", {"input_type": "hidden"}), - ( - "responsable_id", - { - "input_type": "text_suggest", - "size": 50, - "title": "Responsable du module", - "allowed_values": allowed_user_names, - "allow_null": False, - "text_suggest_options": { - "script": "Users/get_userlist_xml?", - "varname": "start", - "json": False, - "noresults": "Valeur invalide !", - "timeout": 60000, - }, - }, - ), - ] - tf = TrivialFormulator( - REQUEST.URL0, - REQUEST.form, - form, - submitlabel="Changer responsable", - cancelbutton="Annuler", - initvalues=initvalues, - ) - if tf[0] == 0: - return "\n".join(H) + tf[1] + help + self.sco_footer(REQUEST) - elif tf[0] == -1: - return REQUEST.RESPONSE.redirect( - "moduleimpl_status?moduleimpl_id=" + moduleimpl_id - ) - else: - responsable_id = self.Users.get_user_name_from_nomplogin( - tf[2]["responsable_id"] - ) - if ( - not responsable_id - ): # presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps) - return REQUEST.RESPONSE.redirect( - "moduleimpl_status?moduleimpl_id=" + moduleimpl_id - ) - sco_moduleimpl.do_moduleimpl_edit( - self, - {"moduleimpl_id": moduleimpl_id, "responsable_id": responsable_id}, - formsemestre_id=sem["formsemestre_id"], - ) - return REQUEST.RESPONSE.redirect( - "moduleimpl_status?moduleimpl_id=" - + moduleimpl_id - + "&head_message=responsable%20modifié" - ) - - _expr_help = """

Expérimental: formule de calcul de la moyenne %(target)s

-

Attention: l'utilisation de formules ralenti considérablement - les traitements. A utiliser uniquement dans els cas ne pouvant pas être traités autrement.

-

Dans la formule, les variables suivantes sont définies:

-
    -
  • moy la moyenne, calculée selon la règle standard (moyenne pondérée)
  • -
  • moy_is_valid vrai si la moyenne est valide (numérique)
  • -
  • moy_val la valeur de la moyenne (nombre, valant 0 si invalide)
  • -
  • notes vecteur des notes (/20) aux %(objs)s
  • -
  • coefs vecteur des coefficients des %(objs)s, les coefs des %(objs)s sans notes (ATT, EXC) étant mis à zéro
  • -
  • cmask vecteur de 0/1, 0 si le coef correspondant a été annulé
  • -
  • Nombre d'absences: nb_abs, nb_abs_just, nb_abs_nojust (en demi-journées)
  • -
-

Les éléments des vecteurs sont ordonnés dans l'ordre des %(objs)s%(ordre)s.

-

Les fonctions suivantes sont utilisables: abs, cmp, dot, len, map, max, min, pow, reduce, round, sum, ifelse.

-

La notation V(1,2,3) représente un vecteur (1,2,3).

-

Pour indiquer que la note calculée n'existe pas, utiliser la chaîne 'NA'.

-

Vous pouvez désactiver la formule (et revenir au mode de calcul "classique") - en supprimant le texte ou en faisant précéder la première ligne par #

- """ - - security.declareProtected(ScoView, "edit_moduleimpl_expr") - - def edit_moduleimpl_expr(self, REQUEST, moduleimpl_id): - """Edition formule calcul moyenne module - Accessible par Admin, dir des etud et responsable module - """ - M, sem = sco_moduleimpl.can_change_ens(self, REQUEST, moduleimpl_id) - H = [ - self.html_sem_header( - REQUEST, - 'Modification règle de calcul du module %s' - % (moduleimpl_id, M["module"]["titre"]), - sem, - ), - self._expr_help - % { - "target": "du module", - "objs": "évaluations", - "ordre": " (le premier élément est la plus ancienne évaluation)", - }, - ] - initvalues = M - form = [ - ("moduleimpl_id", {"input_type": "hidden"}), - ( - "computation_expr", - { - "title": "Formule de calcul", - "input_type": "textarea", - "rows": 4, - "cols": 60, - "explanation": "formule de calcul (expérimental)", - }, - ), - ] - tf = TrivialFormulator( - REQUEST.URL0, - REQUEST.form, - form, - submitlabel="Modifier formule de calcul", - cancelbutton="Annuler", - initvalues=initvalues, - ) - if tf[0] == 0: - return "\n".join(H) + tf[1] + self.sco_footer(REQUEST) - elif tf[0] == -1: - return REQUEST.RESPONSE.redirect( - "moduleimpl_status?moduleimpl_id=" + moduleimpl_id - ) - else: - sco_moduleimpl.do_moduleimpl_edit( - self, - { - "moduleimpl_id": moduleimpl_id, - "computation_expr": tf[2]["computation_expr"], - }, - formsemestre_id=sem["formsemestre_id"], - ) - self._inval_cache( - formsemestre_id=sem["formsemestre_id"] - ) # > modif regle calcul - return REQUEST.RESPONSE.redirect( - "moduleimpl_status?moduleimpl_id=" - + moduleimpl_id - + "&head_message=règle%20de%20calcul%20modifiée" - ) - - security.declareProtected(ScoView, "view_module_abs") - - def view_module_abs(self, REQUEST, moduleimpl_id, format="html"): - """Visualisation des absences a un module""" - M = sco_moduleimpl.do_moduleimpl_withmodule_list( - self, moduleimpl_id=moduleimpl_id - )[0] - sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"]) - debut_sem = ndb.DateDMYtoISO(sem["date_debut"]) - fin_sem = ndb.DateDMYtoISO(sem["date_fin"]) - list_insc = sco_moduleimpl.do_moduleimpl_listeetuds(self, moduleimpl_id) - - T = [] - for etudid in list_insc: - nb_abs = self.Absences.CountAbs( - etudid=etudid, debut=debut_sem, fin=fin_sem, moduleimpl_id=moduleimpl_id - ) - if nb_abs: - nb_abs_just = self.Absences.CountAbsJust( - etudid=etudid, - debut=debut_sem, - fin=fin_sem, - moduleimpl_id=moduleimpl_id, - ) - etud = self.getEtudInfo(etudid=etudid, filled=True)[0] - T.append( - { - "nomprenom": etud["nomprenom"], - "just": nb_abs_just, - "nojust": nb_abs - nb_abs_just, - "total": nb_abs, - "_nomprenom_target": "ficheEtud?etudid=%s" % etudid, - } - ) - - H = [ - self.html_sem_header( - REQUEST, - 'Absences du module %s' - % (moduleimpl_id, M["module"]["titre"]), - page_title="Absences du module %s" % (M["module"]["titre"]), - sem=sem, - ) - ] - if not T and format == "html": - return ( - "\n".join(H) - + "

Aucune absence signalée

" - + self.sco_footer(REQUEST) - ) - - tab = GenTable( - titles={ - "nomprenom": "Nom", - "just": "Just.", - "nojust": "Non Just.", - "total": "Total", - }, - columns_ids=("nomprenom", "just", "nojust", "total"), - rows=T, - html_class="table_leftalign", - base_url="%s?moduleimpl_id=%s" % (REQUEST.URL0, moduleimpl_id), - filename="absmodule_" + scu.make_filename(M["module"]["titre"]), - caption="Absences dans le module %s" % M["module"]["titre"], - preferences=self.get_preferences(), - ) - - if format != "html": - return tab.make_page(self, format=format, REQUEST=REQUEST) - - return "\n".join(H) + tab.html() + self.sco_footer(REQUEST) - - security.declareProtected(ScoView, "edit_ue_expr") - - def edit_ue_expr(self, REQUEST, formsemestre_id, ue_id): - """Edition formule calcul moyenne UE""" - # Check access - sem = sco_formsemestre_edit.can_edit_sem(self, REQUEST, formsemestre_id) - if not sem: - raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération") - cnx = self.GetDBConnexion() - # - ue = self.do_ue_list({"ue_id": ue_id})[0] - H = [ - self.html_sem_header( - REQUEST, - "Modification règle de calcul de l'UE %s (%s)" - % (ue["acronyme"], ue["titre"]), - sem, - ), - self._expr_help % {"target": "de l'UE", "objs": "modules", "ordre": ""}, - ] - el = sco_compute_moy.formsemestre_ue_computation_expr_list( - cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id} - ) - if el: - initvalues = el[0] - else: - initvalues = {} - form = [ - ("ue_id", {"input_type": "hidden"}), - ("formsemestre_id", {"input_type": "hidden"}), - ( - "computation_expr", - { - "title": "Formule de calcul", - "input_type": "textarea", - "rows": 4, - "cols": 60, - "explanation": "formule de calcul (expérimental)", - }, - ), - ] - tf = TrivialFormulator( - REQUEST.URL0, - REQUEST.form, - form, - submitlabel="Modifier formule de calcul", - cancelbutton="Annuler", - initvalues=initvalues, - ) - if tf[0] == 0: - return "\n".join(H) + tf[1] + self.sco_footer(REQUEST) - elif tf[0] == -1: - return REQUEST.RESPONSE.redirect( - "formsemestre_status?formsemestre_id=" + formsemestre_id - ) - else: - if el: - el[0]["computation_expr"] = tf[2]["computation_expr"] - sco_compute_moy.formsemestre_ue_computation_expr_edit(cnx, el[0]) - else: - sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, tf[2]) - - self._inval_cache(formsemestre_id=formsemestre_id) # > modif regle calcul - return REQUEST.RESPONSE.redirect( - "formsemestre_status?formsemestre_id=" - + formsemestre_id - + "&head_message=règle%20de%20calcul%20modifiée" - ) - - security.declareProtected(ScoView, "formsemestre_enseignants_list") - - def formsemestre_enseignants_list(self, REQUEST, formsemestre_id, format="html"): - """Liste les enseignants intervenants dans le semestre (resp. modules et chargés de TD) - et indique les absences saisies par chacun. - """ - sem = sco_formsemestre.get_formsemestre(self, formsemestre_id) - # resp. de modules: - mods = sco_moduleimpl.do_moduleimpl_withmodule_list( - self, formsemestre_id=formsemestre_id - ) - sem_ens = {} - for mod in mods: - if not mod["responsable_id"] in sem_ens: - sem_ens[mod["responsable_id"]] = {"mods": [mod]} - else: - sem_ens[mod["responsable_id"]]["mods"].append(mod) - # charges de TD: - for mod in mods: - for ensd in mod["ens"]: - if not ensd["ens_id"] in sem_ens: - sem_ens[ensd["ens_id"]] = {"mods": [mod]} - else: - sem_ens[ensd["ens_id"]]["mods"].append(mod) - # compte les absences ajoutées par chacun dans tout le semestre - cnx = self.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - for ens in sem_ens: - cursor.execute( - "select * from scolog L, notes_formsemestre_inscription I where method='AddAbsence' and authenticated_user=%(authenticated_user)s and L.etudid = I.etudid and I.formsemestre_id=%(formsemestre_id)s and date > %(date_debut)s and date < %(date_fin)s", - { - "authenticated_user": ens, - "formsemestre_id": formsemestre_id, - "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), - "date_fin": ndb.DateDMYtoISO(sem["date_fin"]), - }, - ) - - events = cursor.dictfetchall() - sem_ens[ens]["nbabsadded"] = len(events) - - # description textuelle des modules - for ens in sem_ens: - sem_ens[ens]["descr_mods"] = ", ".join( - [x["module"]["code"] for x in sem_ens[ens]["mods"]] - ) - - # ajoute infos sur enseignant: - for ens in sem_ens: - sem_ens[ens].update(self.Users.user_info(ens)) - if sem_ens[ens]["email"]: - sem_ens[ens]["_email_target"] = "mailto:%s" % sem_ens[ens]["email"] - - sem_ens_list = sem_ens.values() - sem_ens_list.sort(lambda x, y: cmp(x["nomprenom"], y["nomprenom"])) - - # --- Generate page with table - title = "Enseignants de " + sem["titremois"] - T = GenTable( - columns_ids=["nom_fmt", "prenom_fmt", "descr_mods", "nbabsadded", "email"], - titles={ - "nom_fmt": "Nom", - "prenom_fmt": "Prénom", - "email": "Mail", - "descr_mods": "Modules", - "nbabsadded": "Saisies Abs.", - }, - rows=sem_ens_list, - html_sortable=True, - html_class="table_leftalign", - filename=scu.make_filename("Enseignants-" + sem["titreannee"]), - html_title=self.html_sem_header( - REQUEST, "Enseignants du semestre", sem, with_page_header=False - ), - base_url="%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id), - caption="Tous les enseignants (responsables ou associés aux modules de ce semestre) apparaissent. Le nombre de saisies d'absences est le nombre d'opérations d'ajout effectuées sur ce semestre, sans tenir compte des annulations ou double saisies.", - preferences=self.get_preferences(formsemestre_id), - ) - return T.make_page( - self, page_title=title, title=title, REQUEST=REQUEST, format=format - ) - - security.declareProtected(ScoView, "edit_enseignants_form_delete") - - def edit_enseignants_form_delete(self, REQUEST, moduleimpl_id, ens_id): - "remove ens" - M, _ = sco_moduleimpl.can_change_ens(self, REQUEST, moduleimpl_id) - # search ens_id - ok = False - for ens in M["ens"]: - if ens["ens_id"] == ens_id: - ok = True - break - if not ok: - raise ScoValueError("invalid ens_id (%s)" % ens_id) - sco_moduleimpl.do_ens_delete(self, ens["modules_enseignants_id"]) - return REQUEST.RESPONSE.redirect( - "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id - ) - - # --- Gestion des inscriptions aux modules - _formsemestre_inscriptionEditor = ndb.EditableTable( - "notes_formsemestre_inscription", - "formsemestre_inscription_id", - ("formsemestre_inscription_id", "etudid", "formsemestre_id", "etat", "etape"), - sortkey="formsemestre_id", - ) - - security.declareProtected(ScoEtudInscrit, "do_formsemestre_inscription_create") - - def do_formsemestre_inscription_create(self, args, REQUEST, method=None): - "create a formsemestre_inscription (and sco event)" - cnx = self.GetDBConnexion() - log("do_formsemestre_inscription_create: args=%s" % str(args)) - sems = sco_formsemestre.do_formsemestre_list( - self, {"formsemestre_id": args["formsemestre_id"]} - ) - if len(sems) != 1: - raise ScoValueError( - "code de semestre invalide: %s" % args["formsemestre_id"] - ) - sem = sems[0] - # check lock - if sem["etat"] != "1": - raise ScoValueError("inscription: semestre verrouille") - # - r = self._formsemestre_inscriptionEditor.create(cnx, args) - # Evenement - scolars.scolar_events_create( - cnx, - args={ - "etudid": args["etudid"], - "event_date": time.strftime("%d/%m/%Y"), - "formsemestre_id": args["formsemestre_id"], - "event_type": "INSCRIPTION", - }, - ) - # Log etudiant - logdb( - REQUEST, - cnx, - method=method, - etudid=args["etudid"], - msg="inscription en semestre %s" % args["formsemestre_id"], - commit=False, - ) - # - self._inval_cache( - formsemestre_id=args["formsemestre_id"] - ) # > inscription au semestre - return r - - security.declareProtected(ScoImplement, "do_formsemestre_inscription_delete") - - def do_formsemestre_inscription_delete(self, oid, formsemestre_id=None): - "delete formsemestre_inscription" - cnx = self.GetDBConnexion() - self._formsemestre_inscriptionEditor.delete(cnx, oid) - - self._inval_cache( - formsemestre_id=formsemestre_id - ) # > desinscription du semestre - - security.declareProtected(ScoView, "do_formsemestre_inscription_list") - - def do_formsemestre_inscription_list(self, *args, **kw): - "list formsemestre_inscriptions" - cnx = self.GetDBConnexion() - return self._formsemestre_inscriptionEditor.list(cnx, *args, **kw) - - security.declareProtected(ScoView, "do_formsemestre_inscription_listinscrits") - - def do_formsemestre_inscription_listinscrits( - self, formsemestre_id, format=None, REQUEST=None - ): - """Liste les inscrits (état I) à ce semestre et cache le résultat""" - cache = self.get_formsemestre_inscription_cache() - r = cache.get(formsemestre_id) - if r is None: - # retreive list - r = self.do_formsemestre_inscription_list( - args={"formsemestre_id": formsemestre_id, "etat": "I"} - ) - cache.set(formsemestre_id, r) - return scu.sendResult(REQUEST, r, format=format, name="inscrits") - - security.declareProtected(ScoImplement, "do_formsemestre_inscription_edit") - - def do_formsemestre_inscription_edit(self, args=None, formsemestre_id=None): - "edit a formsemestre_inscription" - cnx = self.GetDBConnexion() - self._formsemestre_inscriptionEditor.edit(cnx, args) - self._inval_cache( - formsemestre_id=formsemestre_id - ) # > modif inscription semestre (demission ?) - - # Cache inscriptions semestres - def get_formsemestre_inscription_cache(self, format=None): - u = self.GetDBConnexionString() - if CACHE_formsemestre_inscription.has_key(u): - return CACHE_formsemestre_inscription[u] - else: - log("get_formsemestre_inscription_cache: new simpleCache") - CACHE_formsemestre_inscription[u] = sco_cache.simpleCache() - return CACHE_formsemestre_inscription[u] - - security.declareProtected(ScoImplement, "formsemestre_desinscription") - - def formsemestre_desinscription( - self, etudid, formsemestre_id, REQUEST=None, dialog_confirmed=False - ): - """desinscrit l'etudiant de ce semestre (et donc de tous les modules). - A n'utiliser qu'en cas d'erreur de saisie. - S'il s'agit d'un semestre extérieur et qu'il n'y a plus d'inscrit, - le semestre sera supprimé. - """ - sem = sco_formsemestre.get_formsemestre(self, formsemestre_id) - # -- check lock - if sem["etat"] != "1": - raise ScoValueError("desinscription impossible: semestre verrouille") - - # -- Si décisions de jury, désinscription interdite - nt = self._getNotesCache().get_NotesTable(self, formsemestre_id) - if nt.etud_has_decision(etudid): - raise ScoValueError( - """Désinscription impossible: l'étudiant a une décision de jury - (la supprimer avant si nécessaire: - - supprimer décision jury - ) - """ - % (etudid, formsemestre_id) - ) - if not dialog_confirmed: - etud = self.getEtudInfo(etudid=etudid, filled=1)[0] - if sem["modalite"] != "EXT": - msg_ext = """ -

%s sera désinscrit de tous les modules du semestre %s (%s - %s).

-

Cette opération ne doit être utilisée que pour corriger une erreur ! - Un étudiant réellement inscrit doit le rester, le faire éventuellement démissionner. -

- """ % ( - etud["nomprenom"], - sem["titre_num"], - sem["date_debut"], - sem["date_fin"], - ) - else: # semestre extérieur - msg_ext = """ -

%s sera désinscrit du semestre extérieur %s (%s - %s).

- """ % ( - etud["nomprenom"], - sem["titre_num"], - sem["date_debut"], - sem["date_fin"], - ) - inscrits = self.do_formsemestre_inscription_list( - args={"formsemestre_id": formsemestre_id} - ) - nbinscrits = len(inscrits) - if nbinscrits <= 1: - msg_ext = """

Attention: le semestre extérieur sera supprimé - car il n'a pas d'autre étudiant inscrit. -

- """ - return self.confirmDialog( - """

Confirmer la demande de desinscription ?

""" + msg_ext, - dest_url="", - REQUEST=REQUEST, - cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id, - parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, - ) - - self.do_formsemestre_desinscription(etudid, formsemestre_id, REQUEST=REQUEST) - - return ( - self.sco_header(REQUEST) - + '

Etudiant désinscrit !

retour à la fiche' - % (self.ScoURL(), etudid) - + self.sco_footer(REQUEST) - ) - - security.declareProtected(ScoImplement, "do_formsemestre_desinscription") - - def do_formsemestre_desinscription(self, etudid, formsemestre_id, REQUEST=None): - """Désinscription d'un étudiant. - Si semestre extérieur et dernier inscrit, suppression de ce semestre. - """ - sem = sco_formsemestre.get_formsemestre(self, formsemestre_id) - # -- check lock - if sem["etat"] != "1": - raise ScoValueError("desinscription impossible: semestre verrouille") - - # -- Si decisions de jury, desinscription interdite - nt = self._getNotesCache().get_NotesTable(self, formsemestre_id) - if nt.etud_has_decision(etudid): - raise ScoValueError( - "desinscription impossible: l'étudiant a une décision de jury (la supprimer avant si nécessaire)" - ) - - insem = self.do_formsemestre_inscription_list( - args={"formsemestre_id": formsemestre_id, "etudid": etudid} - ) - if not insem: - raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid) - insem = insem[0] - # -- desinscription de tous les modules - cnx = self.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cursor.execute( - "select moduleimpl_inscription_id from notes_moduleimpl_inscription Im, notes_moduleimpl M where Im.etudid=%(etudid)s and Im.moduleimpl_id = M.moduleimpl_id and M.formsemestre_id = %(formsemestre_id)s", - {"etudid": etudid, "formsemestre_id": formsemestre_id}, - ) - res = cursor.fetchall() - moduleimpl_inscription_ids = [x[0] for x in res] - for moduleimpl_inscription_id in moduleimpl_inscription_ids: - sco_moduleimpl.do_moduleimpl_inscription_delete( - self, moduleimpl_inscription_id, formsemestre_id=formsemestre_id - ) - # -- desincription du semestre - self.do_formsemestre_inscription_delete( - insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id - ) - # --- Semestre extérieur - if sem["modalite"] == "EXT": - inscrits = self.do_formsemestre_inscription_list( - args={"formsemestre_id": formsemestre_id} - ) - nbinscrits = len(inscrits) - if nbinscrits == 0: - log( - "do_formsemestre_desinscription: suppression du semestre extérieur %s" - % formsemestre_id - ) - sco_formsemestre_edit.do_formsemestre_delete( - self, formsemestre_id, REQUEST=REQUEST - ) - - if REQUEST: - logdb( - REQUEST, - cnx, - method="formsemestre_desinscription", - etudid=etudid, - msg="desinscription semestre %s" % formsemestre_id, - commit=False, - ) - - security.declareProtected(ScoEtudInscrit, "etud_desinscrit_ue") - - def etud_desinscrit_ue(self, etudid, formsemestre_id, ue_id, REQUEST=None): - """Desinscrit l'etudiant de tous les modules de cette UE dans ce semestre.""" - sco_moduleimpl_inscriptions.do_etud_desinscrit_ue( - self, etudid, formsemestre_id, ue_id, REQUEST=REQUEST - ) - return REQUEST.RESPONSE.redirect( - self.ScoURL() - + "/Notes/moduleimpl_inscriptions_stats?formsemestre_id=" - + formsemestre_id - ) - - security.declareProtected(ScoEtudInscrit, "etud_inscrit_ue") - - def etud_inscrit_ue(self, etudid, formsemestre_id, ue_id, REQUEST=None): - """Inscrit l'etudiant de tous les modules de cette UE dans ce semestre.""" - sco_moduleimpl_inscriptions.do_etud_inscrit_ue( - self, etudid, formsemestre_id, ue_id, REQUEST=REQUEST - ) - return REQUEST.RESPONSE.redirect( - self.ScoURL() - + "/Notes/moduleimpl_inscriptions_stats?formsemestre_id=" - + formsemestre_id - ) - - # --- Inscriptions - security.declareProtected( - ScoEtudInscrit, "formsemestre_inscription_with_modules_form" - ) - formsemestre_inscription_with_modules_form = ( - sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_form - ) - - security.declareProtected( - ScoEtudInscrit, "formsemestre_inscription_with_modules_etud" - ) - formsemestre_inscription_with_modules_etud = ( - sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_etud - ) - - security.declareProtected(ScoEtudInscrit, "formsemestre_inscription_with_modules") - formsemestre_inscription_with_modules = ( - sco_formsemestre_inscriptions.formsemestre_inscription_with_modules - ) - - security.declareProtected(ScoEtudInscrit, "formsemestre_inscription_option") - formsemestre_inscription_option = ( - sco_formsemestre_inscriptions.formsemestre_inscription_option - ) - - security.declareProtected(ScoEtudInscrit, "do_moduleimpl_incription_options") - do_moduleimpl_incription_options = ( - sco_formsemestre_inscriptions.do_moduleimpl_incription_options - ) - - security.declareProtected(ScoView, "formsemestre_inscrits_ailleurs") - formsemestre_inscrits_ailleurs = ( - sco_formsemestre_inscriptions.formsemestre_inscrits_ailleurs - ) - - security.declareProtected(ScoEtudInscrit, "moduleimpl_inscriptions_edit") - moduleimpl_inscriptions_edit = ( - sco_moduleimpl_inscriptions.moduleimpl_inscriptions_edit - ) - - security.declareProtected(ScoView, "moduleimpl_inscriptions_stats") - moduleimpl_inscriptions_stats = ( - sco_moduleimpl_inscriptions.moduleimpl_inscriptions_stats - ) - - # --- Evaluations - _evaluationEditor = ndb.EditableTable( - "notes_evaluation", - "evaluation_id", - ( - "evaluation_id", - "moduleimpl_id", - "jour", - "heure_debut", - "heure_fin", - "description", - "note_max", - "coefficient", - "visibulletin", - "publish_incomplete", - "evaluation_type", - "numero", - ), - sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord - output_formators={ - "jour": ndb.DateISOtoDMY, - "visibulletin": str, - "publish_incomplete": str, - "numero": ndb.int_null_is_zero, - }, - input_formators={ - "jour": ndb.DateDMYtoISO, - "heure_debut": ndb.TimetoISO8601, # converti par do_evaluation_list - "heure_fin": ndb.TimetoISO8601, # converti par do_evaluation_list - "visibulletin": int, - "publish_incomplete": int, - }, - ) - - def _evaluation_check_write_access(self, REQUEST, moduleimpl_id=None): - """Vérifie que l'on a le droit de modifier, créer ou détruire une - évaluation dans ce module. - Sinon, lance une exception. - (nb: n'implique pas le droit de saisir ou modifier des notes) - """ - # acces pour resp. moduleimpl et resp. form semestre (dir etud) - if moduleimpl_id is None: - raise ValueError("no moduleimpl specified") # bug - authuser = REQUEST.AUTHENTICATED_USER - uid = str(authuser) - M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=moduleimpl_id)[0] - sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"]) - - if ( - (not authuser.has_permission(ScoEditAllEvals, self)) - and uid != M["responsable_id"] - and uid not in sem["responsables"] - ): - if sem["ens_can_edit_eval"]: - for ens in M["ens"]: - if ens["ens_id"] == uid: - return # ok - raise AccessDenied("Modification évaluation impossible pour %s" % (uid,)) - - security.declareProtected(ScoEnsView, "do_evaluation_create") - - def do_evaluation_create( - self, - moduleimpl_id=None, - jour=None, - heure_debut=None, - heure_fin=None, - description=None, - note_max=None, - coefficient=None, - visibulletin=None, - publish_incomplete=None, - evaluation_type=None, - numero=None, - REQUEST=None, - **kw - ): - """Create an evaluation""" - args = locals() - log("do_evaluation_create: args=" + str(args)) - self._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id) - self._check_evaluation_args(args) - # Check numeros - sco_evaluations.module_evaluation_renumber( - self, moduleimpl_id, REQUEST=REQUEST, only_if_unumbered=True - ) - if not "numero" in args or args["numero"] is None: - n = None - # determine le numero avec la date - # Liste des eval existantes triees par date, la plus ancienne en tete - ModEvals = self.do_evaluation_list( - args={"moduleimpl_id": moduleimpl_id}, - sortkey="jour asc, heure_debut asc", - ) - if args["jour"]: - next_eval = None - t = ( - ndb.DateDMYtoISO(args["jour"]), - ndb.TimetoISO8601(args["heure_debut"]), - ) - for e in ModEvals: - if ( - ndb.DateDMYtoISO(e["jour"]), - ndb.TimetoISO8601(e["heure_debut"]), - ) > t: - next_eval = e - break - if next_eval: - n = sco_evaluations.module_evaluation_insert_before( - self, ModEvals, next_eval, REQUEST - ) - else: - n = None # a placer en fin - if n is None: # pas de date ou en fin: - if ModEvals: - log(pprint.pformat(ModEvals[-1])) - n = ModEvals[-1]["numero"] + 1 - else: - n = 0 # the only one - # log("creating with numero n=%d" % n) - args["numero"] = n - - # - cnx = self.GetDBConnexion() - r = self._evaluationEditor.create(cnx, args) - - # news - M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=moduleimpl_id)[0] - mod = self.do_module_list(args={"module_id": M["module_id"]})[0] - mod["moduleimpl_id"] = M["moduleimpl_id"] - mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod - sco_news.add( - self, - REQUEST, - typ=NEWS_NOTE, - object=moduleimpl_id, - text='Création d\'une évaluation dans %(titre)s' - % mod, - url=mod["url"], - ) - - return r - - def _check_evaluation_args(self, args): - "Check coefficient, dates and duration, raises exception if invalid" - moduleimpl_id = args["moduleimpl_id"] - # check bareme - note_max = args.get("note_max", None) - if note_max is None: - raise ScoValueError("missing note_max") - try: - note_max = float(note_max) - except ValueError: - raise ScoValueError("Invalid note_max value") - if note_max < 0: - raise ScoValueError("Invalid note_max value (must be positive or null)") - # check coefficient - coef = args.get("coefficient", None) - if coef is None: - raise ScoValueError("missing coefficient") - try: - coef = float(coef) - except ValueError: - raise ScoValueError("Invalid coefficient value") - if coef < 0: - raise ScoValueError("Invalid coefficient value (must be positive or null)") - # check date - jour = args.get("jour", None) - args["jour"] = jour - if jour: - M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=moduleimpl_id)[0] - sem = sco_formsemestre.get_formsemestre(self, M["formsemestre_id"]) - d, m, y = [int(x) for x in sem["date_debut"].split("/")] - date_debut = datetime.date(y, m, d) - d, m, y = [int(x) for x in sem["date_fin"].split("/")] - date_fin = datetime.date(y, m, d) - # passe par ndb.DateDMYtoISO pour avoir date pivot - y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")] - jour = datetime.date(y, m, d) - if (jour > date_fin) or (jour < date_debut): - raise ScoValueError( - "La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !" - % (d, m, y) - ) - heure_debut = args.get("heure_debut", None) - args["heure_debut"] = heure_debut - heure_fin = args.get("heure_fin", None) - args["heure_fin"] = heure_fin - if jour and ((not heure_debut) or (not heure_fin)): - raise ScoValueError("Les heures doivent être précisées") - d = ndb.TimeDuration(heure_debut, heure_fin) - if d and ((d < 0) or (d > 60 * 12)): - raise ScoValueError("Heures de l'évaluation incohérentes !") - - security.declareProtected(ScoEnsView, "evaluation_delete") - - def evaluation_delete(self, REQUEST, evaluation_id): - """Form delete evaluation""" - El = self.do_evaluation_list(args={"evaluation_id": evaluation_id}) - if not El: - raise ValueError("Evalution inexistante ! (%s)" % evaluation_id) - E = El[0] - M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=E["moduleimpl_id"])[0] - Mod = self.do_module_list(args={"module_id": M["module_id"]})[0] - tit = "Suppression de l'évaluation %(description)s (%(jour)s)" % E - etat = sco_evaluations.do_evaluation_etat(self, evaluation_id) - H = [ - self.html_sem_header(REQUEST, tit, with_h2=False), - """

Module %(code)s %(titre)s

""" - % Mod, - """

%s

""" % tit, - """

Opération irréversible. Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées.

""", - ] - warning = False - if etat["nb_notes_total"]: - warning = True - nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"] - H.append( - """
Il y a %s notes""" - % etat["nb_notes_total"] - ) - if nb_desinscrits: - H.append( - """ (dont %s d'étudiants qui ne sont plus inscrits)""" - % nb_desinscrits - ) - H.append(""" dans l'évaluation""") - if etat["nb_notes"] == 0: - H.append( - """

Vous pouvez quand même supprimer l'évaluation, les notes des étudiants désincrits seront effacées.

""" - ) - - if etat["nb_notes"]: - H.append( - """

Suppression impossible (effacer les notes d'abord)

retour au tableau de bord du module

""" - % E["moduleimpl_id"] - ) - return "\n".join(H) + self.sco_footer(REQUEST) - if warning: - H.append("""""") - - tf = TrivialFormulator( - REQUEST.URL0, - REQUEST.form, - (("evaluation_id", {"input_type": "hidden"}),), - initvalues=E, - submitlabel="Confirmer la suppression", - cancelbutton="Annuler", - ) - if tf[0] == 0: - return "\n".join(H) + tf[1] + self.sco_footer(REQUEST) - elif tf[0] == -1: - return REQUEST.RESPONSE.redirect( - self.ScoURL() - + "/Notes/moduleimpl_status?moduleimpl_id=" - + E["moduleimpl_id"] - ) - else: - sco_evaluations.do_evaluation_delete(self, REQUEST, E["evaluation_id"]) - return ( - "\n".join(H) - + """

OK, évaluation supprimée.

-

Continuer

""" - % ( - self.ScoURL() - + "/Notes/moduleimpl_status?moduleimpl_id=" - + E["moduleimpl_id"] - ) - + self.sco_footer(REQUEST) - ) - - security.declareProtected(ScoView, "do_evaluation_list") - - def do_evaluation_list(self, args, sortkey=None): - """List evaluations, sorted by numero (or most recent date first). - - Ajoute les champs: - 'duree' : '2h30' - 'matin' : 1 (commence avant 12:00) ou 0 - 'apresmidi' : 1 (termine après 12:00) ou 0 - 'descrheure' : ' de 15h00 à 16h30' - """ - cnx = self.GetDBConnexion() - evals = self._evaluationEditor.list(cnx, args, sortkey=sortkey) - # calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi - for e in evals: - heure_debut_dt = e["heure_debut"] or datetime.time( - 8, 00 - ) # au cas ou pas d'heure (note externe?) - heure_fin_dt = e["heure_fin"] or datetime.time(8, 00) - e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"]) - e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"]) - e["jouriso"] = ndb.DateDMYtoISO(e["jour"]) - heure_debut, heure_fin = e["heure_debut"], e["heure_fin"] - d = ndb.TimeDuration(heure_debut, heure_fin) - if d is not None: - m = d % 60 - e["duree"] = "%dh" % (d / 60) - if m != 0: - e["duree"] += "%02d" % m - else: - e["duree"] = "" - if heure_debut and (not heure_fin or heure_fin == heure_debut): - e["descrheure"] = " à " + heure_debut - elif heure_debut and heure_fin: - e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin) - else: - e["descrheure"] = "" - # matin, apresmidi: utile pour se referer aux absences: - if heure_debut_dt < datetime.time(12, 00): - e["matin"] = 1 - else: - e["matin"] = 0 - if heure_fin_dt > datetime.time(12, 00): - e["apresmidi"] = 1 - else: - e["apresmidi"] = 0 - - return evals - - security.declareProtected(ScoView, "do_evaluation_list_in_formsemestre") - - def do_evaluation_list_in_formsemestre(self, formsemestre_id): - "list evaluations in this formsemestre" - mods = sco_moduleimpl.do_moduleimpl_list(self, formsemestre_id=formsemestre_id) - evals = [] - for mod in mods: - evals += self.do_evaluation_list( - args={"moduleimpl_id": mod["moduleimpl_id"]} - ) - return evals - - security.declareProtected(ScoEnsView, "do_evaluation_edit") - - def do_evaluation_edit(self, REQUEST, args): - "edit a evaluation" - evaluation_id = args["evaluation_id"] - the_evals = self.do_evaluation_list({"evaluation_id": evaluation_id}) - if not the_evals: - raise ValueError("evaluation inexistante !") - moduleimpl_id = the_evals[0]["moduleimpl_id"] - args["moduleimpl_id"] = moduleimpl_id - self._check_evaluation_args(args) - self._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id) - cnx = self.GetDBConnexion() - self._evaluationEditor.edit(cnx, args) - # inval cache pour ce semestre - M = sco_moduleimpl.do_moduleimpl_list(self, moduleimpl_id=moduleimpl_id)[0] - self._inval_cache( - formsemestre_id=M["formsemestre_id"] - ) # > evaluation_edit (coef, ...) - - security.declareProtected(ScoEnsView, "evaluation_edit") - - def evaluation_edit(self, evaluation_id, REQUEST): - "form edit evaluation" - return sco_evaluations.evaluation_create_form( - self, evaluation_id=evaluation_id, REQUEST=REQUEST, edit=True - ) - - security.declareProtected(ScoEnsView, "evaluation_create") - - def evaluation_create(self, moduleimpl_id, REQUEST): - "form create evaluation" - return sco_evaluations.evaluation_create_form( - self, moduleimpl_id=moduleimpl_id, REQUEST=REQUEST, edit=False - ) - - security.declareProtected(ScoView, "evaluation_listenotes") - - def evaluation_listenotes(self, REQUEST=None): - """Affichage des notes d'une évaluation""" - if REQUEST.form.get("format", "html") == "html": - H = self.sco_header( - REQUEST, - cssstyles=["css/verticalhisto.css"], - javascripts=["js/etud_info.js"], - init_qtip=True, - ) - F = self.sco_footer(REQUEST) - else: - H, F = "", "" - B = self.do_evaluation_listenotes(REQUEST) - return H + B + F - - security.declareProtected(ScoView, "do_evaluation_listenotes") - do_evaluation_listenotes = sco_liste_notes.do_evaluation_listenotes - - security.declareProtected(ScoView, "evaluation_list_operations") - evaluation_list_operations = sco_undo_notes.evaluation_list_operations - - security.declareProtected(ScoView, "evaluation_check_absences_html") - evaluation_check_absences_html = sco_liste_notes.evaluation_check_absences_html - - security.declareProtected(ScoView, "formsemestre_check_absences_html") - formsemestre_check_absences_html = sco_liste_notes.formsemestre_check_absences_html - - # --- Placement des étudiants pour l'évaluation - security.declareProtected(ScoEnsView, "placement_eval_selectetuds") - placement_eval_selectetuds = sco_placement.placement_eval_selectetuds - - security.declareProtected(ScoEnsView, "do_placement") - do_placement = sco_placement.do_placement - - # --- Saisie des notes - security.declareProtected(ScoEnsView, "saisie_notes_tableur") - saisie_notes_tableur = sco_saisie_notes.saisie_notes_tableur - - security.declareProtected(ScoEnsView, "feuille_saisie_notes") - feuille_saisie_notes = sco_saisie_notes.feuille_saisie_notes - - security.declareProtected(ScoEnsView, "saisie_notes") - saisie_notes = sco_saisie_notes.saisie_notes - - security.declareProtected(ScoEnsView, "save_note") - save_note = sco_saisie_notes.save_note - - security.declareProtected(ScoEnsView, "do_evaluation_set_missing") - do_evaluation_set_missing = sco_saisie_notes.do_evaluation_set_missing - - security.declareProtected(ScoView, "evaluation_suppress_alln") - evaluation_suppress_alln = sco_saisie_notes.evaluation_suppress_alln - - security.declareProtected(ScoEditAllNotes, "dummy_ScoEditAllNotes") - - def dummy_ScoEditAllNotes(self): - "dummy method, necessary to declare permission ScoEditAllNotes" - return True - - security.declareProtected(ScoEditAllEvals, "dummy_ScoEditAllEvals") - - def dummy_ScoEditAllEvals(self): - "dummy method, necessary to declare permission ScoEditAllEvals" - return True - - security.declareProtected(ScoSuperAdmin, "dummy_ScoSuperAdmin") - - def dummy_ScoSuperAdmin(self): - "dummy method, necessary to declare permission ScoSuperAdmin" - return True - - security.declareProtected(ScoEtudChangeGroups, "dummy_ScoEtudChangeGroups") - - def dummy_ScoEtudChangeGroups(self): - "dummy method, necessary to declare permission ScoEtudChangeGroups" - return True - - security.declareProtected(ScoEtudSupprAnnotations, "dummy_ScoEtudSupprAnnotations") - - def dummy_ScoEtudSupprAnnotations(self): - "dummy method, necessary to declare permission ScoEtudSupprAnnotations" - return True - - security.declareProtected(ScoEditFormationTags, "dummy_ScoEditFormationTags") - - def dummy_ScoEditFormationTags(self): - "dummy method, necessary to declare permission ScoEditFormationTags" - return True - - # cache notes evaluations - def get_evaluations_cache(self): - u = self.GetDBConnexionString() - if CACHE_evaluations.has_key(u): - return CACHE_evaluations[u] - else: - log("get_evaluations_cache: new simpleCache") - CACHE_evaluations[u] = sco_cache.simpleCache() - return CACHE_evaluations[u] - - def _notes_getall( - self, evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None - ): - """get tt les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }} - Attention: inclue aussi les notes des étudiants qui ne sont plus inscrits au module. - """ - # log('_notes_getall( e=%s fs=%s )' % (evaluation_id, filter_suppressed)) - do_cache = ( - filter_suppressed and table == "notes_notes" and (by_uid is None) - ) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant - if do_cache: - cache = self.get_evaluations_cache() - r = cache.get(evaluation_id) - if r != None: - return r - cnx = self.GetDBConnexion() - cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) - cond = " where evaluation_id=%(evaluation_id)s" - if by_uid: - cond += " and uid=%(by_uid)s" - - cursor.execute( - "select * from " + table + cond, - {"evaluation_id": evaluation_id, "by_uid": by_uid}, - ) - res = cursor.dictfetchall() - d = {} - if filter_suppressed: - for x in res: - if x["value"] != scu.NOTES_SUPPRESS: - d[x["etudid"]] = x - else: - for x in res: - d[x["etudid"]] = x - if do_cache: - cache.set(evaluation_id, d) - return d - - # --- Bulletins - security.declareProtected(ScoView, "formsemestre_bulletins_pdf") - - def formsemestre_bulletins_pdf( - self, formsemestre_id, REQUEST, version="selectedevals" - ): - "Publie les bulletins dans un classeur PDF" - pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( - self, formsemestre_id, REQUEST, version=version - ) - return scu.sendPDFFile(REQUEST, pdfdoc, filename) - - security.declareProtected(ScoView, "etud_bulletins_pdf") - - def etud_bulletins_pdf(self, etudid, REQUEST, version="selectedevals"): - "Publie tous les bulletins d'un etudiants dans un classeur PDF" - pdfdoc, filename = sco_bulletins_pdf.get_etud_bulletins_pdf( - self, etudid, REQUEST, version=version - ) - return scu.sendPDFFile(REQUEST, pdfdoc, filename) - - security.declareProtected(ScoView, "formsemestre_bulletins_pdf_choice") - formsemestre_bulletins_pdf_choice = sco_bulletins.formsemestre_bulletins_pdf_choice - - security.declareProtected(ScoView, "formsemestre_bulletins_mailetuds_choice") - formsemestre_bulletins_mailetuds_choice = ( - sco_bulletins.formsemestre_bulletins_mailetuds_choice - ) - - security.declareProtected(ScoView, "formsemestre_bulletins_mailetuds") - - def formsemestre_bulletins_mailetuds( - self, - formsemestre_id, - REQUEST, - version="long", - dialog_confirmed=False, - prefer_mail_perso=0, - ): - "envoi a chaque etudiant (inscrit et ayant un mail) son bulletin" - prefer_mail_perso = int(prefer_mail_perso) - nt = self._getNotesCache().get_NotesTable( - self, formsemestre_id - ) # > get_etudids - etudids = nt.get_etudids() - # - if not sco_bulletins.can_send_bulletin_by_mail(self, formsemestre_id, REQUEST): - raise AccessDenied("vous n'avez pas le droit d'envoyer les bulletins") - # Confirmation dialog - if not dialog_confirmed: - return self.confirmDialog( - "

Envoyer les %d bulletins par e-mail aux étudiants ?" - % len(etudids), - dest_url="", - REQUEST=REQUEST, - cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id, - parameters={ - "version": version, - "formsemestre_id": formsemestre_id, - "prefer_mail_perso": prefer_mail_perso, - }, - ) - - # Make each bulletin - nb_send = 0 - for etudid in etudids: - h, _ = sco_bulletins.do_formsemestre_bulletinetud( - self, - formsemestre_id, - etudid, - version=version, - prefer_mail_perso=prefer_mail_perso, - format="pdfmail", - nohtml=True, - REQUEST=REQUEST, - ) - if h: - nb_send += 1 - # - return ( - self.sco_header(REQUEST) - + '

%d bulletins sur %d envoyés par mail !

continuer

' - % (nb_send, len(etudids), formsemestre_id) - + self.sco_footer(REQUEST) - ) - - security.declareProtected(ScoView, "external_ue_create_form") - external_ue_create_form = sco_ue_external.external_ue_create_form - - security.declareProtected(ScoEnsView, "appreciation_add_form") - - def appreciation_add_form( - self, - etudid=None, - formsemestre_id=None, - id=None, # si id, edit - suppress=False, # si true, supress id - REQUEST=None, - ): - "form ajout ou edition d'une appreciation" - cnx = self.GetDBConnexion() - authuser = REQUEST.AUTHENTICATED_USER - if id: # edit mode - apps = scolars.appreciations_list(cnx, args={"id": id}) - if not apps: - raise ScoValueError("id d'appreciation invalide !") - app = apps[0] - formsemestre_id = app["formsemestre_id"] - etudid = app["etudid"] - if REQUEST.form.has_key("edit"): - edit = int(REQUEST.form["edit"]) - elif id: - edit = 1 - else: - edit = 0 - sem = sco_formsemestre.get_formsemestre(self, formsemestre_id) - # check custom access permission - can_edit_app = (str(authuser) in sem["responsables"]) or ( - authuser.has_permission(ScoEtudInscrit, self) - ) - if not can_edit_app: - raise AccessDenied("vous n'avez pas le droit d'ajouter une appreciation") - # - bull_url = "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" % ( - formsemestre_id, - etudid, - ) - if suppress: - scolars.appreciations_delete(cnx, id) - logdb(REQUEST, cnx, method="appreciation_suppress", etudid=etudid, msg="") - return REQUEST.RESPONSE.redirect(bull_url) - # - etud = self.getEtudInfo(etudid=etudid, filled=1)[0] - if id: - a = "Edition" - else: - a = "Ajout" - H = [ - self.sco_header(REQUEST) - + "

%s d'une appréciation sur %s

" % (a, etud["nomprenom"]) - ] - F = self.sco_footer(REQUEST) - descr = [ - ("edit", {"input_type": "hidden", "default": edit}), - ("etudid", {"input_type": "hidden"}), - ("formsemestre_id", {"input_type": "hidden"}), - ("id", {"input_type": "hidden"}), - ("comment", {"title": "", "input_type": "textarea", "rows": 4, "cols": 60}), - ] - if id: - initvalues = { - "etudid": etudid, - "formsemestre_id": formsemestre_id, - "comment": app["comment"], - } - else: - initvalues = {} - tf = TrivialFormulator( - REQUEST.URL0, - REQUEST.form, - descr, - initvalues=initvalues, - cancelbutton="Annuler", - submitlabel="Ajouter appréciation", - ) - if tf[0] == 0: - return "\n".join(H) + "\n" + tf[1] + F - elif tf[0] == -1: - return REQUEST.RESPONSE.redirect(bull_url) - else: - args = { - "etudid": etudid, - "formsemestre_id": formsemestre_id, - "author": str(authuser), - "comment": tf[2]["comment"], - "zope_authenticated_user": str(authuser), - "zope_remote_addr": REQUEST.REMOTE_ADDR, - } - if edit: - args["id"] = id - scolars.appreciations_edit(cnx, args) - else: # nouvelle - scolars.appreciations_create(cnx, args, has_uniq_values=False) - # log - logdb( - REQUEST, - cnx, - method="appreciation_add", - etudid=etudid, - msg=tf[2]["comment"], - ) - # ennuyeux mais necessaire (pour le PDF seulement) - self._inval_cache( - pdfonly=True, formsemestre_id=formsemestre_id - ) # > appreciation_add - return REQUEST.RESPONSE.redirect(bull_url) - - def _can_edit_pv(self, REQUEST, formsemestre_id): - "Vrai si utilisateur peut editer un PV de jury de ce semestre" - - sem = sco_formsemestre.get_formsemestre(self, formsemestre_id) - if self._is_chef_or_diretud(REQUEST, sem): - return True - # Autorise les secrétariats, repérés via la permission ScoEtudChangeAdr - # (ceci nous évite d'ajouter une permission Zope aux installations existantes) - authuser = REQUEST.AUTHENTICATED_USER - return authuser.has_permission(ScoEtudChangeAdr, self) - - # --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES - def _can_validate_sem(self, REQUEST, formsemestre_id): - "Vrai si utilisateur peut saisir decision de jury dans ce semestre" - sem = sco_formsemestre.get_formsemestre(self, formsemestre_id) - if sem["etat"] != "1": - return False # semestre verrouillé - - return self._is_chef_or_diretud(REQUEST, sem) - - def _is_chef_or_diretud(self, REQUEST, sem): - "Vrai si utilisateur est admin, chef dept ou responsable du semestre" - authuser = REQUEST.AUTHENTICATED_USER - if authuser.has_permission(ScoImplement, self): - return True # admin, chef dept - uid = str(authuser) - if uid in sem["responsables"]: - return True - - return False - - security.declareProtected(ScoView, "formsemestre_validation_etud_form") - - def formsemestre_validation_etud_form( - self, - formsemestre_id, - etudid=None, - etud_index=None, - check=0, - desturl="", - sortcol=None, - REQUEST=None, - ): - "Formulaire choix jury pour un étudiant" - readonly = not self._can_validate_sem(REQUEST, formsemestre_id) - return sco_formsemestre_validation.formsemestre_validation_etud_form( - self, - formsemestre_id, - etudid=etudid, - etud_index=etud_index, - check=check, - readonly=readonly, - desturl=desturl, - sortcol=sortcol, - REQUEST=REQUEST, - ) - - security.declareProtected(ScoView, "formsemestre_validation_etud") - - def formsemestre_validation_etud( - self, - formsemestre_id, - etudid=None, - codechoice=None, - desturl="", - sortcol=None, - REQUEST=None, - ): - "Enregistre choix jury pour un étudiant" - if not self._can_validate_sem(REQUEST, formsemestre_id): - return self.confirmDialog( - message="

Opération non autorisée pour %s" - % REQUEST.AUTHENTICATED_USER, - dest_url=self.ScoURL(), - REQUEST=REQUEST, - ) - - return sco_formsemestre_validation.formsemestre_validation_etud( - self, - formsemestre_id, - etudid=etudid, - codechoice=codechoice, - desturl=desturl, - sortcol=sortcol, - REQUEST=REQUEST, - ) - - security.declareProtected(ScoView, "formsemestre_validation_etud_manu") - - def formsemestre_validation_etud_manu( - self, - formsemestre_id, - etudid=None, - code_etat="", - new_code_prev="", - devenir="", - assidu=False, - desturl="", - sortcol=None, - REQUEST=None, - ): - "Enregistre choix jury pour un étudiant" - if not self._can_validate_sem(REQUEST, formsemestre_id): - return self.confirmDialog( - message="

Opération non autorisée pour %s" - % REQUEST.AUTHENTICATED_USER, - dest_url=self.ScoURL(), - REQUEST=REQUEST, - ) - - return sco_formsemestre_validation.formsemestre_validation_etud_manu( - self, - formsemestre_id, - etudid=etudid, - code_etat=code_etat, - new_code_prev=new_code_prev, - devenir=devenir, - assidu=assidu, - desturl=desturl, - sortcol=sortcol, - REQUEST=REQUEST, - ) - - security.declareProtected(ScoView, "formsemestre_validate_previous_ue") - - def formsemestre_validate_previous_ue( - self, formsemestre_id, etudid=None, REQUEST=None - ): - "Form. saisie UE validée hors ScoDoc " - if not self._can_validate_sem(REQUEST, formsemestre_id): - return self.confirmDialog( - message="

Opération non autorisée pour %s" - % REQUEST.AUTHENTICATED_USER, - dest_url=self.ScoURL(), - REQUEST=REQUEST, - ) - return sco_formsemestre_validation.formsemestre_validate_previous_ue( - self, formsemestre_id, etudid, REQUEST=REQUEST - ) - - security.declareProtected(ScoView, "formsemestre_ext_create_form") - formsemestre_ext_create_form = ( - sco_formsemestre_exterieurs.formsemestre_ext_create_form - ) - - security.declareProtected(ScoView, "formsemestre_ext_edit_ue_validations") - - def formsemestre_ext_edit_ue_validations( - self, formsemestre_id, etudid=None, REQUEST=None - ): - "Form. edition UE semestre extérieur" - if not self._can_validate_sem(REQUEST, formsemestre_id): - return self.confirmDialog( - message="

Opération non autorisée pour %s" - % REQUEST.AUTHENTICATED_USER, - dest_url=self.ScoURL(), - REQUEST=REQUEST, - ) - return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations( - self, formsemestre_id, etudid, REQUEST=REQUEST - ) - - security.declareProtected(ScoView, "get_etud_ue_cap_html") - get_etud_ue_cap_html = sco_formsemestre_validation.get_etud_ue_cap_html - - security.declareProtected(ScoView, "etud_ue_suppress_validation") - - def etud_ue_suppress_validation(self, etudid, formsemestre_id, ue_id, REQUEST=None): - """Suppress a validation (ue_id, etudid) and redirect to formsemestre""" - if not self._can_validate_sem(REQUEST, formsemestre_id): - return self.confirmDialog( - message="

Opération non autorisée pour %s" - % REQUEST.AUTHENTICATED_USER, - dest_url=self.ScoURL(), - REQUEST=REQUEST, - ) - return sco_formsemestre_validation.etud_ue_suppress_validation( - self, etudid, formsemestre_id, ue_id, REQUEST=REQUEST - ) - - security.declareProtected(ScoView, "formsemestre_validation_auto") - - def formsemestre_validation_auto(self, formsemestre_id, REQUEST): - "Formulaire saisie automatisee des decisions d'un semestre" - if not self._can_validate_sem(REQUEST, formsemestre_id): - return self.confirmDialog( - message="

Opération non autorisée pour %s" - % REQUEST.AUTHENTICATED_USER, - dest_url=self.ScoURL(), - REQUEST=REQUEST, - ) - - return sco_formsemestre_validation.formsemestre_validation_auto( - self, formsemestre_id, REQUEST - ) - - security.declareProtected(ScoView, "do_formsemestre_validation_auto") - - def do_formsemestre_validation_auto(self, formsemestre_id, REQUEST): - "Formulaire saisie automatisee des decisions d'un semestre" - if not self._can_validate_sem(REQUEST, formsemestre_id): - return self.confirmDialog( - message="

Opération non autorisée pour %s" - % REQUEST.AUTHENTICATED_USER, - dest_url=self.ScoURL(), - REQUEST=REQUEST, - ) - - return sco_formsemestre_validation.do_formsemestre_validation_auto( - self, formsemestre_id, REQUEST - ) - - security.declareProtected(ScoView, "formsemestre_fix_validation_ues") - - def formsemestre_fix_validation_ues(self, formsemestre_id, REQUEST=None): - "Verif/reparation codes UE" - if not self._can_validate_sem(REQUEST, formsemestre_id): - return self.confirmDialog( - message="

Opération non autorisée pour %s" - % REQUEST.AUTHENTICATED_USER, - dest_url=self.ScoURL(), - REQUEST=REQUEST, - ) - - return sco_formsemestre_validation.formsemestre_fix_validation_ues( - self, formsemestre_id, REQUEST - ) - - security.declareProtected(ScoView, "formsemestre_validation_suppress_etud") - - def formsemestre_validation_suppress_etud( - self, formsemestre_id, etudid, REQUEST=None, dialog_confirmed=False - ): - """Suppression des decisions de jury pour un etudiant.""" - if not self._can_validate_sem(REQUEST, formsemestre_id): - return self.confirmDialog( - message="

Opération non autorisée pour %s" - % REQUEST.AUTHENTICATED_USER, - dest_url=self.ScoURL(), - REQUEST=REQUEST, - ) - if not dialog_confirmed: - sem = sco_formsemestre.get_formsemestre(self, formsemestre_id) - etud = self.getEtudInfo(etudid=etudid, filled=1)[0] - nt = self._getNotesCache().get_NotesTable( - self, formsemestre_id - ) # > get_etud_decision_sem - decision_jury = nt.get_etud_decision_sem(etudid) - if decision_jury: - existing = ( - "

Décision existante: %(code)s du %(event_date)s

" - % decision_jury - ) - else: - existing = "" - return self.confirmDialog( - """

Confirmer la suppression des décisions du semestre %s (%s - %s) pour %s ?

%s -

Cette opération est irréversible. -

- """ - % ( - sem["titre_num"], - sem["date_debut"], - sem["date_fin"], - etud["nomprenom"], - existing, - ), - OK="Supprimer", - dest_url="", - REQUEST=REQUEST, - cancel_url="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s" - % (formsemestre_id, etudid), - parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, - ) - - sco_formsemestre_validation.formsemestre_validation_suppress_etud( - self, formsemestre_id, etudid - ) - return REQUEST.RESPONSE.redirect( - self.ScoURL() - + "/Notes/formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&head_message=Décision%%20supprimée" - % (formsemestre_id, etudid) - ) - - # ------------- PV de JURY et archives - security.declareProtected(ScoView, "formsemestre_pvjury") - formsemestre_pvjury = sco_pvjury.formsemestre_pvjury - - security.declareProtected(ScoView, "formsemestre_lettres_individuelles") - formsemestre_lettres_individuelles = sco_pvjury.formsemestre_lettres_individuelles - security.declareProtected(ScoView, "formsemestre_pvjury_pdf") - formsemestre_pvjury_pdf = sco_pvjury.formsemestre_pvjury_pdf - - security.declareProtected(ScoView, "feuille_preparation_jury") - feuille_preparation_jury = sco_prepajury.feuille_preparation_jury - - security.declareProtected(ScoView, "formsemestre_archive") - formsemestre_archive = sco_archives.formsemestre_archive - - security.declareProtected(ScoView, "formsemestre_delete_archive") - formsemestre_delete_archive = sco_archives.formsemestre_delete_archive - - security.declareProtected(ScoView, "formsemestre_list_archives") - formsemestre_list_archives = sco_archives.formsemestre_list_archives - - security.declareProtected(ScoView, "formsemestre_get_archived_file") - formsemestre_get_archived_file = sco_archives.formsemestre_get_archived_file - - security.declareProtected(ScoEditApo, "view_apo_csv") - view_apo_csv = sco_etape_apogee_view.view_apo_csv - - security.declareProtected(ScoEditApo, "view_apo_csv_store") - view_apo_csv_store = sco_etape_apogee_view.view_apo_csv_store - - security.declareProtected(ScoEditApo, "view_apo_csv_download_and_store") - view_apo_csv_download_and_store = ( - sco_etape_apogee_view.view_apo_csv_download_and_store - ) - - security.declareProtected(ScoEditApo, "view_apo_csv_delete") - view_apo_csv_delete = sco_etape_apogee_view.view_apo_csv_delete - - security.declareProtected(ScoEditApo, "view_scodoc_etuds") - view_scodoc_etuds = sco_etape_apogee_view.view_scodoc_etuds - - security.declareProtected(ScoEditApo, "view_apo_etuds") - view_apo_etuds = sco_etape_apogee_view.view_apo_etuds - - security.declareProtected(ScoEditApo, "apo_semset_maq_status") - apo_semset_maq_status = sco_etape_apogee_view.apo_semset_maq_status - - security.declareProtected(ScoEditApo, "apo_csv_export_results") - apo_csv_export_results = sco_etape_apogee_view.apo_csv_export_results - - # sco_semset - security.declareProtected(ScoEditApo, "semset_page") - semset_page = sco_semset.semset_page - - security.declareProtected(ScoEditApo, "do_semset_create") - do_semset_create = sco_semset.do_semset_create - - security.declareProtected(ScoEditApo, "do_semset_delete") - do_semset_delete = sco_semset.do_semset_delete - - security.declareProtected(ScoEditApo, "edit_semset_set_title") - edit_semset_set_title = sco_semset.edit_semset_set_title - - security.declareProtected(ScoEditApo, "do_semset_add_sem") - do_semset_add_sem = sco_semset.do_semset_add_sem - - security.declareProtected(ScoEditApo, "do_semset_remove_sem") - do_semset_remove_sem = sco_semset.do_semset_remove_sem - - # sco_export_result - security.declareProtected(ScoEditApo, "scodoc_table_results") - scodoc_table_results = sco_export_results.scodoc_table_results - - security.declareProtected(ScoView, "apo_compare_csv_form") - apo_compare_csv_form = sco_apogee_compare.apo_compare_csv_form - - security.declareProtected(ScoView, "apo_compare_csv") - apo_compare_csv = sco_apogee_compare.apo_compare_csv - - # ------------- INSCRIPTIONS: PASSAGE D'UN SEMESTRE A UN AUTRE - security.declareProtected(ScoEtudInscrit, "formsemestre_inscr_passage") - formsemestre_inscr_passage = sco_inscr_passage.formsemestre_inscr_passage - - security.declareProtected(ScoView, "formsemestre_synchro_etuds") - formsemestre_synchro_etuds = sco_synchro_etuds.formsemestre_synchro_etuds - - # ------------- RAPPORTS STATISTIQUES - security.declareProtected(ScoView, "formsemestre_report_counts") - formsemestre_report_counts = sco_report.formsemestre_report_counts - - security.declareProtected(ScoView, "formsemestre_suivi_cohorte") - formsemestre_suivi_cohorte = sco_report.formsemestre_suivi_cohorte - - security.declareProtected(ScoView, "formsemestre_suivi_parcours") - formsemestre_suivi_parcours = sco_report.formsemestre_suivi_parcours - - security.declareProtected(ScoView, "formsemestre_etuds_lycees") - formsemestre_etuds_lycees = sco_lycee.formsemestre_etuds_lycees - - security.declareProtected(ScoView, "scodoc_table_etuds_lycees") - scodoc_table_etuds_lycees = sco_lycee.scodoc_table_etuds_lycees - - security.declareProtected(ScoView, "formsemestre_graph_parcours") - formsemestre_graph_parcours = sco_report.formsemestre_graph_parcours - - security.declareProtected(ScoView, "formsemestre_poursuite_report") - formsemestre_poursuite_report = sco_poursuite_dut.formsemestre_poursuite_report - - security.declareProtected(ScoView, "pe_view_sem_recap") - pe_view_sem_recap = pe_view.pe_view_sem_recap - - security.declareProtected(ScoView, "report_debouche_date") - report_debouche_date = sco_debouche.report_debouche_date - - security.declareProtected(ScoView, "formsemestre_estim_cost") - formsemestre_estim_cost = sco_cost_formation.formsemestre_estim_cost - - # -------------------------------------------------------------------- - # DEBUG - security.declareProtected(ScoView, "check_sem_integrity") - - def check_sem_integrity(self, formsemestre_id, REQUEST): - """Debug. - Check that ue and module formations are consistents - """ - sem = sco_formsemestre.get_formsemestre(self, formsemestre_id) - - modimpls = sco_moduleimpl.do_moduleimpl_list( - self, formsemestre_id=formsemestre_id - ) - bad_ue = [] - bad_sem = [] - for modimpl in modimpls: - mod = self.do_module_list({"module_id": modimpl["module_id"]})[0] - ue = self.do_ue_list({"ue_id": mod["ue_id"]})[0] - if ue["formation_id"] != mod["formation_id"]: - modimpl["mod"] = mod - modimpl["ue"] = ue - bad_ue.append(modimpl) - if sem["formation_id"] != mod["formation_id"]: - bad_sem.append(modimpl) - modimpl["mod"] = mod - - return ( - self.sco_header(REQUEST=REQUEST) - + "

formation_id=%s" % sem["formation_id"] - + "

Inconsistent UE/MOD:

" - + "
".join([str(x) for x in bad_ue]) - + "

Inconsistent SEM/MOD:

" - + "
".join([str(x) for x in bad_sem]) - + self.sco_footer(REQUEST) - ) - - security.declareProtected(ScoView, "check_form_integrity") - - def check_form_integrity(self, formation_id, fix=False, REQUEST=None): - "debug" - log("check_form_integrity: formation_id=%s fix=%s" % (formation_id, fix)) - ues = self.do_ue_list(args={"formation_id": formation_id}) - bad = [] - for ue in ues: - mats = self.do_matiere_list(args={"ue_id": ue["ue_id"]}) - for mat in mats: - mods = self.do_module_list({"matiere_id": mat["matiere_id"]}) - for mod in mods: - if mod["ue_id"] != ue["ue_id"]: - if fix: - # fix mod.ue_id - log( - "fix: mod.ue_id = %s (was %s)" - % (ue["ue_id"], mod["ue_id"]) - ) - mod["ue_id"] = ue["ue_id"] - self.do_module_edit(mod) - bad.append(mod) - if mod["formation_id"] != formation_id: - bad.append(mod) - if bad: - txth = "
".join([str(x) for x in bad]) - txt = "\n".join([str(x) for x in bad]) - log( - "check_form_integrity: formation_id=%s\ninconsistencies:" % formation_id - ) - log(txt) - # Notify by e-mail - sendAlarm(self, "Notes: formation incoherente !", txt) - else: - txth = "OK" - log("ok") - return self.sco_header(REQUEST=REQUEST) + txth + self.sco_footer(REQUEST) - - security.declareProtected(ScoView, "check_formsemestre_integrity") - - def check_formsemestre_integrity(self, formsemestre_id, REQUEST=None): - "debug" - log("check_formsemestre_integrity: formsemestre_id=%s" % (formsemestre_id)) - # verifie que tous les moduleimpl d'un formsemestre - # se réfèrent à un module dont l'UE appartient a la même formation - # Ancien bug: les ue_id étaient mal copiés lors des création de versions - # de formations - diag = [] - - Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( - self, formsemestre_id=formsemestre_id - ) - for mod in Mlist: - if mod["module"]["ue_id"] != mod["matiere"]["ue_id"]: - diag.append( - "moduleimpl %s: module.ue_id=%s != matiere.ue_id=%s" - % ( - mod["moduleimpl_id"], - mod["module"]["ue_id"], - mod["matiere"]["ue_id"], - ) - ) - if mod["ue"]["formation_id"] != mod["module"]["formation_id"]: - diag.append( - "moduleimpl %s: ue.formation_id=%s != mod.formation_id=%s" - % ( - mod["moduleimpl_id"], - mod["ue"]["formation_id"], - mod["module"]["formation_id"], - ) - ) - if diag: - sendAlarm( - self, - "Notes: formation incoherente dans semestre %s !" % formsemestre_id, - "\n".join(diag), - ) - log("check_formsemestre_integrity: formsemestre_id=%s" % formsemestre_id) - log("inconsistencies:\n" + "\n".join(diag)) - else: - diag = ["OK"] - log("ok") - return ( - self.sco_header(REQUEST=REQUEST) - + "
".join(diag) - + self.sco_footer(REQUEST) - ) - - security.declareProtected(ScoView, "check_integrity_all") - - def check_integrity_all(self, REQUEST=None): - "debug: verifie tous les semestres et tt les formations" - # formations - for F in self.formation_list(): - self.check_form_integrity(F["formation_id"], REQUEST=REQUEST) - # semestres - for sem in sco_formsemestre.do_formsemestre_list(self): - self.check_formsemestre_integrity(sem["formsemestre_id"], REQUEST=REQUEST) - return ( - self.sco_header(REQUEST=REQUEST) - + "

empty page: see logs and mails

" - + self.sco_footer(REQUEST) - ) - - # -------------------------------------------------------------------- - # Support for legacy ScoDoc 7 API - # -------------------------------------------------------------------- - security.declareProtected(ScoView, "do_moduleimpl_list") - do_moduleimpl_list = sco_moduleimpl.do_moduleimpl_list - - security.declareProtected(ScoView, "do_moduleimpl_withmodule_list") - do_moduleimpl_withmodule_list = sco_moduleimpl.do_moduleimpl_withmodule_list diff --git a/app/scodoc/ZScolar.py b/app/scodoc/ZScolar.py index e62de73c..32d81f8a 100644 --- a/app/scodoc/ZScolar.py +++ b/app/scodoc/ZScolar.py @@ -416,11 +416,6 @@ REQUEST.URL0=%s
# GESTION DE LA BD # # -------------------------------------------------------------------- - security.declareProtected(ScoSuperAdmin, "GetDBConnexionString") - - def GetDBConnexionString(self): - # should not be published (but used from contained classes via acquisition) - return self._db_cnx_string security.declareProtected(ScoSuperAdmin, "GetDBConnexion") GetDBConnexion = ndb.GetDBConnexion @@ -780,7 +775,9 @@ REQUEST.URL0=%s
# -------------------------- INFOS SUR ETUDIANTS -------------------------- security.declareProtected(ScoView, "getEtudInfo") - def getEtudInfo(self, etudid=False, code_nip=False, filled=False, REQUEST=None, format=None): + def getEtudInfo( + self, etudid=False, code_nip=False, filled=False, REQUEST=None, format=None + ): """infos sur un etudiant pour utilisation en Zope DTML On peut specifier etudid ou bien cherche dans REQUEST.form: etudid, code_nip, code_ine diff --git a/app/scodoc/sco_exceptions.py b/app/scodoc/sco_exceptions.py index 445b6fee..95044dcb 100644 --- a/app/scodoc/sco_exceptions.py +++ b/app/scodoc/sco_exceptions.py @@ -66,6 +66,12 @@ class FormatError(ScoValueError): pass +class ScoConfigurationError(ScoValueError): + """Configuration invalid""" + + pass + + class ScoLockedFormError(ScoException): def __init__(self, msg="", REQUEST=None): msg = ( diff --git a/app/scodoc/sco_permissions.py b/app/scodoc/sco_permissions.py index 9d8cf893..4f43bb71 100644 --- a/app/scodoc/sco_permissions.py +++ b/app/scodoc/sco_permissions.py @@ -49,6 +49,8 @@ class Permission: def init_permissions(): for (perm, symbol, description) in _SCO_PERMISSIONS: setattr(Permission, symbol, perm) + # Crée aussi les attributs dans le module (ScoDoc7 compat) + globals()[symbol] = perm Permission.description[symbol] = description Permission.NBITS = len(_SCO_PERMISSIONS) diff --git a/app/templates/main/index.html b/app/templates/main/index.html index efee516e..61ec1a72 100644 --- a/app/templates/main/index.html +++ b/app/templates/main/index.html @@ -2,7 +2,7 @@ {% import 'bootstrap/wtf.html' as wtf %} {% block app_content %} -

Essais Flask pour ScoDoc 8: accueil

+

Protoype ScoDoc 8: accueil

Avec login requis

    diff --git a/app/views/__init__.py b/app/views/__init__.py index a4209247..9a6eed3a 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -3,6 +3,9 @@ """ from flask import Blueprint +scolar_bp = Blueprint("scolar", __name__) notes_bp = Blueprint("notes", __name__) +absences_bp = Blueprint("absences", __name__) +essais_bp = Blueprint("essais", __name__) -from app.views import notes +from app.views import notes, scolar, absences \ No newline at end of file diff --git a/app/views/absences.py b/app/views/absences.py new file mode 100644 index 00000000..2353d645 --- /dev/null +++ b/app/views/absences.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +Module absences: issu de ScoDoc7 / ZAbsences.py + +Emmanuel Viennet, 2021 +""" + +from flask import g +from flask import current_app + +from app.decorators import ( + scodoc7func, + ScoDoc7Context, + permission_required, + admin_required, + login_required, +) +from app.auth.models import Permission + +from app.views import absences_bp as bp + +context = ScoDoc7Context(globals()) + + +@bp.route("/") +@scodoc7func(context) +def index_html(): + """Un exemple de fonction ScoDoc 7 dans ZAbsences""" + return """ +

    ScoDoc 8 ZAbsences !

    +

    ZAbsences ScoDoc 8

    +

    g.scodoc_dept=%(scodoc_dept)s

    + + + """ % { + "scodoc_dept": g.scodoc_dept, + } diff --git a/app/views/essais.py b/app/views/essais.py new file mode 100644 index 00000000..2bb124e3 --- /dev/null +++ b/app/views/essais.py @@ -0,0 +1,65 @@ +# -*- coding: UTF-8 -* +""" +Module Essais: divers essais pour la migration vers Flask + +Emmanuel Viennet, 2021 +""" + +from flask import g +from flask import current_app + +from app.decorators import ( + scodoc7func, + ScoDoc7Context, + permission_required, + admin_required, + login_required, +) +from app.auth.models import Permission + +from app.views import notes_bp as bp + +# import sco_core deviendra: +from app.ScoDoc import sco_core + +context = ScoDoc7Context(globals()) + + +@bp.route("//Scolarite/sco_exemple") +@scodoc7func(context) +def sco_exemple(etudid="NON"): + """Un exemple de fonction ScoDoc 7""" + return """ +

    ScoDoc 7 rules !

    +

    etudid=%(etudid)s

    +

    g.scodoc_dept=%(scodoc_dept)s

    + + + """ % { + "etudid": etudid, + "scodoc_dept": g.scodoc_dept, + } + + +# En ScoDoc 7, on a souvent des vues qui en appellent d'autres +# avec context.sco_exemple( etudid="E12" ) +@bp.route("//Scolarite/sco_exemple2") +@login_required +@scodoc7func(context) +def sco_exemple2(): + return "Exemple 2" + context.sco_exemple(etudid="deux") + + +# Test avec un seul argument REQUEST positionnel +@bp.route("//Scolarite/sco_get_version") +@scodoc7func(context) +def sco_get_version(REQUEST): + return sco_core.sco_get_version(REQUEST) + + +# Fonction ressemblant à une méthode Zope protégée +@bp.route("//Scolarite/sco_test_view") +@scodoc7func(context) +@permission_required(Permission.ScoView) +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 f2aa981d..f0823924 100644 --- a/app/views/notes.py +++ b/app/views/notes.py @@ -1,9 +1,45 @@ -# -*- coding: UTF-8 -* -"""Module scodoc: un exemple de fonctions +# -*- 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 +# +############################################################################## + """ +Module notes: issu de ScoDoc7 / ZNotes.py + +Emmanuel Viennet, 2021 +""" +import time +import datetime +import jaxml +import pprint + from flask import g from flask import current_app +from config import Config +from scodoc_manager import sco_mgr from app.decorators import ( scodoc7func, ScoDoc7Context, @@ -15,47 +51,3481 @@ from app.auth.models import Permission from app.views import notes_bp as bp -# import sco_core deviendra: -from app.ScoDoc import sco_core +# --------------- + +from app.ScoDoc import sco_utils as scu +from app.ScoDoc import notesdb as ndb +from app.ScoDoc.notes_log import log, sendAlarm + +from app.ScoDoc import scolog +from app.ScoDoc.scolog import logdb + +from app.ScoDoc.sco_exceptions import ( + ScoValueError, + ScoLockedFormError, + ScoGenError, + AccessDenied, +) +from app.ScoDoc.sco_permissions import Permission, ScoImplement +from app.ScoDoc.TrivialFormulator import TrivialFormulator +import app.ScoDoc.htmlutils as htmlutils +import app.ScoDoc.sco_excel as sco_excel + +from app.ScoDoc.gen_tables import GenTable +from app.ScoDoc import sco_cache as sco_cache +from app.ScoDoc import scolars as scolars +from app.ScoDoc import sco_news as sco_news +from app.ScoDoc.sco_news import NEWS_INSCR, NEWS_NOTE, NEWS_FORM, NEWS_SEM, NEWS_MISC + +from app.ScoDoc import sco_formsemestre as sco_formsemestre +from app.ScoDoc import sco_formsemestre_edit as sco_formsemestre_edit +from app.ScoDoc import sco_formsemestre_status as sco_formsemestre_status +from app.ScoDoc import sco_formsemestre_inscriptions as sco_formsemestre_inscriptions +from app.ScoDoc import sco_formsemestre_custommenu as sco_formsemestre_custommenu +from app.ScoDoc import sco_moduleimpl as sco_moduleimpl +from app.ScoDoc import sco_moduleimpl_status as sco_moduleimpl_status +from app.ScoDoc import sco_moduleimpl_inscriptions as sco_moduleimpl_inscriptions +from app.ScoDoc import sco_evaluations as sco_evaluations +from app.ScoDoc import sco_groups as sco_groups +from app.ScoDoc import sco_edit_ue as sco_edit_ue +from app.ScoDoc import sco_edit_formation as sco_edit_formation +from app.ScoDoc import sco_edit_matiere as sco_edit_matiere +from app.ScoDoc import sco_edit_module as sco_edit_module +from app.ScoDoc import sco_tag_module as sco_tag_module +from app.ScoDoc import sco_bulletins as sco_bulletins +from app.ScoDoc import sco_bulletins_pdf as sco_bulletins_pdf +from app.ScoDoc import sco_compute_moy as sco_compute_moy +from app.ScoDoc import sco_recapcomplet as sco_recapcomplet +from app.ScoDoc import sco_liste_notes as sco_liste_notes +from app.ScoDoc import sco_saisie_notes as sco_saisie_notes +from app.ScoDoc import sco_placement as sco_placement +from app.ScoDoc import sco_undo_notes as sco_undo_notes +from app.ScoDoc import sco_formations as sco_formations +from app.ScoDoc import sco_report as sco_report +from app.ScoDoc import sco_lycee as sco_lycee +from app.ScoDoc import sco_poursuite_dut as sco_poursuite_dut +from app.ScoDoc import pe_view as pe_view +from app.ScoDoc import sco_debouche as sco_debouche +from app.ScoDoc import sco_ue_external as sco_ue_external +from app.ScoDoc import sco_cost_formation as sco_cost_formation +from app.ScoDoc import sco_formsemestre_validation as sco_formsemestre_validation +from app.ScoDoc import sco_parcours_dut as sco_parcours_dut +from app.ScoDoc import sco_codes_parcours as sco_codes_parcours +from app.ScoDoc import sco_pvjury as sco_pvjury +from app.ScoDoc import sco_pvpdf as sco_pvpdf +from app.ScoDoc import sco_prepajury as sco_prepajury +from app.ScoDoc import sco_inscr_passage as sco_inscr_passage +from app.ScoDoc import sco_synchro_etuds as sco_synchro_etuds +from app.ScoDoc import sco_archives as sco_archives +from app.ScoDoc import sco_apogee_csv as sco_apogee_csv +from app.ScoDoc import sco_etape_apogee_view as sco_etape_apogee_view +from app.ScoDoc import sco_apogee_compare as sco_apogee_compare +from app.ScoDoc import sco_semset as sco_semset +from app.ScoDoc import sco_export_results as sco_export_results +from app.ScoDoc import sco_formsemestre_exterieurs as sco_formsemestre_exterieurs + +from app.ScoDoc.sco_pdf import PDFLOCK +from app.ScoDoc import notes_table as notes_table +from app.ScoDoc.notes_table import NOTES_CACHE_INST, CacheNotesTable +import app.ScoDoc.VERSION as VERSION context = ScoDoc7Context(globals()) -@bp.route("//Scolarite/sco_exemple") -@scodoc7func -def sco_exemple(etudid="NON"): - """Un exemple de fonction ScoDoc 7""" - return """ -

    ScoDoc 7 rules !

    -

    etudid=%(etudid)s

    -

    g.scodoc_dept=%(scodoc_dept)s

    - - - """ % { - "etudid": etudid, - "scodoc_dept": g.scodoc_dept, - } +def sco_publish(route, function, permission): + """Declare a route for a python function, + protected by permission and called following ScoDoc 7 Zope standards. + """ + return bp.route(route)( + permission_required(permission)(scodoc7func(context)(function)) + ) -# En ScoDoc 7, on a souvent des vues qui en appellent d'autres -# avec context.sco_exemple( etudid="E12" ) -@bp.route("//Scolarite/sco_exemple2") -@login_required -@scodoc7func -def sco_exemple2(): - return "Exemple 2" + context.sco_exemple(etudid="deux") - - -# Test avec un seul argument REQUEST positionnel -@bp.route("//Scolarite/sco_get_version") -@scodoc7func -def sco_get_version(REQUEST): - return sco_core.sco_get_version(REQUEST) - - -# Fonction ressemblant à une méthode Zope protégée -@bp.route("//Scolarite/sco_test_view") -@scodoc7func +# --------------------- Quelques essais élémentaires: +@bp.route("/essai") @permission_required(Permission.ScoView) -def sco_test_view(REQUEST=None): - return """Vous avez vu sco_test_view !""" +@scodoc7func(context) +def essai(context, REQUEST=None): + return essai_(context, REQUEST) + + +def essai_(context, REQUEST): + return "

    essai !

    %s

    %s

    " % ( + context, + REQUEST, + ) + + +def essai2(context): + return essai_(context, "sans request") + + +sco_publish("/essai2", essai2, Permission.ScoView) + +# --------------------- + +# +# Cache global: chaque instance, repérée par sa connexion db, a un cache +# qui est recréé à la demande +# +CACHE_formsemestre_inscription = {} +CACHE_evaluations = {} + +# --------------- +def _getNotesCache(context): + "returns CacheNotesTable instance for us" + u = sco_mgr.get_db_uri() # identifie le dept de facon unique + if not NOTES_CACHE_INST.has_key(u): + log("getNotesCache: creating cache for %s" % u) + NOTES_CACHE_INST[u] = CacheNotesTable() + return NOTES_CACHE_INST[u] + + +def _inval_cache( + context, formsemestre_id=None, pdfonly=False, formsemestre_id_list=None +): # > + "expire cache pour un semestre (ou tous si pas d'argument)" + if formsemestre_id_list: + for formsemestre_id in formsemestre_id_list: + context._getNotesCache().inval_cache( + context, formsemestre_id=formsemestre_id, pdfonly=pdfonly + ) + # Affecte aussi cache inscriptions + context.get_formsemestre_inscription_cache().inval_cache( + key=formsemestre_id + ) + else: + context._getNotesCache().inval_cache( + context, formsemestre_id=formsemestre_id, pdfonly=pdfonly + ) + # Affecte aussi cache inscriptions + context.get_formsemestre_inscription_cache().inval_cache(key=formsemestre_id) + + +@bp.route("/clearcache") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def clearcache(context, REQUEST=None): + "Efface les caches de notes (utile pendant developpement slt)" + log("*** clearcache request") + # Debugging code: compare results before and after cache reconstruction + # (_should_ be identicals !) + # Compare XML representation + cache = context._getNotesCache() + formsemestre_ids = cache.get_cached_formsemestre_ids() + docs_before = [] + for formsemestre_id in formsemestre_ids: + docs_before.append( + sco_recapcomplet.do_formsemestre_recapcomplet( + context, REQUEST, formsemestre_id, format="xml", xml_nodate=True + ) + ) + # + cache.inval_cache(context) # > + # Rebuild cache (useful only to debug) + docs_after = [] + for formsemestre_id in formsemestre_ids: + docs_after.append( + sco_recapcomplet.do_formsemestre_recapcomplet( + context, REQUEST, formsemestre_id, format="xml", xml_nodate=True + ) + ) + if docs_before != docs_after: + log("clearcache: inconsistency !") + txt = "before=" + repr(docs_before) + "\n\nafter=" + repr(docs_after) + "\n" + log(txt) + sendAlarm(context, "clearcache: inconsistency !", txt) + + +# -------------------------------------------------------------------- +# +# Notes/ methods +# +# -------------------------------------------------------------------- + +sco_publish( + "/formsemestre_status", + sco_formsemestre_status.formsemestre_status, + Permission.ScoView, +) + +sco_publish( + "/formsemestre_createwithmodules", + sco_formsemestre_edit.formsemestre_createwithmodules, + Permission.ScoImplement, +) + +# controle d'acces specifique pour dir. etud: +sco_publish( + "formsemestre_editwithmodules", + sco_formsemestre_edit.formsemestre_editwithmodules, + Permission.ScoView, +) + +sco_publish( + "formsemestre_clone", + sco_formsemestre_edit.formsemestre_clone, + Permission.ScoImplement, +) +sco_publish( + "/formsemestre_associate_new_version", + sco_formsemestre_edit.formsemestre_associate_new_version, + Permission.ScoChangeFormation, +) +sco_publish( + "/formsemestre_delete", + sco_formsemestre_edit.formsemestre_delete, + Permission.ScoImplement, +) +sco_publish( + "/formsemestre_delete2", + sco_formsemestre_edit.formsemestre_delete2, + Permission.ScoImplement, +) +sco_publish( + "/formsemestre_recapcomplet", + sco_recapcomplet.formsemestre_recapcomplet, + Permission.ScoView, +) +sco_publish( + "/formsemestres_bulletins", + sco_recapcomplet.formsemestres_bulletins, + Permission.ScoObservateur, +) +sco_publish( + "/moduleimpl_status", sco_moduleimpl_status.moduleimpl_status, Permission.ScoView +) +sco_publish( + "/formsemestre_description", + sco_formsemestre_status.formsemestre_description, + Permission.ScoView, +) +sco_publish( + "/formsemestre_lists", + sco_formsemestre_status.formsemestre_lists, + Permission.ScoView, +) +sco_publish( + "/formsemestre_status_menubar", + sco_formsemestre_status.formsemestre_status_menubar, + Permission.ScoView, +) +sco_publish( + "/formation_create", + sco_edit_formation.formation_create, + Permission.ScoChangeFormation, +) +sco_publish( + "/formation_delete", + sco_edit_formation.formation_delete, + Permission.ScoChangeFormation, +) +sco_publish( + "/formation_edit", sco_edit_formation.formation_edit, Permission.ScoChangeFormation +) +sco_publish( + "/formsemestre_bulletinetud", + sco_bulletins.formsemestre_bulletinetud, + Permission.ScoView, +) +sco_publish( + "/formsemestre_evaluations_cal", + sco_evaluations.formsemestre_evaluations_cal, + Permission.ScoView, +) +sco_publish( + "/formsemestre_evaluations_delai_correction", + sco_evaluations.formsemestre_evaluations_delai_correction, + Permission.ScoView, +) +sco_publish( + "/module_evaluation_renumber", + sco_evaluations.module_evaluation_renumber, + Permission.ScoView, +) +sco_publish( + "/module_evaluation_move", + sco_evaluations.module_evaluation_move, + Permission.ScoView, +) +sco_publish( + "/formsemestre_list_saisies_notes", + sco_undo_notes.formsemestre_list_saisies_notes, + Permission.ScoView, +) +sco_publish("/ue_create", sco_edit_ue.ue_create, Permission.ScoChangeFormation) +sco_publish("/ue_delete", sco_edit_ue.ue_delete, Permission.ScoChangeFormation) +sco_publish("/ue_edit", sco_edit_ue.ue_edit, Permission.ScoChangeFormation) +sco_publish("/ue_list", sco_edit_ue.ue_list, Permission.ScoView) +sco_publish("/ue_sharing_code", sco_edit_ue.ue_sharing_code, Permission.ScoView) +sco_publish( + "/edit_ue_set_code_apogee", + sco_edit_ue.edit_ue_set_code_apogee, + Permission.ScoChangeFormation, +) +sco_publish( + "/formation_table_recap", sco_edit_ue.formation_table_recap, Permission.ScoView +) +sco_publish( + "/formation_add_malus_modules", + sco_edit_module.formation_add_malus_modules, + Permission.ScoChangeFormation, +) +sco_publish( + "/matiere_create", sco_edit_matiere.matiere_create, Permission.ScoChangeFormation +) +sco_publish( + "/matiere_delete", sco_edit_matiere.matiere_delete, Permission.ScoChangeFormation +) +sco_publish( + "/matiere_edit", sco_edit_matiere.matiere_edit, Permission.ScoChangeFormation +) +sco_publish( + "/module_create", sco_edit_module.module_create, Permission.ScoChangeFormation +) +sco_publish( + "/module_delete", sco_edit_module.module_delete, Permission.ScoChangeFormation +) +sco_publish("/module_edit", sco_edit_module.module_edit, Permission.ScoChangeFormation) +sco_publish( + "/edit_module_set_code_apogee", + sco_edit_module.edit_module_set_code_apogee, + Permission.ScoChangeFormation, +) +sco_publish("/module_list", sco_edit_module.module_list, Permission.ScoView) +sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) +sco_publish( + "/module_tag_set", sco_tag_module.module_tag_set, Permission.ScoEditFormationTags +) + +# +@bp.route("/") +@bp.route("/index_html") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def index_html(context, REQUEST=None): + "Page accueil formations" + + editable = REQUEST.AUTHENTICATED_USER.has_permission(ScoChangeFormation, context) + + H = [ + context.sco_header(REQUEST, page_title="Programmes formations"), + """

    Programmes pédagogiques

    + """, + ] + T = sco_formations.formation_list_table(context, REQUEST=REQUEST) + + H.append(T.html()) + + if editable: + H.append( + """

    Créer une formation

    +

    Importer une formation (xml)

    +

    Une "formation" est un programme pédagogique structuré en UE, matières et modules. Chaque semestre se réfère à une formation. La modification d'une formation affecte tous les semestres qui s'y réfèrent.

    + """ + ) + + H.append(context.sco_footer(REQUEST)) + return "\n".join(H) + + +# -------------------------------------------------------------------- +# +# Notes Methods +# +# -------------------------------------------------------------------- + +# --- Formations +_formationEditor = ndb.EditableTable( + "notes_formations", + "formation_id", + ( + "formation_id", + "acronyme", + "titre", + "titre_officiel", + "version", + "formation_code", + "type_parcours", + "code_specialite", + ), + sortkey="acronyme", +) + + +@bp.route("/do_formation_create") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def do_formation_create(context, args, REQUEST): + "create a formation" + cnx = context.GetDBConnexion() + # check unique acronyme/titre/version + a = args.copy() + if a.has_key("formation_id"): + del a["formation_id"] + F = context.formation_list(args=a) + if len(F) > 0: + log("do_formation_create: error: %d formations matching args=%s" % (len(F), a)) + raise ScoValueError("Formation non unique (%s) !" % str(a)) + # Si pas de formation_code, l'enleve (default SQL) + if args.has_key("formation_code") and not args["formation_code"]: + del args["formation_code"] + # + r = context._formationEditor.create(cnx, args) + + sco_news.add( + context, + REQUEST, + typ=NEWS_FORM, + text="Création de la formation %(titre)s (%(acronyme)s)" % args, + ) + return r + + +@bp.route("/do_formation_delete") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def do_formation_delete(context, oid, REQUEST): + """delete a formation (and all its UE, matieres, modules) + XXX delete all ues, will break if there are validations ! USE WITH CARE ! + """ + F = context.formation_list(args={"formation_id": oid})[0] + if context.formation_has_locked_sems(oid): + raise ScoLockedFormError() + cnx = context.GetDBConnexion() + # delete all UE in this formation + ues = context.do_ue_list({"formation_id": oid}) + for ue in ues: + context._do_ue_delete(ue["ue_id"], REQUEST=REQUEST, force=True) + + context._formationEditor.delete(cnx, oid) + + # news + sco_news.add( + context, + REQUEST, + typ=NEWS_FORM, + object=oid, + text="Suppression de la formation %(acronyme)s" % F, + ) + + +@bp.route("/formation_list") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formation_list(context, format=None, REQUEST=None, formation_id=None, args={}): + """List formation(s) with given id, or matching args + (when args is given, formation_id is ignored). + """ + # logCallStack() + if not args: + if formation_id is None: + args = {} + else: + args = {"formation_id": formation_id} + cnx = context.GetDBConnexion() + r = context._formationEditor.list(cnx, args=args) + # log('%d formations found' % len(r)) + return scu.sendResult(REQUEST, r, name="formation", format=format) + + +@bp.route("/formation_export") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formation_export( + context, formation_id, export_ids=False, format=None, REQUEST=None +): + "Export de la formation au format indiqué (xml ou json)" + return sco_formations.formation_export( + context, formation_id, export_ids=export_ids, format=format, REQUEST=REQUEST + ) + + +@bp.route("/formation_import_xml") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def formation_import_xml(context, file, REQUEST): + "import d'une formation en XML" + log("formation_import_xml") + doc = file.read() + return sco_formations.formation_import_xml(context, REQUEST, doc) + + +@bp.route("/formation_import_xml_form") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def formation_import_xml_form(context, REQUEST): + "form import d'une formation en XML" + H = [ + context.sco_header(page_title="Import d'une formation", REQUEST=REQUEST), + """

    Import d'une formation

    +

    Création d'une formation (avec UE, matières, modules) + à partir un fichier XML (réservé aux utilisateurs avertis)

    + """, + ] + footer = context.sco_footer(REQUEST) + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + (("xmlfile", {"input_type": "file", "title": "Fichier XML", "size": 30}),), + submitlabel="Importer", + cancelbutton="Annuler", + ) + if tf[0] == 0: + return "\n".join(H) + tf[1] + footer + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect(context.NotesURL()) + else: + formation_id, _, _ = context.formation_import_xml(tf[2]["xmlfile"], REQUEST) + + return ( + "\n".join(H) + + """

    Import effectué !

    +

    Voir la formation

    """ + % formation_id + + footer + ) + + +@bp.route("/formation_create_new_version") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def formation_create_new_version(context, formation_id, redirect=True, REQUEST=None): + "duplicate formation, with new version number" + xml = sco_formations.formation_export( + context, formation_id, export_ids=True, format="xml" + ) + new_id, modules_old2new, ues_old2new = sco_formations.formation_import_xml( + context, REQUEST, xml + ) + # news + F = context.formation_list(args={"formation_id": new_id})[0] + sco_news.add( + context, + REQUEST, + typ=NEWS_FORM, + object=new_id, + text="Nouvelle version de la formation %(acronyme)s" % F, + ) + if redirect: + return REQUEST.RESPONSE.redirect( + "ue_list?formation_id=" + new_id + "&msg=Nouvelle version !" + ) + else: + return new_id, modules_old2new, ues_old2new + + +# --- UE +_ueEditor = ndb.EditableTable( + "notes_ue", + "ue_id", + ( + "ue_id", + "formation_id", + "acronyme", + "numero", + "titre", + "type", + "ue_code", + "ects", + "is_external", + "code_apogee", + "coefficient", + ), + sortkey="numero", + input_formators={"type": ndb.int_null_is_zero}, + output_formators={ + "numero": ndb.int_null_is_zero, + "ects": ndb.float_null_is_null, + "coefficient": ndb.float_null_is_zero, + }, +) + + +@bp.route("/do_ue_create") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def do_ue_create(context, args, REQUEST): + "create an ue" + cnx = context.GetDBConnexion() + # check duplicates + ues = context.do_ue_list( + {"formation_id": args["formation_id"], "acronyme": args["acronyme"]} + ) + if ues: + raise ScoValueError('Acronyme d\'UE "%s" déjà utilisé !' % args["acronyme"]) + # create + r = context._ueEditor.create(cnx, args) + + # news + F = context.formation_list(args={"formation_id": args["formation_id"]})[0] + sco_news.add( + context, + REQUEST, + typ=NEWS_FORM, + object=args["formation_id"], + text="Modification de la formation %(acronyme)s" % F, + ) + return r + + +def _do_ue_delete(context, ue_id, delete_validations=False, REQUEST=None, force=False): + "delete UE and attached matieres (but not modules (it should ?))" + cnx = context.GetDBConnexion() + log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue_id, delete_validations)) + # check + ue = context.do_ue_list({"ue_id": ue_id}) + if not ue: + raise ScoValueError("UE inexistante !") + ue = ue[0] + if context.ue_is_locked(ue["ue_id"]): + raise ScoLockedFormError() + # Il y a-t-il des etudiants ayant validé cette UE ? + # si oui, propose de supprimer les validations + validations = sco_parcours_dut.scolar_formsemestre_validation_list( + cnx, args={"ue_id": ue_id} + ) + if validations and not delete_validations and not force: + return context.confirmDialog( + "

    %d étudiants ont validé l'UE %s (%s)

    Si vous supprimez cette UE, ces validations vont être supprimées !

    " + % (len(validations), ue["acronyme"], ue["titre"]), + dest_url="", + REQUEST=REQUEST, + target_variable="delete_validations", + cancel_url="ue_list?formation_id=%s" % ue["formation_id"], + parameters={"ue_id": ue_id, "dialog_confirmed": 1}, + ) + if delete_validations: + log("deleting all validations of UE %s" % ue_id) + ndb.SimpleQuery( + context, + "DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s", + {"ue_id": ue_id}, + ) + + # delete all matiere in this UE + mats = context.do_matiere_list({"ue_id": ue_id}) + for mat in mats: + context.do_matiere_delete(mat["matiere_id"], REQUEST) + # delete uecoef and events + ndb.SimpleQuery( + context, + "DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s", + {"ue_id": ue_id}, + ) + ndb.SimpleQuery( + context, "DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue_id} + ) + cnx = context.GetDBConnexion() + context._ueEditor.delete(cnx, ue_id) + context._inval_cache() # > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?) + # news + F = context.formation_list(args={"formation_id": ue["formation_id"]})[0] + sco_news.add( + context, + REQUEST, + typ=NEWS_FORM, + object=ue["formation_id"], + text="Modification de la formation %(acronyme)s" % F, + ) + # + if not force: + return REQUEST.RESPONSE.redirect( + context.NotesURL() + "/ue_list?formation_id=" + str(ue["formation_id"]) + ) + else: + return None + + +@bp.route("/do_ue_list") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def do_ue_list(context, *args, **kw): + "list UEs" + cnx = context.GetDBConnexion() + return context._ueEditor.list(cnx, *args, **kw) + + +# --- Matieres +_matiereEditor = ndb.EditableTable( + "notes_matieres", + "matiere_id", + ("matiere_id", "ue_id", "numero", "titre"), + sortkey="numero", + output_formators={"numero": ndb.int_null_is_zero}, +) + + +@bp.route("/do_matiere_create") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def do_matiere_create(context, args, REQUEST): + "create a matiere" + cnx = context.GetDBConnexion() + # check + ue = context.do_ue_list({"ue_id": args["ue_id"]})[0] + # create matiere + r = context._matiereEditor.create(cnx, args) + + # news + F = context.formation_list(args={"formation_id": ue["formation_id"]})[0] + sco_news.add( + context, + REQUEST, + typ=NEWS_FORM, + object=ue["formation_id"], + text="Modification de la formation %(acronyme)s" % F, + ) + return r + + +@bp.route("/do_matiere_delete") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def do_matiere_delete(context, oid, REQUEST): + "delete matiere and attached modules" + cnx = context.GetDBConnexion() + # check + mat = context.do_matiere_list({"matiere_id": oid})[0] + ue = context.do_ue_list({"ue_id": mat["ue_id"]})[0] + locked = context.matiere_is_locked(mat["matiere_id"]) + if locked: + log("do_matiere_delete: mat=%s" % mat) + log("do_matiere_delete: ue=%s" % ue) + log("do_matiere_delete: locked sems: %s" % locked) + raise ScoLockedFormError() + log("do_matiere_delete: matiere_id=%s" % oid) + # delete all modules in this matiere + mods = context.do_module_list({"matiere_id": oid}) + for mod in mods: + context.do_module_delete(mod["module_id"], REQUEST) + context._matiereEditor.delete(cnx, oid) + + # news + F = context.formation_list(args={"formation_id": ue["formation_id"]})[0] + sco_news.add( + context, + REQUEST, + typ=NEWS_FORM, + object=ue["formation_id"], + text="Modification de la formation %(acronyme)s" % F, + ) + + +@bp.route("/do_matiere_list") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def do_matiere_list(context, *args, **kw): + "list matieres" + cnx = context.GetDBConnexion() + return context._matiereEditor.list(cnx, *args, **kw) + + +@bp.route("/do_matiere_edit") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def do_matiere_edit(context, *args, **kw): + "edit a matiere" + cnx = context.GetDBConnexion() + # check + mat = context.do_matiere_list({"matiere_id": args[0]["matiere_id"]})[0] + if context.matiere_is_locked(mat["matiere_id"]): + raise ScoLockedFormError() + # edit + context._matiereEditor.edit(cnx, *args, **kw) + context._inval_cache() # > modif matiere + + +@bp.route("/do_matiere_formation_id") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def do_matiere_formation_id(context, matiere_id): + "get formation_id from matiere" + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + cursor.execute( + "select UE.formation_id from notes_matieres M, notes_ue UE where M.matiere_id = %(matiere_id)s and M.ue_id = UE.ue_id", + {"matiere_id": matiere_id}, + ) + res = cursor.fetchall() + return res[0][0] + + +# --- Modules +_moduleEditor = ndb.EditableTable( + "notes_modules", + "module_id", + ( + "module_id", + "titre", + "code", + "abbrev", + "heures_cours", + "heures_td", + "heures_tp", + "coefficient", + "ue_id", + "matiere_id", + "formation_id", + "semestre_id", + "numero", + "code_apogee", + "module_type" + #'ects' + ), + sortkey="numero, code, titre", + output_formators={ + "heures_cours": ndb.float_null_is_zero, + "heures_td": ndb.float_null_is_zero, + "heures_tp": ndb.float_null_is_zero, + "numero": ndb.int_null_is_zero, + "coefficient": ndb.float_null_is_zero, + "module_type": ndb.int_null_is_zero + #'ects' : ndb.float_null_is_null + }, +) + + +@bp.route("/do_module_create") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def do_module_create(context, args, REQUEST): + "create a module" + # create + cnx = context.GetDBConnexion() + r = context._moduleEditor.create(cnx, args) + + # news + F = context.formation_list(args={"formation_id": args["formation_id"]})[0] + sco_news.add( + context, + REQUEST, + typ=NEWS_FORM, + object=args["formation_id"], + text="Modification de la formation %(acronyme)s" % F, + ) + return r + + +@bp.route("/do_module_delete") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def do_module_delete(context, oid, REQUEST): + "delete module" + mod = context.do_module_list({"module_id": oid})[0] + if context.module_is_locked(mod["module_id"]): + raise ScoLockedFormError() + + # S'il y a des moduleimpls, on ne peut pas detruire le module ! + mods = sco_moduleimpl.do_moduleimpl_list(context, module_id=oid) + if mods: + err_page = context.confirmDialog( + message="""

    Destruction du module impossible car il est utilisé dans des semestres existants !

    """, + helpmsg="""Il faut d'abord supprimer le semestre. Mais il est peut être préférable de laisser ce programme intact et d'en créer une nouvelle version pour la modifier.""", + dest_url="ue_list", + parameters={"formation_id": mod["formation_id"]}, + REQUEST=REQUEST, + ) + raise ScoGenError(err_page) + # delete + cnx = context.GetDBConnexion() + context._moduleEditor.delete(cnx, oid) + + # news + F = context.formation_list(args={"formation_id": mod["formation_id"]})[0] + sco_news.add( + context, + REQUEST, + typ=NEWS_FORM, + object=mod["formation_id"], + text="Modification de la formation %(acronyme)s" % F, + ) + + +@bp.route("/do_module_list") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def do_module_list(context, *args, **kw): + "list modules" + cnx = context.GetDBConnexion() + return context._moduleEditor.list(cnx, *args, **kw) + + +@bp.route("/do_module_edit") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def do_module_edit(context, val): + "edit a module" + # check + mod = context.do_module_list({"module_id": val["module_id"]})[0] + if context.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] + # edit + cnx = context.GetDBConnexion() + context._moduleEditor.edit(cnx, val) + + sems = sco_formsemestre.do_formsemestre_list( + context, args={"formation_id": mod["formation_id"]} + ) + if sems: + context._inval_cache( + formsemestre_id_list=[s["formsemestre_id"] for s in sems] + ) # > modif module + + +# +@bp.route("/formation_has_locked_sems") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formation_has_locked_sems(context, formation_id): + "True if there is a locked formsemestre in this formation" + sems = sco_formsemestre.do_formsemestre_list( + context, args={"formation_id": formation_id, "etat": "0"} + ) + return sems + + +@bp.route("/formation_count_sems") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formation_count_sems(context, formation_id): + "Number of formsemestre in this formation (locked or not)" + sems = sco_formsemestre.do_formsemestre_list( + context, args={"formation_id": formation_id} + ) + return len(sems) + + +@bp.route("/module_count_moduleimpls") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def module_count_moduleimpls(context, module_id): + "Number of moduleimpls using this module" + mods = sco_moduleimpl.do_moduleimpl_list(context, module_id=module_id) + return len(mods) + + +@bp.route("/module_is_locked") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def module_is_locked(context, module_id): + """True if UE should not be modified + (used in a locked formsemestre) + """ + r = ndb.SimpleDictFetch( + context, + """SELECT mi.* from notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi + WHERE mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id + AND mi.module_id = %(module_id)s AND sem.etat = 0 + """, + {"module_id": module_id}, + ) + return len(r) > 0 + + +@bp.route("/matiere_is_locked") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def matiere_is_locked(context, matiere_id): + """True if matiere should not be modified + (contains modules used in a locked formsemestre) + """ + r = ndb.SimpleDictFetch( + context, + """SELECT ma.* from notes_matieres ma, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi + WHERE ma.matiere_id = mod.matiere_id AND mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id + AND ma.matiere_id = %(matiere_id)s AND sem.etat = 0 + """, + {"matiere_id": matiere_id}, + ) + return len(r) > 0 + + +@bp.route("/ue_is_locked") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def ue_is_locked(context, ue_id): + """True if module should not be modified + (contains modules used in a locked formsemestre) + """ + r = ndb.SimpleDictFetch( + context, + """SELECT ue.* FROM notes_ue ue, notes_modules mod, notes_formsemestre sem, notes_moduleimpl mi + WHERE ue.ue_id = mod.ue_id + AND mi.module_id = mod.module_id AND mi.formsemestre_id = sem.formsemestre_id + AND ue.ue_id = %(ue_id)s AND sem.etat = 0 + """, + {"ue_id": ue_id}, + ) + return len(r) > 0 + + +@bp.route("/module_move") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def module_move(context, module_id, after=0, REQUEST=None, redirect=1): + """Move before/after previous one (decrement/increment numero)""" + module = context.do_module_list({"module_id": module_id})[0] + redirect = int(redirect) + after = int(after) # 0: deplace avant, 1 deplace apres + if after not in (0, 1): + raise ValueError('invalid value for "after"') + formation_id = module["formation_id"] + others = context.do_module_list({"matiere_id": module["matiere_id"]}) + # log('others=%s' % others) + if len(others) > 1: + idx = [p["module_id"] for p in others].index(module_id) + # log('module_move: after=%s idx=%s' % (after, idx)) + neigh = None # object to swap with + if after == 0 and idx > 0: + neigh = others[idx - 1] + elif after == 1 and idx < len(others) - 1: + neigh = others[idx + 1] + if neigh: # + # swap numero between partition and its neighbor + # log('moving module %s' % module_id) + cnx = context.GetDBConnexion() + module["numero"], neigh["numero"] = neigh["numero"], module["numero"] + if module["numero"] == neigh["numero"]: + neigh["numero"] -= 2 * after - 1 + context._moduleEditor.edit(cnx, module) + context._moduleEditor.edit(cnx, neigh) + + # redirect to ue_list page: + if redirect: + return REQUEST.RESPONSE.redirect("ue_list?formation_id=" + formation_id) + + +@bp.route("/ue_move") +@scodoc7func(context) +@permission_required(Permission.ScoChangeFormation) +def ue_move(context, ue_id, after=0, REQUEST=None, redirect=1): + """Move UE before/after previous one (decrement/increment numero)""" + o = context.do_ue_list({"ue_id": ue_id})[0] + # log('ue_move %s (#%s) after=%s' % (ue_id, o['numero'], after)) + redirect = int(redirect) + after = int(after) # 0: deplace avant, 1 deplace apres + if after not in (0, 1): + raise ValueError('invalid value for "after"') + formation_id = o["formation_id"] + others = context.do_ue_list({"formation_id": formation_id}) + if len(others) > 1: + idx = [p["ue_id"] for p in others].index(ue_id) + neigh = None # object to swap with + if after == 0 and idx > 0: + neigh = others[idx - 1] + elif after == 1 and idx < len(others) - 1: + neigh = others[idx + 1] + if neigh: # + # swap numero between partition and its neighbor + # log('moving ue %s (neigh #%s)' % (ue_id, neigh['numero'])) + cnx = context.GetDBConnexion() + o["numero"], neigh["numero"] = neigh["numero"], o["numero"] + if o["numero"] == neigh["numero"]: + neigh["numero"] -= 2 * after - 1 + context._ueEditor.edit(cnx, o) + context._ueEditor.edit(cnx, neigh) + # redirect to ue_list page + if redirect: + return REQUEST.RESPONSE.redirect("ue_list?formation_id=" + o["formation_id"]) + + +# --- Semestres de formation + + +@bp.route("/do_formsemestre_create") +@scodoc7func(context) +@permission_required(Permission.ScoImplement) +def do_formsemestre_create(context, args, REQUEST, silent=False): + "create a formsemestre" + cnx = context.GetDBConnexion() + formsemestre_id = sco_formsemestre._formsemestreEditor.create(cnx, args) + if args["etapes"]: + args["formsemestre_id"] = formsemestre_id + sco_formsemestre.write_formsemestre_etapes(context, args) + if args["responsables"]: + args["formsemestre_id"] = formsemestre_id + sco_formsemestre.write_formsemestre_responsables(context, args) + + # create default partition + partition_id = sco_groups.partition_create( + context, formsemestre_id, default=True, redirect=0, REQUEST=REQUEST + ) + _group_id = sco_groups.createGroup( + context, partition_id, default=True, REQUEST=REQUEST + ) + + # news + if not args.has_key("titre"): + args["titre"] = "sans titre" + args["formsemestre_id"] = formsemestre_id + args["url"] = "Notes/formsemestre_status?formsemestre_id=%(formsemestre_id)s" % args + if not silent: + sco_news.add( + context, + REQUEST, + typ=NEWS_SEM, + text='Création du semestre %(titre)s' % args, + url=args["url"], + ) + return formsemestre_id + + +@bp.route("/formsemestre_list") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_list( + context, + format=None, + REQUEST=None, + formsemestre_id=None, + formation_id=None, + etape_apo=None, +): + """List formsemestres in given format. + kw can specify some conditions: examples: + formsemestre_list( format='json', formation_id='F777', REQUEST=REQUEST) + """ + # XAPI: new json api + args = {} + L = locals() + for argname in ("formsemestre_id", "formation_id", "etape_apo"): + if L[argname] is not None: + args[argname] = L[argname] + sems = sco_formsemestre.do_formsemestre_list(context, args=args) + # log('formsemestre_list: format="%s", %s semestres found' % (format,len(sems))) + return scu.sendResult(REQUEST, sems, name="formsemestre", format=format) + + +@bp.route("/XMLgetFormsemestres") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def XMLgetFormsemestres(context, etape_apo=None, formsemestre_id=None, REQUEST=None): + """List all formsemestres matching etape, XML format + DEPRECATED: use formsemestre_list() + """ + log("Warning: calling deprecated XMLgetFormsemestres") + args = {} + if etape_apo: + args["etape_apo"] = etape_apo + if formsemestre_id: + args["formsemestre_id"] = formsemestre_id + if REQUEST: + REQUEST.RESPONSE.setHeader("content-type", scu.XML_MIMETYPE) + doc = jaxml.XML_document(encoding=scu.SCO_ENCODING) + doc.formsemestrelist() + for sem in sco_formsemestre.do_formsemestre_list(context, args=args): + doc._push() + doc.formsemestre(sem) + doc._pop() + return repr(doc) + + +sco_publish( + "/do_formsemestre_edit", + sco_formsemestre.do_formsemestre_edit, + Permission.ScoImplement, +) +sco_publish( + "/formsemestre_edit_options", + sco_formsemestre_edit.formsemestre_edit_options, + Permission.ScoView, +) +sco_publish( + "/formsemestre_change_lock", + sco_formsemestre_edit.formsemestre_change_lock, + Permission.ScoView, +) +sco_publish( + "/formsemestre_change_publication_bul", + sco_formsemestre_edit.formsemestre_change_publication_bul, + Permission.ScoView, +) +sco_publish( + "/view_formsemestre_by_etape", + sco_formsemestre.view_formsemestre_by_etape, + Permission.ScoView, +) + + +def _check_access_diretud( + context, formsemestre_id, REQUEST, required_permission=ScoImplement +): + """Check if access granted: responsable or ScoImplement + Return True|False, HTML_error_page + """ + authuser = REQUEST.AUTHENTICATED_USER + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + header = context.sco_header(page_title="Accès interdit", REQUEST=REQUEST) + footer = context.sco_footer(REQUEST) + if (str(authuser) not in sem["responsables"]) and not authuser.has_permission( + required_permission, context + ): + return ( + False, + "\n".join( + [ + header, + "

    Opération non autorisée pour %s

    " % authuser, + "

    Responsable de ce semestre : %s

    " + % ", ".join(sem["responsables"]), + footer, + ] + ), + ) + else: + return True, "" + + +@bp.route("/formsemestre_custommenu_edit") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_custommenu_edit(context, REQUEST, formsemestre_id): + "Dialogue modif menu" + # accessible à tous ! + return sco_formsemestre_custommenu.formsemestre_custommenu_edit( + context, formsemestre_id, REQUEST=REQUEST + ) + + +sco_publish( + "/formsemestre_custommenu_html", + sco_formsemestre_custommenu.formsemestre_custommenu_html, + Permission.ScoView, +) + + +@bp.route("/html_sem_header") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def html_sem_header( + context, + REQUEST, + title, + sem=None, + with_page_header=True, + with_h2=True, + page_title=None, + **args +): + "Titre d'une page semestre avec lien vers tableau de bord" + # sem now unused and thus optional... + if with_page_header: + h = context.sco_header(REQUEST, page_title="%s" % (page_title or title), **args) + else: + h = "" + if with_h2: + return h + """

    %s

    """ % (title) + else: + return h + + +# --- dialogue modif enseignants/moduleimpl +@bp.route("/edit_enseignants_form") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def edit_enseignants_form(context, REQUEST, moduleimpl_id): + "modif liste enseignants/moduleimpl" + M, sem = sco_moduleimpl.can_change_ens(context, REQUEST, moduleimpl_id) + # -- + header = context.html_sem_header( + REQUEST, + 'Enseignants du module %s' + % (moduleimpl_id, M["module"]["titre"]), + page_title="Enseignants du module %s" % M["module"]["titre"], + javascripts=["libjs/AutoSuggest.js"], + cssstyles=["css/autosuggest_inquisitor.css"], + bodyOnLoad="init_tf_form('')", + ) + footer = context.sco_footer(REQUEST) + + # Liste des enseignants avec forme pour affichage / saisie avec suggestion + userlist = context.Users.get_userlist() + login2display = {} # user_name : forme pour affichage = "NOM Prenom (login)" + for u in userlist: + login2display[u["user_name"]] = u["nomplogin"] + allowed_user_names = login2display.values() + + H = [ + "
    • %s (responsable)
    • " + % login2display.get(M["responsable_id"], M["responsable_id"]) + ] + for ens in M["ens"]: + H.append( + '
    • %s (supprimer)
    • ' + % ( + login2display.get(ens["ens_id"], ens["ens_id"]), + moduleimpl_id, + ens["ens_id"], + ) + ) + H.append("
    ") + F = """

    Les enseignants d'un module ont le droit de + saisir et modifier toutes les notes des évaluations de ce module. +

    +

    Pour changer le responsable du module, passez par la + page "Modification du semestre", accessible uniquement au responsable de la formation (chef de département) +

    + """ % ( + sem["formation_id"], + M["formsemestre_id"], + ) + + modform = [ + ("moduleimpl_id", {"input_type": "hidden"}), + ( + "ens_id", + { + "input_type": "text_suggest", + "size": 50, + "title": "Ajouter un enseignant", + "allowed_values": allowed_user_names, + "allow_null": False, + "text_suggest_options": { + "script": "Users/get_userlist_xml?", + "varname": "start", + "json": False, + "noresults": "Valeur invalide !", + "timeout": 60000, + }, + }, + ), + ] + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + modform, + submitlabel="Ajouter enseignant", + cancelbutton="Annuler", + ) + if tf[0] == 0: + return header + "\n".join(H) + tf[1] + F + footer + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect( + "moduleimpl_status?moduleimpl_id=" + moduleimpl_id + ) + else: + ens_id = context.Users.get_user_name_from_nomplogin(tf[2]["ens_id"]) + if not ens_id: + H.append( + '

    Pour ajouter un enseignant, choisissez un nom dans le menu

    ' + ) + else: + # et qu'il n'est pas deja: + if ( + ens_id in [x["ens_id"] for x in M["ens"]] + or ens_id == M["responsable_id"] + ): + H.append( + '

    Enseignant %s déjà dans la liste !

    ' % ens_id + ) + else: + sco_moduleimpl.do_ens_create( + context, {"moduleimpl_id": moduleimpl_id, "ens_id": ens_id} + ) + return REQUEST.RESPONSE.redirect( + "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id + ) + return header + "\n".join(H) + tf[1] + F + footer + + +@bp.route("/edit_moduleimpl_resp") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def edit_moduleimpl_resp(context, REQUEST, moduleimpl_id): + """Changement d'un enseignant responsable de module + Accessible par Admin et dir des etud si flag resp_can_change_ens + """ + M, sem = sco_moduleimpl.can_change_module_resp(context, REQUEST, moduleimpl_id) + H = [ + context.html_sem_header( + REQUEST, + 'Modification du responsable du module %s' + % (moduleimpl_id, M["module"]["titre"]), + sem, + javascripts=["libjs/AutoSuggest.js"], + cssstyles=["css/autosuggest_inquisitor.css"], + bodyOnLoad="init_tf_form('')", + ) + ] + help = """

    Taper le début du nom de l'enseignant.

    """ + # Liste des enseignants avec forme pour affichage / saisie avec suggestion + userlist = context.Users.get_userlist() + login2display = {} # user_name : forme pour affichage = "NOM Prenom (login)" + for u in userlist: + login2display[u["user_name"]] = u["nomplogin"] + allowed_user_names = login2display.values() + + initvalues = M + initvalues["responsable_id"] = login2display.get( + M["responsable_id"], M["responsable_id"] + ) + form = [ + ("moduleimpl_id", {"input_type": "hidden"}), + ( + "responsable_id", + { + "input_type": "text_suggest", + "size": 50, + "title": "Responsable du module", + "allowed_values": allowed_user_names, + "allow_null": False, + "text_suggest_options": { + "script": "Users/get_userlist_xml?", + "varname": "start", + "json": False, + "noresults": "Valeur invalide !", + "timeout": 60000, + }, + }, + ), + ] + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + form, + submitlabel="Changer responsable", + cancelbutton="Annuler", + initvalues=initvalues, + ) + if tf[0] == 0: + return "\n".join(H) + tf[1] + help + context.sco_footer(REQUEST) + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect( + "moduleimpl_status?moduleimpl_id=" + moduleimpl_id + ) + else: + responsable_id = context.Users.get_user_name_from_nomplogin( + tf[2]["responsable_id"] + ) + if ( + not responsable_id + ): # presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps) + return REQUEST.RESPONSE.redirect( + "moduleimpl_status?moduleimpl_id=" + moduleimpl_id + ) + sco_moduleimpl.do_moduleimpl_edit( + context, + {"moduleimpl_id": moduleimpl_id, "responsable_id": responsable_id}, + formsemestre_id=sem["formsemestre_id"], + ) + return REQUEST.RESPONSE.redirect( + "moduleimpl_status?moduleimpl_id=" + + moduleimpl_id + + "&head_message=responsable%20modifié" + ) + + +_expr_help = """

    Expérimental: formule de calcul de la moyenne %(target)s

    +

    Attention: l'utilisation de formules ralenti considérablement + les traitements. A utiliser uniquement dans els cas ne pouvant pas être traités autrement.

    +

    Dans la formule, les variables suivantes sont définies:

    +
      +
    • moy la moyenne, calculée selon la règle standard (moyenne pondérée)
    • +
    • moy_is_valid vrai si la moyenne est valide (numérique)
    • +
    • moy_val la valeur de la moyenne (nombre, valant 0 si invalide)
    • +
    • notes vecteur des notes (/20) aux %(objs)s
    • +
    • coefs vecteur des coefficients des %(objs)s, les coefs des %(objs)s sans notes (ATT, EXC) étant mis à zéro
    • +
    • cmask vecteur de 0/1, 0 si le coef correspondant a été annulé
    • +
    • Nombre d'absences: nb_abs, nb_abs_just, nb_abs_nojust (en demi-journées)
    • +
    +

    Les éléments des vecteurs sont ordonnés dans l'ordre des %(objs)s%(ordre)s.

    +

    Les fonctions suivantes sont utilisables: abs, cmp, dot, len, map, max, min, pow, reduce, round, sum, ifelse.

    +

    La notation V(1,2,3) représente un vecteur (1,2,3).

    +

    Pour indiquer que la note calculée n'existe pas, utiliser la chaîne 'NA'.

    +

    Vous pouvez désactiver la formule (et revenir au mode de calcul "classique") + en supprimant le texte ou en faisant précéder la première ligne par #

    +""" + + +@bp.route("/edit_moduleimpl_expr") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def edit_moduleimpl_expr(context, REQUEST, moduleimpl_id): + """Edition formule calcul moyenne module + Accessible par Admin, dir des etud et responsable module + """ + M, sem = sco_moduleimpl.can_change_ens(context, REQUEST, moduleimpl_id) + H = [ + context.html_sem_header( + REQUEST, + 'Modification règle de calcul du module %s' + % (moduleimpl_id, M["module"]["titre"]), + sem, + ), + context._expr_help + % { + "target": "du module", + "objs": "évaluations", + "ordre": " (le premier élément est la plus ancienne évaluation)", + }, + ] + initvalues = M + form = [ + ("moduleimpl_id", {"input_type": "hidden"}), + ( + "computation_expr", + { + "title": "Formule de calcul", + "input_type": "textarea", + "rows": 4, + "cols": 60, + "explanation": "formule de calcul (expérimental)", + }, + ), + ] + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + form, + submitlabel="Modifier formule de calcul", + cancelbutton="Annuler", + initvalues=initvalues, + ) + if tf[0] == 0: + return "\n".join(H) + tf[1] + context.sco_footer(REQUEST) + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect( + "moduleimpl_status?moduleimpl_id=" + moduleimpl_id + ) + else: + sco_moduleimpl.do_moduleimpl_edit( + context, + { + "moduleimpl_id": moduleimpl_id, + "computation_expr": tf[2]["computation_expr"], + }, + formsemestre_id=sem["formsemestre_id"], + ) + context._inval_cache( + formsemestre_id=sem["formsemestre_id"] + ) # > modif regle calcul + return REQUEST.RESPONSE.redirect( + "moduleimpl_status?moduleimpl_id=" + + moduleimpl_id + + "&head_message=règle%20de%20calcul%20modifiée" + ) + + +@bp.route("/view_module_abs") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def view_module_abs(context, REQUEST, moduleimpl_id, format="html"): + """Visualisation des absences a un module""" + M = sco_moduleimpl.do_moduleimpl_withmodule_list( + context, moduleimpl_id=moduleimpl_id + )[0] + sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"]) + debut_sem = ndb.DateDMYtoISO(sem["date_debut"]) + fin_sem = ndb.DateDMYtoISO(sem["date_fin"]) + list_insc = sco_moduleimpl.do_moduleimpl_listeetuds(context, moduleimpl_id) + + T = [] + for etudid in list_insc: + nb_abs = context.Absences.CountAbs( + etudid=etudid, debut=debut_sem, fin=fin_sem, moduleimpl_id=moduleimpl_id + ) + if nb_abs: + nb_abs_just = context.Absences.CountAbsJust( + etudid=etudid, + debut=debut_sem, + fin=fin_sem, + moduleimpl_id=moduleimpl_id, + ) + etud = context.getEtudInfo(etudid=etudid, filled=True)[0] + T.append( + { + "nomprenom": etud["nomprenom"], + "just": nb_abs_just, + "nojust": nb_abs - nb_abs_just, + "total": nb_abs, + "_nomprenom_target": "ficheEtud?etudid=%s" % etudid, + } + ) + + H = [ + context.html_sem_header( + REQUEST, + 'Absences du module %s' + % (moduleimpl_id, M["module"]["titre"]), + page_title="Absences du module %s" % (M["module"]["titre"]), + sem=sem, + ) + ] + if not T and format == "html": + return ( + "\n".join(H) + + "

    Aucune absence signalée

    " + + context.sco_footer(REQUEST) + ) + + tab = GenTable( + titles={ + "nomprenom": "Nom", + "just": "Just.", + "nojust": "Non Just.", + "total": "Total", + }, + columns_ids=("nomprenom", "just", "nojust", "total"), + rows=T, + html_class="table_leftalign", + base_url="%s?moduleimpl_id=%s" % (REQUEST.URL0, moduleimpl_id), + filename="absmodule_" + scu.make_filename(M["module"]["titre"]), + caption="Absences dans le module %s" % M["module"]["titre"], + preferences=context.get_preferences(), + ) + + if format != "html": + return tab.make_page(context, format=format, REQUEST=REQUEST) + + return "\n".join(H) + tab.html() + context.sco_footer(REQUEST) + + +@bp.route("/edit_ue_expr") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def edit_ue_expr(context, REQUEST, formsemestre_id, ue_id): + """Edition formule calcul moyenne UE""" + # Check access + sem = sco_formsemestre_edit.can_edit_sem(context, REQUEST, formsemestre_id) + if not sem: + raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération") + cnx = context.GetDBConnexion() + # + ue = context.do_ue_list({"ue_id": ue_id})[0] + H = [ + context.html_sem_header( + REQUEST, + "Modification règle de calcul de l'UE %s (%s)" + % (ue["acronyme"], ue["titre"]), + sem, + ), + context._expr_help % {"target": "de l'UE", "objs": "modules", "ordre": ""}, + ] + el = sco_compute_moy.formsemestre_ue_computation_expr_list( + cnx, {"formsemestre_id": formsemestre_id, "ue_id": ue_id} + ) + if el: + initvalues = el[0] + else: + initvalues = {} + form = [ + ("ue_id", {"input_type": "hidden"}), + ("formsemestre_id", {"input_type": "hidden"}), + ( + "computation_expr", + { + "title": "Formule de calcul", + "input_type": "textarea", + "rows": 4, + "cols": 60, + "explanation": "formule de calcul (expérimental)", + }, + ), + ] + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + form, + submitlabel="Modifier formule de calcul", + cancelbutton="Annuler", + initvalues=initvalues, + ) + if tf[0] == 0: + return "\n".join(H) + tf[1] + context.sco_footer(REQUEST) + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect( + "formsemestre_status?formsemestre_id=" + formsemestre_id + ) + else: + if el: + el[0]["computation_expr"] = tf[2]["computation_expr"] + sco_compute_moy.formsemestre_ue_computation_expr_edit(cnx, el[0]) + else: + sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, tf[2]) + + context._inval_cache(formsemestre_id=formsemestre_id) # > modif regle calcul + return REQUEST.RESPONSE.redirect( + "formsemestre_status?formsemestre_id=" + + formsemestre_id + + "&head_message=règle%20de%20calcul%20modifiée" + ) + + +@bp.route("/formsemestre_enseignants_list") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_enseignants_list(context, REQUEST, formsemestre_id, format="html"): + """Liste les enseignants intervenants dans le semestre (resp. modules et chargés de TD) + et indique les absences saisies par chacun. + """ + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + # resp. de modules: + mods = sco_moduleimpl.do_moduleimpl_withmodule_list( + context, formsemestre_id=formsemestre_id + ) + sem_ens = {} + for mod in mods: + if not mod["responsable_id"] in sem_ens: + sem_ens[mod["responsable_id"]] = {"mods": [mod]} + else: + sem_ens[mod["responsable_id"]]["mods"].append(mod) + # charges de TD: + for mod in mods: + for ensd in mod["ens"]: + if not ensd["ens_id"] in sem_ens: + sem_ens[ensd["ens_id"]] = {"mods": [mod]} + else: + sem_ens[ensd["ens_id"]]["mods"].append(mod) + # compte les absences ajoutées par chacun dans tout le semestre + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + for ens in sem_ens: + cursor.execute( + "select * from scolog L, notes_formsemestre_inscription I where method='AddAbsence' and authenticated_user=%(authenticated_user)s and L.etudid = I.etudid and I.formsemestre_id=%(formsemestre_id)s and date > %(date_debut)s and date < %(date_fin)s", + { + "authenticated_user": ens, + "formsemestre_id": formsemestre_id, + "date_debut": ndb.DateDMYtoISO(sem["date_debut"]), + "date_fin": ndb.DateDMYtoISO(sem["date_fin"]), + }, + ) + + events = cursor.dictfetchall() + sem_ens[ens]["nbabsadded"] = len(events) + + # description textuelle des modules + for ens in sem_ens: + sem_ens[ens]["descr_mods"] = ", ".join( + [x["module"]["code"] for x in sem_ens[ens]["mods"]] + ) + + # ajoute infos sur enseignant: + for ens in sem_ens: + sem_ens[ens].update(context.Users.user_info(ens)) + if sem_ens[ens]["email"]: + sem_ens[ens]["_email_target"] = "mailto:%s" % sem_ens[ens]["email"] + + sem_ens_list = sem_ens.values() + sem_ens_list.sort(lambda x, y: cmp(x["nomprenom"], y["nomprenom"])) + + # --- Generate page with table + title = "Enseignants de " + sem["titremois"] + T = GenTable( + columns_ids=["nom_fmt", "prenom_fmt", "descr_mods", "nbabsadded", "email"], + titles={ + "nom_fmt": "Nom", + "prenom_fmt": "Prénom", + "email": "Mail", + "descr_mods": "Modules", + "nbabsadded": "Saisies Abs.", + }, + rows=sem_ens_list, + html_sortable=True, + html_class="table_leftalign", + filename=scu.make_filename("Enseignants-" + sem["titreannee"]), + html_title=context.html_sem_header( + REQUEST, "Enseignants du semestre", sem, with_page_header=False + ), + base_url="%s?formsemestre_id=%s" % (REQUEST.URL0, formsemestre_id), + caption="Tous les enseignants (responsables ou associés aux modules de ce semestre) apparaissent. Le nombre de saisies d'absences est le nombre d'opérations d'ajout effectuées sur ce semestre, sans tenir compte des annulations ou double saisies.", + preferences=context.get_preferences(formsemestre_id), + ) + return T.make_page( + context, page_title=title, title=title, REQUEST=REQUEST, format=format + ) + + +@bp.route("/edit_enseignants_form_delete") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def edit_enseignants_form_delete(context, REQUEST, moduleimpl_id, ens_id): + "remove ens" + M, _ = sco_moduleimpl.can_change_ens(context, REQUEST, moduleimpl_id) + # search ens_id + ok = False + for ens in M["ens"]: + if ens["ens_id"] == ens_id: + ok = True + break + if not ok: + raise ScoValueError("invalid ens_id (%s)" % ens_id) + sco_moduleimpl.do_ens_delete(context, ens["modules_enseignants_id"]) + return REQUEST.RESPONSE.redirect( + "edit_enseignants_form?moduleimpl_id=%s" % moduleimpl_id + ) + + +# --- Gestion des inscriptions aux modules +_formsemestre_inscriptionEditor = ndb.EditableTable( + "notes_formsemestre_inscription", + "formsemestre_inscription_id", + ("formsemestre_inscription_id", "etudid", "formsemestre_id", "etat", "etape"), + sortkey="formsemestre_id", +) + + +@bp.route("/do_formsemestre_inscription_create") +@scodoc7func(context) +@permission_required(Permission.ScoEtudInscrit) +def do_formsemestre_inscription_create(context, args, REQUEST, method=None): + "create a formsemestre_inscription (and sco event)" + cnx = context.GetDBConnexion() + log("do_formsemestre_inscription_create: args=%s" % str(args)) + sems = sco_formsemestre.do_formsemestre_list( + context, {"formsemestre_id": args["formsemestre_id"]} + ) + if len(sems) != 1: + raise ScoValueError("code de semestre invalide: %s" % args["formsemestre_id"]) + sem = sems[0] + # check lock + if sem["etat"] != "1": + raise ScoValueError("inscription: semestre verrouille") + # + r = context._formsemestre_inscriptionEditor.create(cnx, args) + # Evenement + scolars.scolar_events_create( + cnx, + args={ + "etudid": args["etudid"], + "event_date": time.strftime("%d/%m/%Y"), + "formsemestre_id": args["formsemestre_id"], + "event_type": "INSCRIPTION", + }, + ) + # Log etudiant + logdb( + REQUEST, + cnx, + method=method, + etudid=args["etudid"], + msg="inscription en semestre %s" % args["formsemestre_id"], + commit=False, + ) + # + context._inval_cache( + formsemestre_id=args["formsemestre_id"] + ) # > inscription au semestre + return r + + +@bp.route("/do_formsemestre_inscription_delete") +@scodoc7func(context) +@permission_required(Permission.ScoImplement) +def do_formsemestre_inscription_delete(context, oid, formsemestre_id=None): + "delete formsemestre_inscription" + cnx = context.GetDBConnexion() + context._formsemestre_inscriptionEditor.delete(cnx, oid) + + context._inval_cache( + formsemestre_id=formsemestre_id + ) # > desinscription du semestre + + +@bp.route("/do_formsemestre_inscription_list") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def do_formsemestre_inscription_list(context, *args, **kw): + "list formsemestre_inscriptions" + cnx = context.GetDBConnexion() + return context._formsemestre_inscriptionEditor.list(cnx, *args, **kw) + + +@bp.route("/do_formsemestre_inscription_listinscrits") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def do_formsemestre_inscription_listinscrits( + context, formsemestre_id, format=None, REQUEST=None +): + """Liste les inscrits (état I) à ce semestre et cache le résultat""" + cache = context.get_formsemestre_inscription_cache() + r = cache.get(formsemestre_id) + if r is None: + # retreive list + r = context.do_formsemestre_inscription_list( + args={"formsemestre_id": formsemestre_id, "etat": "I"} + ) + cache.set(formsemestre_id, r) + return scu.sendResult(REQUEST, r, format=format, name="inscrits") + + +@bp.route("/do_formsemestre_inscription_edit") +@scodoc7func(context) +@permission_required(Permission.ScoImplement) +def do_formsemestre_inscription_edit(context, args=None, formsemestre_id=None): + "edit a formsemestre_inscription" + cnx = context.GetDBConnexion() + context._formsemestre_inscriptionEditor.edit(cnx, args) + context._inval_cache( + formsemestre_id=formsemestre_id + ) # > modif inscription semestre (demission ?) + + +# Cache inscriptions semestres +def get_formsemestre_inscription_cache(context, format=None): + u = context.GetDBConnexionString() + if CACHE_formsemestre_inscription.has_key(u): + return CACHE_formsemestre_inscription[u] + else: + log("get_formsemestre_inscription_cache: new simpleCache") + CACHE_formsemestre_inscription[u] = sco_cache.simpleCache() + return CACHE_formsemestre_inscription[u] + + +@bp.route("/formsemestre_desinscription") +@scodoc7func(context) +@permission_required(Permission.ScoImplement) +def formsemestre_desinscription( + context, etudid, formsemestre_id, REQUEST=None, dialog_confirmed=False +): + """desinscrit l'etudiant de ce semestre (et donc de tous les modules). + A n'utiliser qu'en cas d'erreur de saisie. + S'il s'agit d'un semestre extérieur et qu'il n'y a plus d'inscrit, + le semestre sera supprimé. + """ + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + # -- check lock + if sem["etat"] != "1": + raise ScoValueError("desinscription impossible: semestre verrouille") + + # -- Si décisions de jury, désinscription interdite + nt = context._getNotesCache().get_NotesTable(context, formsemestre_id) + if nt.etud_has_decision(etudid): + raise ScoValueError( + """Désinscription impossible: l'étudiant a une décision de jury + (la supprimer avant si nécessaire: + + supprimer décision jury + ) + """ + % (etudid, formsemestre_id) + ) + if not dialog_confirmed: + etud = context.getEtudInfo(etudid=etudid, filled=1)[0] + if sem["modalite"] != "EXT": + msg_ext = """ +

    %s sera désinscrit de tous les modules du semestre %s (%s - %s).

    +

    Cette opération ne doit être utilisée que pour corriger une erreur ! + Un étudiant réellement inscrit doit le rester, le faire éventuellement démissionner. +

    + """ % ( + etud["nomprenom"], + sem["titre_num"], + sem["date_debut"], + sem["date_fin"], + ) + else: # semestre extérieur + msg_ext = """ +

    %s sera désinscrit du semestre extérieur %s (%s - %s).

    + """ % ( + etud["nomprenom"], + sem["titre_num"], + sem["date_debut"], + sem["date_fin"], + ) + inscrits = context.do_formsemestre_inscription_list( + args={"formsemestre_id": formsemestre_id} + ) + nbinscrits = len(inscrits) + if nbinscrits <= 1: + msg_ext = """

    Attention: le semestre extérieur sera supprimé + car il n'a pas d'autre étudiant inscrit. +

    + """ + return context.confirmDialog( + """

    Confirmer la demande de desinscription ?

    """ + msg_ext, + dest_url="", + REQUEST=REQUEST, + cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id, + parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, + ) + + context.do_formsemestre_desinscription(etudid, formsemestre_id, REQUEST=REQUEST) + + return ( + context.sco_header(REQUEST) + + '

    Etudiant désinscrit !

    retour à la fiche' + % (context.ScoURL(), etudid) + + context.sco_footer(REQUEST) + ) + + +@bp.route("/do_formsemestre_desinscription") +@scodoc7func(context) +@permission_required(Permission.ScoImplement) +def do_formsemestre_desinscription(context, etudid, formsemestre_id, REQUEST=None): + """Désinscription d'un étudiant. + Si semestre extérieur et dernier inscrit, suppression de ce semestre. + """ + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + # -- check lock + if sem["etat"] != "1": + raise ScoValueError("desinscription impossible: semestre verrouille") + + # -- Si decisions de jury, desinscription interdite + nt = context._getNotesCache().get_NotesTable(context, formsemestre_id) + if nt.etud_has_decision(etudid): + raise ScoValueError( + "desinscription impossible: l'étudiant a une décision de jury (la supprimer avant si nécessaire)" + ) + + insem = context.do_formsemestre_inscription_list( + args={"formsemestre_id": formsemestre_id, "etudid": etudid} + ) + if not insem: + raise ScoValueError("%s n'est pas inscrit au semestre !" % etudid) + insem = insem[0] + # -- desinscription de tous les modules + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + cursor.execute( + "select moduleimpl_inscription_id from notes_moduleimpl_inscription Im, notes_moduleimpl M where Im.etudid=%(etudid)s and Im.moduleimpl_id = M.moduleimpl_id and M.formsemestre_id = %(formsemestre_id)s", + {"etudid": etudid, "formsemestre_id": formsemestre_id}, + ) + res = cursor.fetchall() + moduleimpl_inscription_ids = [x[0] for x in res] + for moduleimpl_inscription_id in moduleimpl_inscription_ids: + sco_moduleimpl.do_moduleimpl_inscription_delete( + context, moduleimpl_inscription_id, formsemestre_id=formsemestre_id + ) + # -- desincription du semestre + context.do_formsemestre_inscription_delete( + insem["formsemestre_inscription_id"], formsemestre_id=formsemestre_id + ) + # --- Semestre extérieur + if sem["modalite"] == "EXT": + inscrits = context.do_formsemestre_inscription_list( + args={"formsemestre_id": formsemestre_id} + ) + nbinscrits = len(inscrits) + if nbinscrits == 0: + log( + "do_formsemestre_desinscription: suppression du semestre extérieur %s" + % formsemestre_id + ) + sco_formsemestre_edit.do_formsemestre_delete( + context, formsemestre_id, REQUEST=REQUEST + ) + + if REQUEST: + logdb( + REQUEST, + cnx, + method="formsemestre_desinscription", + etudid=etudid, + msg="desinscription semestre %s" % formsemestre_id, + commit=False, + ) + + +@bp.route("/etud_desinscrit_ue") +@scodoc7func(context) +@permission_required(Permission.ScoEtudInscrit) +def etud_desinscrit_ue(context, etudid, formsemestre_id, ue_id, REQUEST=None): + """Desinscrit l'etudiant de tous les modules de cette UE dans ce semestre.""" + sco_moduleimpl_inscriptions.do_etud_desinscrit_ue( + context, etudid, formsemestre_id, ue_id, REQUEST=REQUEST + ) + return REQUEST.RESPONSE.redirect( + context.ScoURL() + + "/Notes/moduleimpl_inscriptions_stats?formsemestre_id=" + + formsemestre_id + ) + + +@bp.route("/etud_inscrit_ue") +@scodoc7func(context) +@permission_required(Permission.ScoEtudInscrit) +def etud_inscrit_ue(context, etudid, formsemestre_id, ue_id, REQUEST=None): + """Inscrit l'etudiant de tous les modules de cette UE dans ce semestre.""" + sco_moduleimpl_inscriptions.do_etud_inscrit_ue( + context, etudid, formsemestre_id, ue_id, REQUEST=REQUEST + ) + return REQUEST.RESPONSE.redirect( + context.ScoURL() + + "/Notes/moduleimpl_inscriptions_stats?formsemestre_id=" + + formsemestre_id + ) + + +# --- Inscriptions +sco_publish( + "/formsemestre_inscription_with_modules_form", + sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_form, + Permission.ScoEtudInscrit, +) +sco_publish( + "/formsemestre_inscription_with_modules_etud", + sco_formsemestre_inscriptions.formsemestre_inscription_with_modules_etud, + Permission.ScoEtudInscrit, +) +sco_publish( + "/formsemestre_inscription_with_modules", + sco_formsemestre_inscriptions.formsemestre_inscription_with_modules, + Permission.ScoEtudInscrit, +) +sco_publish( + "/formsemestre_inscription_option", + sco_formsemestre_inscriptions.formsemestre_inscription_option, + Permission.ScoEtudInscrit, +) +sco_publish( + "/do_moduleimpl_incription_options", + sco_formsemestre_inscriptions.do_moduleimpl_incription_options, + Permission.ScoEtudInscrit, +) +sco_publish( + "/formsemestre_inscrits_ailleurs", + sco_formsemestre_inscriptions.formsemestre_inscrits_ailleurs, + Permission.ScoView, +) +sco_publish( + "/moduleimpl_inscriptions_edit", + sco_moduleimpl_inscriptions.moduleimpl_inscriptions_edit, + Permission.ScoEtudInscrit, +) +sco_publish( + "/moduleimpl_inscriptions_stats", + sco_moduleimpl_inscriptions.moduleimpl_inscriptions_stats, + Permission.ScoView, +) + + +# --- Evaluations +_evaluationEditor = ndb.EditableTable( + "notes_evaluation", + "evaluation_id", + ( + "evaluation_id", + "moduleimpl_id", + "jour", + "heure_debut", + "heure_fin", + "description", + "note_max", + "coefficient", + "visibulletin", + "publish_incomplete", + "evaluation_type", + "numero", + ), + sortkey="numero desc, jour desc, heure_debut desc", # plus recente d'abord + output_formators={ + "jour": ndb.DateISOtoDMY, + "visibulletin": str, + "publish_incomplete": str, + "numero": ndb.int_null_is_zero, + }, + input_formators={ + "jour": ndb.DateDMYtoISO, + "heure_debut": ndb.TimetoISO8601, # converti par do_evaluation_list + "heure_fin": ndb.TimetoISO8601, # converti par do_evaluation_list + "visibulletin": int, + "publish_incomplete": int, + }, +) + + +def _evaluation_check_write_access(context, REQUEST, moduleimpl_id=None): + """Vérifie que l'on a le droit de modifier, créer ou détruire une + évaluation dans ce module. + Sinon, lance une exception. + (nb: n'implique pas le droit de saisir ou modifier des notes) + """ + # acces pour resp. moduleimpl et resp. form semestre (dir etud) + if moduleimpl_id is None: + raise ValueError("no moduleimpl specified") # bug + authuser = REQUEST.AUTHENTICATED_USER + uid = str(authuser) + M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)[0] + sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"]) + + if ( + (not authuser.has_permission(ScoEditAllEvals, context)) + and uid != M["responsable_id"] + and uid not in sem["responsables"] + ): + if sem["ens_can_edit_eval"]: + for ens in M["ens"]: + if ens["ens_id"] == uid: + return # ok + raise AccessDenied("Modification évaluation impossible pour %s" % (uid,)) + + +@bp.route("/do_evaluation_create") +@scodoc7func(context) +@permission_required(Permission.ScoEnsView) +def do_evaluation_create( + context, + moduleimpl_id=None, + jour=None, + heure_debut=None, + heure_fin=None, + description=None, + note_max=None, + coefficient=None, + visibulletin=None, + publish_incomplete=None, + evaluation_type=None, + numero=None, + REQUEST=None, + **kw +): + """Create an evaluation""" + args = locals() + log("do_evaluation_create: args=" + str(args)) + context._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id) + context._check_evaluation_args(args) + # Check numeros + sco_evaluations.module_evaluation_renumber( + context, moduleimpl_id, REQUEST=REQUEST, only_if_unumbered=True + ) + if not "numero" in args or args["numero"] is None: + n = None + # determine le numero avec la date + # Liste des eval existantes triees par date, la plus ancienne en tete + ModEvals = context.do_evaluation_list( + args={"moduleimpl_id": moduleimpl_id}, + sortkey="jour asc, heure_debut asc", + ) + if args["jour"]: + next_eval = None + t = ( + ndb.DateDMYtoISO(args["jour"]), + ndb.TimetoISO8601(args["heure_debut"]), + ) + for e in ModEvals: + if ( + ndb.DateDMYtoISO(e["jour"]), + ndb.TimetoISO8601(e["heure_debut"]), + ) > t: + next_eval = e + break + if next_eval: + n = sco_evaluations.module_evaluation_insert_before( + context, ModEvals, next_eval, REQUEST + ) + else: + n = None # a placer en fin + if n is None: # pas de date ou en fin: + if ModEvals: + log(pprint.pformat(ModEvals[-1])) + n = ModEvals[-1]["numero"] + 1 + else: + n = 0 # the only one + # log("creating with numero n=%d" % n) + args["numero"] = n + + # + cnx = context.GetDBConnexion() + r = context._evaluationEditor.create(cnx, args) + + # news + M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)[0] + mod = context.do_module_list(args={"module_id": M["module_id"]})[0] + mod["moduleimpl_id"] = M["moduleimpl_id"] + mod["url"] = "Notes/moduleimpl_status?moduleimpl_id=%(moduleimpl_id)s" % mod + sco_news.add( + context, + REQUEST, + typ=NEWS_NOTE, + object=moduleimpl_id, + text='Création d\'une évaluation dans %(titre)s' % mod, + url=mod["url"], + ) + + return r + + +def _check_evaluation_args(context, args): + "Check coefficient, dates and duration, raises exception if invalid" + moduleimpl_id = args["moduleimpl_id"] + # check bareme + note_max = args.get("note_max", None) + if note_max is None: + raise ScoValueError("missing note_max") + try: + note_max = float(note_max) + except ValueError: + raise ScoValueError("Invalid note_max value") + if note_max < 0: + raise ScoValueError("Invalid note_max value (must be positive or null)") + # check coefficient + coef = args.get("coefficient", None) + if coef is None: + raise ScoValueError("missing coefficient") + try: + coef = float(coef) + except ValueError: + raise ScoValueError("Invalid coefficient value") + if coef < 0: + raise ScoValueError("Invalid coefficient value (must be positive or null)") + # check date + jour = args.get("jour", None) + args["jour"] = jour + if jour: + M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)[0] + sem = sco_formsemestre.get_formsemestre(context, M["formsemestre_id"]) + d, m, y = [int(x) for x in sem["date_debut"].split("/")] + date_debut = datetime.date(y, m, d) + d, m, y = [int(x) for x in sem["date_fin"].split("/")] + date_fin = datetime.date(y, m, d) + # passe par ndb.DateDMYtoISO pour avoir date pivot + y, m, d = [int(x) for x in ndb.DateDMYtoISO(jour).split("-")] + jour = datetime.date(y, m, d) + if (jour > date_fin) or (jour < date_debut): + raise ScoValueError( + "La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !" + % (d, m, y) + ) + heure_debut = args.get("heure_debut", None) + args["heure_debut"] = heure_debut + heure_fin = args.get("heure_fin", None) + args["heure_fin"] = heure_fin + if jour and ((not heure_debut) or (not heure_fin)): + raise ScoValueError("Les heures doivent être précisées") + d = ndb.TimeDuration(heure_debut, heure_fin) + if d and ((d < 0) or (d > 60 * 12)): + raise ScoValueError("Heures de l'évaluation incohérentes !") + + +@bp.route("/evaluation_delete") +@scodoc7func(context) +@permission_required(Permission.ScoEnsView) +def evaluation_delete(context, REQUEST, evaluation_id): + """Form delete evaluation""" + El = context.do_evaluation_list(args={"evaluation_id": evaluation_id}) + if not El: + raise ValueError("Evalution inexistante ! (%s)" % evaluation_id) + E = El[0] + M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=E["moduleimpl_id"])[0] + Mod = context.do_module_list(args={"module_id": M["module_id"]})[0] + tit = "Suppression de l'évaluation %(description)s (%(jour)s)" % E + etat = sco_evaluations.do_evaluation_etat(context, evaluation_id) + H = [ + context.html_sem_header(REQUEST, tit, with_h2=False), + """

    Module %(code)s %(titre)s

    """ % Mod, + """

    %s

    """ % tit, + """

    Opération irréversible. Si vous supprimez l'évaluation, vous ne pourrez pas retrouver les notes associées.

    """, + ] + warning = False + if etat["nb_notes_total"]: + warning = True + nb_desinscrits = etat["nb_notes_total"] - etat["nb_notes"] + H.append( + """
    Il y a %s notes""" % etat["nb_notes_total"] + ) + if nb_desinscrits: + H.append( + """ (dont %s d'étudiants qui ne sont plus inscrits)""" % nb_desinscrits + ) + H.append(""" dans l'évaluation""") + if etat["nb_notes"] == 0: + H.append( + """

    Vous pouvez quand même supprimer l'évaluation, les notes des étudiants désincrits seront effacées.

    """ + ) + + if etat["nb_notes"]: + H.append( + """

    Suppression impossible (effacer les notes d'abord)

    retour au tableau de bord du module

    """ + % E["moduleimpl_id"] + ) + return "\n".join(H) + context.sco_footer(REQUEST) + if warning: + H.append("""
""") + + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + (("evaluation_id", {"input_type": "hidden"}),), + initvalues=E, + submitlabel="Confirmer la suppression", + cancelbutton="Annuler", + ) + if tf[0] == 0: + return "\n".join(H) + tf[1] + context.sco_footer(REQUEST) + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect( + context.ScoURL() + + "/Notes/moduleimpl_status?moduleimpl_id=" + + E["moduleimpl_id"] + ) + else: + sco_evaluations.do_evaluation_delete(context, REQUEST, E["evaluation_id"]) + return ( + "\n".join(H) + + """

OK, évaluation supprimée.

+

Continuer

""" + % ( + context.ScoURL() + + "/Notes/moduleimpl_status?moduleimpl_id=" + + E["moduleimpl_id"] + ) + + context.sco_footer(REQUEST) + ) + + +@bp.route("/do_evaluation_list") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def do_evaluation_list(context, args, sortkey=None): + """List evaluations, sorted by numero (or most recent date first). + + Ajoute les champs: + 'duree' : '2h30' + 'matin' : 1 (commence avant 12:00) ou 0 + 'apresmidi' : 1 (termine après 12:00) ou 0 + 'descrheure' : ' de 15h00 à 16h30' + """ + cnx = context.GetDBConnexion() + evals = context._evaluationEditor.list(cnx, args, sortkey=sortkey) + # calcule duree (chaine de car.) de chaque evaluation et ajoute jouriso, matin, apresmidi + for e in evals: + heure_debut_dt = e["heure_debut"] or datetime.time( + 8, 00 + ) # au cas ou pas d'heure (note externe?) + heure_fin_dt = e["heure_fin"] or datetime.time(8, 00) + e["heure_debut"] = ndb.TimefromISO8601(e["heure_debut"]) + e["heure_fin"] = ndb.TimefromISO8601(e["heure_fin"]) + e["jouriso"] = ndb.DateDMYtoISO(e["jour"]) + heure_debut, heure_fin = e["heure_debut"], e["heure_fin"] + d = ndb.TimeDuration(heure_debut, heure_fin) + if d is not None: + m = d % 60 + e["duree"] = "%dh" % (d / 60) + if m != 0: + e["duree"] += "%02d" % m + else: + e["duree"] = "" + if heure_debut and (not heure_fin or heure_fin == heure_debut): + e["descrheure"] = " à " + heure_debut + elif heure_debut and heure_fin: + e["descrheure"] = " de %s à %s" % (heure_debut, heure_fin) + else: + e["descrheure"] = "" + # matin, apresmidi: utile pour se referer aux absences: + if heure_debut_dt < datetime.time(12, 00): + e["matin"] = 1 + else: + e["matin"] = 0 + if heure_fin_dt > datetime.time(12, 00): + e["apresmidi"] = 1 + else: + e["apresmidi"] = 0 + + return evals + + +@bp.route("/do_evaluation_list_in_formsemestre") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def do_evaluation_list_in_formsemestre(context, formsemestre_id): + "list evaluations in this formsemestre" + mods = sco_moduleimpl.do_moduleimpl_list(context, formsemestre_id=formsemestre_id) + evals = [] + for mod in mods: + evals += context.do_evaluation_list( + args={"moduleimpl_id": mod["moduleimpl_id"]} + ) + return evals + + +@bp.route("/do_evaluation_edit") +@scodoc7func(context) +@permission_required(Permission.ScoEnsView) +def do_evaluation_edit(context, REQUEST, args): + "edit a evaluation" + evaluation_id = args["evaluation_id"] + the_evals = context.do_evaluation_list({"evaluation_id": evaluation_id}) + if not the_evals: + raise ValueError("evaluation inexistante !") + moduleimpl_id = the_evals[0]["moduleimpl_id"] + args["moduleimpl_id"] = moduleimpl_id + context._check_evaluation_args(args) + context._evaluation_check_write_access(REQUEST, moduleimpl_id=moduleimpl_id) + cnx = context.GetDBConnexion() + context._evaluationEditor.edit(cnx, args) + # inval cache pour ce semestre + M = sco_moduleimpl.do_moduleimpl_list(context, moduleimpl_id=moduleimpl_id)[0] + context._inval_cache( + formsemestre_id=M["formsemestre_id"] + ) # > evaluation_edit (coef, ...) + + +@bp.route("/evaluation_edit") +@scodoc7func(context) +@permission_required(Permission.ScoEnsView) +def evaluation_edit(context, evaluation_id, REQUEST): + "form edit evaluation" + return sco_evaluations.evaluation_create_form( + context, evaluation_id=evaluation_id, REQUEST=REQUEST, edit=True + ) + + +@bp.route("/evaluation_create") +@scodoc7func(context) +@permission_required(Permission.ScoEnsView) +def evaluation_create(context, moduleimpl_id, REQUEST): + "form create evaluation" + return sco_evaluations.evaluation_create_form( + context, moduleimpl_id=moduleimpl_id, REQUEST=REQUEST, edit=False + ) + + +@bp.route("/evaluation_listenotes") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def evaluation_listenotes(context, REQUEST=None): + """Affichage des notes d'une évaluation""" + if REQUEST.form.get("format", "html") == "html": + H = context.sco_header( + REQUEST, + cssstyles=["css/verticalhisto.css"], + javascripts=["js/etud_info.js"], + init_qtip=True, + ) + F = context.sco_footer(REQUEST) + else: + H, F = "", "" + B = context.do_evaluation_listenotes(REQUEST) + return H + B + F + + +sco_publish( + "/do_evaluation_listenotes", + sco_liste_notes.do_evaluation_listenotes, + Permission.ScoView, +) +sco_publish( + "/evaluation_list_operations", + sco_undo_notes.evaluation_list_operations, + Permission.ScoView, +) +sco_publish( + "/evaluation_check_absences_html", + sco_liste_notes.evaluation_check_absences_html, + Permission.ScoView, +) +sco_publish( + "/formsemestre_check_absences_html", + sco_liste_notes.formsemestre_check_absences_html, + Permission.ScoView, +) + +# --- Placement des étudiants pour l'évaluation +sco_publish( + "/placement_eval_selectetuds", + sco_placement.placement_eval_selectetuds, + Permission.ScoEnsView, +) +sco_publish("/do_placement", sco_placement.do_placement, Permission.ScoEnsView) + +# --- Saisie des notes +sco_publish( + "/saisie_notes_tableur", + sco_saisie_notes.saisie_notes_tableur, + Permission.ScoEnsView, +) +sco_publish( + "/feuille_saisie_notes", + sco_saisie_notes.feuille_saisie_notes, + Permission.ScoEnsView, +) +sco_publish("/saisie_notes", sco_saisie_notes.saisie_notes, Permission.ScoEnsView) +sco_publish("/save_note", sco_saisie_notes.save_note, Permission.ScoEnsView) +sco_publish( + "/do_evaluation_set_missing", + sco_saisie_notes.do_evaluation_set_missing, + Permission.ScoEnsView, +) +sco_publish( + "/evaluation_suppress_alln", + sco_saisie_notes.evaluation_suppress_alln, + Permission.ScoView, +) + + +# cache notes evaluations +def get_evaluations_cache(context): + u = context.GetDBConnexionString() + if CACHE_evaluations.has_key(u): + return CACHE_evaluations[u] + else: + log("get_evaluations_cache: new simpleCache") + CACHE_evaluations[u] = sco_cache.simpleCache() + return CACHE_evaluations[u] + + +def _notes_getall( + context, evaluation_id, table="notes_notes", filter_suppressed=True, by_uid=None +): + """get tt les notes pour une evaluation: { etudid : { 'value' : value, 'date' : date ... }} + Attention: inclue aussi les notes des étudiants qui ne sont plus inscrits au module. + """ + # log('_notes_getall( e=%s fs=%s )' % (evaluation_id, filter_suppressed)) + do_cache = ( + filter_suppressed and table == "notes_notes" and (by_uid is None) + ) # pas de cache pour (rares) appels via undo_notes ou specifiant un enseignant + if do_cache: + cache = context.get_evaluations_cache() + r = cache.get(evaluation_id) + if r != None: + return r + cnx = context.GetDBConnexion() + cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) + cond = " where evaluation_id=%(evaluation_id)s" + if by_uid: + cond += " and uid=%(by_uid)s" + + cursor.execute( + "select * from " + table + cond, + {"evaluation_id": evaluation_id, "by_uid": by_uid}, + ) + res = cursor.dictfetchall() + d = {} + if filter_suppressed: + for x in res: + if x["value"] != scu.NOTES_SUPPRESS: + d[x["etudid"]] = x + else: + for x in res: + d[x["etudid"]] = x + if do_cache: + cache.set(evaluation_id, d) + return d + + +# --- Bulletins +@bp.route("/formsemestre_bulletins_pdf") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_bulletins_pdf( + context, formsemestre_id, REQUEST, version="selectedevals" +): + "Publie les bulletins dans un classeur PDF" + pdfdoc, filename = sco_bulletins_pdf.get_formsemestre_bulletins_pdf( + context, formsemestre_id, REQUEST, version=version + ) + return scu.sendPDFFile(REQUEST, pdfdoc, filename) + + +@bp.route("/etud_bulletins_pdf") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def etud_bulletins_pdf(context, etudid, REQUEST, version="selectedevals"): + "Publie tous les bulletins d'un etudiants dans un classeur PDF" + pdfdoc, filename = sco_bulletins_pdf.get_etud_bulletins_pdf( + context, etudid, REQUEST, version=version + ) + return scu.sendPDFFile(REQUEST, pdfdoc, filename) + + +sco_publish( + "/formsemestre_bulletins_pdf_choice", + sco_bulletins.formsemestre_bulletins_pdf_choice, + Permission.ScoView, +) +sco_publish( + "/formsemestre_bulletins_mailetuds_choice", + sco_bulletins.formsemestre_bulletins_mailetuds_choice, + Permission.ScoView, +) + + +@bp.route("/formsemestre_bulletins_mailetuds") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_bulletins_mailetuds( + context, + formsemestre_id, + REQUEST, + version="long", + dialog_confirmed=False, + prefer_mail_perso=0, +): + "envoi a chaque etudiant (inscrit et ayant un mail) son bulletin" + prefer_mail_perso = int(prefer_mail_perso) + nt = context._getNotesCache().get_NotesTable( + context, formsemestre_id + ) # > get_etudids + etudids = nt.get_etudids() + # + if not sco_bulletins.can_send_bulletin_by_mail(context, formsemestre_id, REQUEST): + raise AccessDenied("vous n'avez pas le droit d'envoyer les bulletins") + # Confirmation dialog + if not dialog_confirmed: + return context.confirmDialog( + "

Envoyer les %d bulletins par e-mail aux étudiants ?" % len(etudids), + dest_url="", + REQUEST=REQUEST, + cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id, + parameters={ + "version": version, + "formsemestre_id": formsemestre_id, + "prefer_mail_perso": prefer_mail_perso, + }, + ) + + # Make each bulletin + nb_send = 0 + for etudid in etudids: + h, _ = sco_bulletins.do_formsemestre_bulletinetud( + context, + formsemestre_id, + etudid, + version=version, + prefer_mail_perso=prefer_mail_perso, + format="pdfmail", + nohtml=True, + REQUEST=REQUEST, + ) + if h: + nb_send += 1 + # + return ( + context.sco_header(REQUEST) + + '

%d bulletins sur %d envoyés par mail !

continuer

' + % (nb_send, len(etudids), formsemestre_id) + + context.sco_footer(REQUEST) + ) + + +sco_publish( + "/external_ue_create_form", + sco_ue_external.external_ue_create_form, + Permission.ScoView, +) + + +@bp.route("/appreciation_add_form") +@scodoc7func(context) +@permission_required(Permission.ScoEnsView) +def appreciation_add_form( + context, + etudid=None, + formsemestre_id=None, + id=None, # si id, edit + suppress=False, # si true, supress id + REQUEST=None, +): + "form ajout ou edition d'une appreciation" + cnx = context.GetDBConnexion() + authuser = REQUEST.AUTHENTICATED_USER + if id: # edit mode + apps = scolars.appreciations_list(cnx, args={"id": id}) + if not apps: + raise ScoValueError("id d'appreciation invalide !") + app = apps[0] + formsemestre_id = app["formsemestre_id"] + etudid = app["etudid"] + if REQUEST.form.has_key("edit"): + edit = int(REQUEST.form["edit"]) + elif id: + edit = 1 + else: + edit = 0 + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + # check custom access permission + can_edit_app = (str(authuser) in sem["responsables"]) or ( + authuser.has_permission(ScoEtudInscrit, context) + ) + if not can_edit_app: + raise AccessDenied("vous n'avez pas le droit d'ajouter une appreciation") + # + bull_url = "formsemestre_bulletinetud?formsemestre_id=%s&etudid=%s" % ( + formsemestre_id, + etudid, + ) + if suppress: + scolars.appreciations_delete(cnx, id) + logdb(REQUEST, cnx, method="appreciation_suppress", etudid=etudid, msg="") + return REQUEST.RESPONSE.redirect(bull_url) + # + etud = context.getEtudInfo(etudid=etudid, filled=1)[0] + if id: + a = "Edition" + else: + a = "Ajout" + H = [ + context.sco_header(REQUEST) + + "

%s d'une appréciation sur %s

" % (a, etud["nomprenom"]) + ] + F = context.sco_footer(REQUEST) + descr = [ + ("edit", {"input_type": "hidden", "default": edit}), + ("etudid", {"input_type": "hidden"}), + ("formsemestre_id", {"input_type": "hidden"}), + ("id", {"input_type": "hidden"}), + ("comment", {"title": "", "input_type": "textarea", "rows": 4, "cols": 60}), + ] + if id: + initvalues = { + "etudid": etudid, + "formsemestre_id": formsemestre_id, + "comment": app["comment"], + } + else: + initvalues = {} + tf = TrivialFormulator( + REQUEST.URL0, + REQUEST.form, + descr, + initvalues=initvalues, + cancelbutton="Annuler", + submitlabel="Ajouter appréciation", + ) + if tf[0] == 0: + return "\n".join(H) + "\n" + tf[1] + F + elif tf[0] == -1: + return REQUEST.RESPONSE.redirect(bull_url) + else: + args = { + "etudid": etudid, + "formsemestre_id": formsemestre_id, + "author": str(authuser), + "comment": tf[2]["comment"], + "zope_authenticated_user": str(authuser), + "zope_remote_addr": REQUEST.REMOTE_ADDR, + } + if edit: + args["id"] = id + scolars.appreciations_edit(cnx, args) + else: # nouvelle + scolars.appreciations_create(cnx, args, has_uniq_values=False) + # log + logdb( + REQUEST, + cnx, + method="appreciation_add", + etudid=etudid, + msg=tf[2]["comment"], + ) + # ennuyeux mais necessaire (pour le PDF seulement) + context._inval_cache( + pdfonly=True, formsemestre_id=formsemestre_id + ) # > appreciation_add + return REQUEST.RESPONSE.redirect(bull_url) + + +def _can_edit_pv(context, REQUEST, formsemestre_id): + "Vrai si utilisateur peut editer un PV de jury de ce semestre" + + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + if context._is_chef_or_diretud(REQUEST, sem): + return True + # Autorise les secrétariats, repérés via la permission ScoEtudChangeAdr + # (ceci nous évite d'ajouter une permission Zope aux installations existantes) + authuser = REQUEST.AUTHENTICATED_USER + return authuser.has_permission(ScoEtudChangeAdr, context) + + +# --- FORMULAIRE POUR VALIDATION DES UE ET SEMESTRES +def _can_validate_sem(context, REQUEST, formsemestre_id): + "Vrai si utilisateur peut saisir decision de jury dans ce semestre" + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + if sem["etat"] != "1": + return False # semestre verrouillé + + return context._is_chef_or_diretud(REQUEST, sem) + + +def _is_chef_or_diretud(context, REQUEST, sem): + "Vrai si utilisateur est admin, chef dept ou responsable du semestre" + authuser = REQUEST.AUTHENTICATED_USER + if authuser.has_permission(ScoImplement, context): + return True # admin, chef dept + uid = str(authuser) + if uid in sem["responsables"]: + return True + + return False + + +@bp.route("/formsemestre_validation_etud_form") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_validation_etud_form( + context, + formsemestre_id, + etudid=None, + etud_index=None, + check=0, + desturl="", + sortcol=None, + REQUEST=None, +): + "Formulaire choix jury pour un étudiant" + readonly = not context._can_validate_sem(REQUEST, formsemestre_id) + return sco_formsemestre_validation.formsemestre_validation_etud_form( + context, + formsemestre_id, + etudid=etudid, + etud_index=etud_index, + check=check, + readonly=readonly, + desturl=desturl, + sortcol=sortcol, + REQUEST=REQUEST, + ) + + +@bp.route("/formsemestre_validation_etud") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_validation_etud( + context, + formsemestre_id, + etudid=None, + codechoice=None, + desturl="", + sortcol=None, + REQUEST=None, +): + "Enregistre choix jury pour un étudiant" + if not context._can_validate_sem(REQUEST, formsemestre_id): + return context.confirmDialog( + message="

Opération non autorisée pour %s" + % REQUEST.AUTHENTICATED_USER, + dest_url=context.ScoURL(), + REQUEST=REQUEST, + ) + + return sco_formsemestre_validation.formsemestre_validation_etud( + context, + formsemestre_id, + etudid=etudid, + codechoice=codechoice, + desturl=desturl, + sortcol=sortcol, + REQUEST=REQUEST, + ) + + +@bp.route("/formsemestre_validation_etud_manu") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_validation_etud_manu( + context, + formsemestre_id, + etudid=None, + code_etat="", + new_code_prev="", + devenir="", + assidu=False, + desturl="", + sortcol=None, + REQUEST=None, +): + "Enregistre choix jury pour un étudiant" + if not context._can_validate_sem(REQUEST, formsemestre_id): + return context.confirmDialog( + message="

Opération non autorisée pour %s" + % REQUEST.AUTHENTICATED_USER, + dest_url=context.ScoURL(), + REQUEST=REQUEST, + ) + + return sco_formsemestre_validation.formsemestre_validation_etud_manu( + context, + formsemestre_id, + etudid=etudid, + code_etat=code_etat, + new_code_prev=new_code_prev, + devenir=devenir, + assidu=assidu, + desturl=desturl, + sortcol=sortcol, + REQUEST=REQUEST, + ) + + +@bp.route("/formsemestre_validate_previous_ue") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_validate_previous_ue( + context, formsemestre_id, etudid=None, REQUEST=None +): + "Form. saisie UE validée hors ScoDoc " + if not context._can_validate_sem(REQUEST, formsemestre_id): + return context.confirmDialog( + message="

Opération non autorisée pour %s" + % REQUEST.AUTHENTICATED_USER, + dest_url=context.ScoURL(), + REQUEST=REQUEST, + ) + return sco_formsemestre_validation.formsemestre_validate_previous_ue( + context, formsemestre_id, etudid, REQUEST=REQUEST + ) + + +sco_publish( + "/formsemestre_ext_create_form", + sco_formsemestre_exterieurs.formsemestre_ext_create_form, + Permission.ScoView, +) + + +@bp.route("/formsemestre_ext_edit_ue_validations") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_ext_edit_ue_validations( + context, formsemestre_id, etudid=None, REQUEST=None +): + "Form. edition UE semestre extérieur" + if not context._can_validate_sem(REQUEST, formsemestre_id): + return context.confirmDialog( + message="

Opération non autorisée pour %s" + % REQUEST.AUTHENTICATED_USER, + dest_url=context.ScoURL(), + REQUEST=REQUEST, + ) + return sco_formsemestre_exterieurs.formsemestre_ext_edit_ue_validations( + context, formsemestre_id, etudid, REQUEST=REQUEST + ) + + +sco_publish( + "/get_etud_ue_cap_html", + sco_formsemestre_validation.get_etud_ue_cap_html, + Permission.ScoView, +) + + +@bp.route("/etud_ue_suppress_validation") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def etud_ue_suppress_validation(context, etudid, formsemestre_id, ue_id, REQUEST=None): + """Suppress a validation (ue_id, etudid) and redirect to formsemestre""" + if not context._can_validate_sem(REQUEST, formsemestre_id): + return context.confirmDialog( + message="

Opération non autorisée pour %s" + % REQUEST.AUTHENTICATED_USER, + dest_url=context.ScoURL(), + REQUEST=REQUEST, + ) + return sco_formsemestre_validation.etud_ue_suppress_validation( + context, etudid, formsemestre_id, ue_id, REQUEST=REQUEST + ) + + +@bp.route("/formsemestre_validation_auto") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_validation_auto(context, formsemestre_id, REQUEST): + "Formulaire saisie automatisee des decisions d'un semestre" + if not context._can_validate_sem(REQUEST, formsemestre_id): + return context.confirmDialog( + message="

Opération non autorisée pour %s" + % REQUEST.AUTHENTICATED_USER, + dest_url=context.ScoURL(), + REQUEST=REQUEST, + ) + + return sco_formsemestre_validation.formsemestre_validation_auto( + context, formsemestre_id, REQUEST + ) + + +@bp.route("/do_formsemestre_validation_auto") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def do_formsemestre_validation_auto(context, formsemestre_id, REQUEST): + "Formulaire saisie automatisee des decisions d'un semestre" + if not context._can_validate_sem(REQUEST, formsemestre_id): + return context.confirmDialog( + message="

Opération non autorisée pour %s" + % REQUEST.AUTHENTICATED_USER, + dest_url=context.ScoURL(), + REQUEST=REQUEST, + ) + + return sco_formsemestre_validation.do_formsemestre_validation_auto( + context, formsemestre_id, REQUEST + ) + + +@bp.route("/formsemestre_fix_validation_ues") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_fix_validation_ues(context, formsemestre_id, REQUEST=None): + "Verif/reparation codes UE" + if not context._can_validate_sem(REQUEST, formsemestre_id): + return context.confirmDialog( + message="

Opération non autorisée pour %s" + % REQUEST.AUTHENTICATED_USER, + dest_url=context.ScoURL(), + REQUEST=REQUEST, + ) + + return sco_formsemestre_validation.formsemestre_fix_validation_ues( + context, formsemestre_id, REQUEST + ) + + +@bp.route("/formsemestre_validation_suppress_etud") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def formsemestre_validation_suppress_etud( + context, formsemestre_id, etudid, REQUEST=None, dialog_confirmed=False +): + """Suppression des decisions de jury pour un etudiant.""" + if not context._can_validate_sem(REQUEST, formsemestre_id): + return context.confirmDialog( + message="

Opération non autorisée pour %s" + % REQUEST.AUTHENTICATED_USER, + dest_url=context.ScoURL(), + REQUEST=REQUEST, + ) + if not dialog_confirmed: + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + etud = context.getEtudInfo(etudid=etudid, filled=1)[0] + nt = context._getNotesCache().get_NotesTable( + context, formsemestre_id + ) # > get_etud_decision_sem + decision_jury = nt.get_etud_decision_sem(etudid) + if decision_jury: + existing = ( + "

Décision existante: %(code)s du %(event_date)s

" % decision_jury + ) + else: + existing = "" + return context.confirmDialog( + """

Confirmer la suppression des décisions du semestre %s (%s - %s) pour %s ?

%s +

Cette opération est irréversible. +

+ """ + % ( + sem["titre_num"], + sem["date_debut"], + sem["date_fin"], + etud["nomprenom"], + existing, + ), + OK="Supprimer", + dest_url="", + REQUEST=REQUEST, + cancel_url="formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s" + % (formsemestre_id, etudid), + parameters={"etudid": etudid, "formsemestre_id": formsemestre_id}, + ) + + sco_formsemestre_validation.formsemestre_validation_suppress_etud( + context, formsemestre_id, etudid + ) + return REQUEST.RESPONSE.redirect( + context.ScoURL() + + "/Notes/formsemestre_validation_etud_form?formsemestre_id=%s&etudid=%s&head_message=Décision%%20supprimée" + % (formsemestre_id, etudid) + ) + + +# ------------- PV de JURY et archives +sco_publish("/formsemestre_pvjury", sco_pvjury.formsemestre_pvjury, Permission.ScoView) +sco_publish( + "/formsemestre_lettres_individuelles", + sco_pvjury.formsemestre_lettres_individuelles, + Permission.ScoView, +) +sco_publish( + "/formsemestre_pvjury_pdf", sco_pvjury.formsemestre_pvjury_pdf, Permission.ScoView +) +sco_publish( + "/feuille_preparation_jury", + sco_prepajury.feuille_preparation_jury, + Permission.ScoView, +) +sco_publish( + "/formsemestre_archive", sco_archives.formsemestre_archive, Permission.ScoView +) +sco_publish( + "/formsemestre_delete_archive", + sco_archives.formsemestre_delete_archive, + Permission.ScoView, +) +sco_publish( + "/formsemestre_list_archives", + sco_archives.formsemestre_list_archives, + Permission.ScoView, +) +sco_publish( + "/formsemestre_get_archived_file", + sco_archives.formsemestre_get_archived_file, + Permission.ScoView, +) +sco_publish("/view_apo_csv", sco_etape_apogee_view.view_apo_csv, Permission.ScoEditApo) +sco_publish( + "/view_apo_csv_store", + sco_etape_apogee_view.view_apo_csv_store, + Permission.ScoEditApo, +) +sco_publish( + "/view_apo_csv_download_and_store", + sco_etape_apogee_view.view_apo_csv_download_and_store, + Permission.ScoEditApo, +) +sco_publish( + "/view_apo_csv_delete", + sco_etape_apogee_view.view_apo_csv_delete, + Permission.ScoEditApo, +) +sco_publish( + "/view_scodoc_etuds", sco_etape_apogee_view.view_scodoc_etuds, Permission.ScoEditApo +) +sco_publish( + "/view_apo_etuds", sco_etape_apogee_view.view_apo_etuds, Permission.ScoEditApo +) +sco_publish( + "/apo_semset_maq_status", + sco_etape_apogee_view.apo_semset_maq_status, + Permission.ScoEditApo, +) +sco_publish( + "/apo_csv_export_results", + sco_etape_apogee_view.apo_csv_export_results, + Permission.ScoEditApo, +) + +# sco_semset +sco_publish("/semset_page", sco_semset.semset_page, Permission.ScoEditApo) +sco_publish("/do_semset_create", sco_semset.do_semset_create, Permission.ScoEditApo) +sco_publish("/do_semset_delete", sco_semset.do_semset_delete, Permission.ScoEditApo) +sco_publish( + "/edit_semset_set_title", sco_semset.edit_semset_set_title, Permission.ScoEditApo +) +sco_publish("/do_semset_add_sem", sco_semset.do_semset_add_sem, Permission.ScoEditApo) +sco_publish( + "/do_semset_remove_sem", sco_semset.do_semset_remove_sem, Permission.ScoEditApo +) + +# sco_export_result +sco_publish( + "/scodoc_table_results", + sco_export_results.scodoc_table_results, + Permission.ScoEditApo, +) + +sco_publish( + "/apo_compare_csv_form", sco_apogee_compare.apo_compare_csv_form, Permission.ScoView +) +sco_publish("/apo_compare_csv", sco_apogee_compare.apo_compare_csv, Permission.ScoView) + +# ------------- INSCRIPTIONS: PASSAGE D'UN SEMESTRE A UN AUTRE +sco_publish( + "/formsemestre_inscr_passage", + sco_inscr_passage.formsemestre_inscr_passage, + Permission.ScoEtudInscrit, +) +sco_publish( + "/formsemestre_synchro_etuds", + sco_synchro_etuds.formsemestre_synchro_etuds, + Permission.ScoView, +) + +# ------------- RAPPORTS STATISTIQUES +sco_publish( + "/formsemestre_report_counts", + sco_report.formsemestre_report_counts, + Permission.ScoView, +) +sco_publish( + "/formsemestre_suivi_cohorte", + sco_report.formsemestre_suivi_cohorte, + Permission.ScoView, +) +sco_publish( + "/formsemestre_suivi_parcours", + sco_report.formsemestre_suivi_parcours, + Permission.ScoView, +) +sco_publish( + "/formsemestre_etuds_lycees", + sco_lycee.formsemestre_etuds_lycees, + Permission.ScoView, +) +sco_publish( + "/scodoc_table_etuds_lycees", + sco_lycee.scodoc_table_etuds_lycees, + Permission.ScoView, +) +sco_publish( + "/formsemestre_graph_parcours", + sco_report.formsemestre_graph_parcours, + Permission.ScoView, +) +sco_publish( + "/formsemestre_poursuite_report", + sco_poursuite_dut.formsemestre_poursuite_report, + Permission.ScoView, +) +sco_publish("/pe_view_sem_recap", pe_view.pe_view_sem_recap, Permission.ScoView) +sco_publish( + "/report_debouche_date", sco_debouche.report_debouche_date, Permission.ScoView +) +sco_publish( + "/formsemestre_estim_cost", + sco_cost_formation.formsemestre_estim_cost, + Permission.ScoView, +) + +# -------------------------------------------------------------------- +# DEBUG + + +@bp.route("/check_sem_integrity") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def check_sem_integrity(context, formsemestre_id, REQUEST): + """Debug. + Check that ue and module formations are consistents + """ + sem = sco_formsemestre.get_formsemestre(context, formsemestre_id) + + modimpls = sco_moduleimpl.do_moduleimpl_list( + context, formsemestre_id=formsemestre_id + ) + bad_ue = [] + bad_sem = [] + for modimpl in modimpls: + mod = context.do_module_list({"module_id": modimpl["module_id"]})[0] + ue = context.do_ue_list({"ue_id": mod["ue_id"]})[0] + if ue["formation_id"] != mod["formation_id"]: + modimpl["mod"] = mod + modimpl["ue"] = ue + bad_ue.append(modimpl) + if sem["formation_id"] != mod["formation_id"]: + bad_sem.append(modimpl) + modimpl["mod"] = mod + + return ( + context.sco_header(REQUEST=REQUEST) + + "

formation_id=%s" % sem["formation_id"] + + "

Inconsistent UE/MOD:

" + + "
".join([str(x) for x in bad_ue]) + + "

Inconsistent SEM/MOD:

" + + "
".join([str(x) for x in bad_sem]) + + context.sco_footer(REQUEST) + ) + + +@bp.route("/check_form_integrity") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def check_form_integrity(context, formation_id, fix=False, REQUEST=None): + "debug" + log("check_form_integrity: formation_id=%s fix=%s" % (formation_id, fix)) + ues = context.do_ue_list(args={"formation_id": formation_id}) + bad = [] + for ue in ues: + mats = context.do_matiere_list(args={"ue_id": ue["ue_id"]}) + for mat in mats: + mods = context.do_module_list({"matiere_id": mat["matiere_id"]}) + for mod in mods: + if mod["ue_id"] != ue["ue_id"]: + if fix: + # fix mod.ue_id + log( + "fix: mod.ue_id = %s (was %s)" % (ue["ue_id"], mod["ue_id"]) + ) + mod["ue_id"] = ue["ue_id"] + context.do_module_edit(mod) + bad.append(mod) + if mod["formation_id"] != formation_id: + bad.append(mod) + if bad: + txth = "
".join([str(x) for x in bad]) + txt = "\n".join([str(x) for x in bad]) + log("check_form_integrity: formation_id=%s\ninconsistencies:" % formation_id) + log(txt) + # Notify by e-mail + sendAlarm(context, "Notes: formation incoherente !", txt) + else: + txth = "OK" + log("ok") + return context.sco_header(REQUEST=REQUEST) + txth + context.sco_footer(REQUEST) + + +@bp.route("/check_formsemestre_integrity") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def check_formsemestre_integrity(context, formsemestre_id, REQUEST=None): + "debug" + log("check_formsemestre_integrity: formsemestre_id=%s" % (formsemestre_id)) + # verifie que tous les moduleimpl d'un formsemestre + # se réfèrent à un module dont l'UE appartient a la même formation + # Ancien bug: les ue_id étaient mal copiés lors des création de versions + # de formations + diag = [] + + Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( + context, formsemestre_id=formsemestre_id + ) + for mod in Mlist: + if mod["module"]["ue_id"] != mod["matiere"]["ue_id"]: + diag.append( + "moduleimpl %s: module.ue_id=%s != matiere.ue_id=%s" + % ( + mod["moduleimpl_id"], + mod["module"]["ue_id"], + mod["matiere"]["ue_id"], + ) + ) + if mod["ue"]["formation_id"] != mod["module"]["formation_id"]: + diag.append( + "moduleimpl %s: ue.formation_id=%s != mod.formation_id=%s" + % ( + mod["moduleimpl_id"], + mod["ue"]["formation_id"], + mod["module"]["formation_id"], + ) + ) + if diag: + sendAlarm( + context, + "Notes: formation incoherente dans semestre %s !" % formsemestre_id, + "\n".join(diag), + ) + log("check_formsemestre_integrity: formsemestre_id=%s" % formsemestre_id) + log("inconsistencies:\n" + "\n".join(diag)) + else: + diag = ["OK"] + log("ok") + return ( + context.sco_header(REQUEST=REQUEST) + + "
".join(diag) + + context.sco_footer(REQUEST) + ) + + +@bp.route("/check_integrity_all") +@scodoc7func(context) +@permission_required(Permission.ScoView) +def check_integrity_all(context, REQUEST=None): + "debug: verifie tous les semestres et tt les formations" + # formations + for F in context.formation_list(): + context.check_form_integrity(F["formation_id"], REQUEST=REQUEST) + # semestres + for sem in sco_formsemestre.do_formsemestre_list(context): + context.check_formsemestre_integrity(sem["formsemestre_id"], REQUEST=REQUEST) + return ( + context.sco_header(REQUEST=REQUEST) + + "

empty page: see logs and mails

" + + context.sco_footer(REQUEST) + ) + + +# -------------------------------------------------------------------- +# Support for legacy ScoDoc 7 API +# -------------------------------------------------------------------- +sco_publish( + "/do_moduleimpl_list", sco_moduleimpl.do_moduleimpl_list, Permission.ScoView +) +sco_publish( + "/do_moduleimpl_withmodule_list", + sco_moduleimpl.do_moduleimpl_withmodule_list, + Permission.ScoView, +) diff --git a/app/views/scolar.py b/app/views/scolar.py new file mode 100644 index 00000000..84842dd9 --- /dev/null +++ b/app/views/scolar.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +""" +Module scolar: issu de ScoDoc7 / ZScolar.py + +Emmanuel Viennet, 2021 +""" + +from flask import g +from flask import current_app + +from app.decorators import ( + scodoc7func, + ScoDoc7Context, + permission_required, + admin_required, + login_required, +) +from app.auth.models import Permission + +from app.views import scolar_bp as bp + +context = ScoDoc7Context(globals()) + + +@bp.route("/about") +@scodoc7func(context) +def about(): + """Un exemple de fonction ScoDoc 7 dans ZScolar""" + return """ +

ScoDoc 7 ZScolar !

+

ZScolar ScoDoc 8

+

g.scodoc_dept=%(scodoc_dept)s

+ + + """ % { + "scodoc_dept": g.scodoc_dept, + } diff --git a/config.py b/config.py old mode 100644 new mode 100755 index d6db8dd9..7a9cf90e --- a/config.py +++ b/config.py @@ -7,12 +7,13 @@ BASEDIR = os.path.abspath(os.path.dirname(__file__)) load_dotenv(os.path.join(BASEDIR, ".env")) -class Config(object): - """General configution. Mostly loaded from environment via .env""" +class ConfigClass(object): + """General configuration. Mostly loaded from environment via .env""" SECRET_KEY = os.environ.get("SECRET_KEY") or "un-grand-secret-introuvable" SQLALCHEMY_DATABASE_URI = ( - os.environ.get("DATABASE_URL") or "postgresql://scodoc@localhost:5432/SCO8USERS" + os.environ.get("USERS_DATABASE_URI") + or "postgresql://scodoc@localhost:5432/SCO8USERS" ) SQLALCHEMY_TRACK_MODIFICATIONS = False LOG_TO_STDOUT = os.environ.get("LOG_TO_STDOUT") @@ -28,4 +29,15 @@ class Config(object): SCODOC_ERR_MAIL = os.environ.get("SCODOC_ERR_MAIL") BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL") # for ScoDoc 7 compat (à changer) - INSTANCE_HOME = os.environ.get("INSTANCE_HOME", "/opt/scodoc") \ No newline at end of file + INSTANCE_HOME = os.environ.get("INSTANCE_HOME", "/opt/scodoc") + + # For legacy ScoDoc7 installs: postgresql user + SCODOC7_SQL_USER = os.environ.get("SCODOC7_SQL_USER", "www-data") + DEFAULT_SQL_PORT = os.environ.get("DEFAULT_SQL_PORT", "5432") + + def __init__(self): + """Used to build some config variable at startup time""" + self.SCODOC_VAR_DIR = os.path.join(self.INSTANCE_HOME, "var", "scodoc") + + +Config = ConfigClass() diff --git a/requirements.txt b/requirements.txt index 849b88ee..5d0ff50b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ Flask-Migrate==2.7.0 Flask-Moment==0.11.0 Flask-SQLAlchemy==2.4.4 Flask-WTF==0.14.3 +icalendar==4.0.7 idna==2.10 itsdangerous==1.1.0 jaxml==3.2 @@ -24,13 +25,17 @@ MarkupSafe==1.1.1 Pillow==6.2.2 pkg-resources==0.0.0 psycopg2==2.8.6 +pyExcelerator==0.6.3a0 PyJWT==1.7.1 +PyRSS2Gen==1.1 python-dateutil==2.8.1 python-dotenv==0.15.0 python-editor==1.0.4 pytz==2021.1 +reportlab==3.5.59 six==1.15.0 SQLAlchemy==1.3.23 +stripogram==1.5 typing==3.7.4.3 visitor==0.1.3 Werkzeug==1.0.1 diff --git a/scodoc_manager.py b/scodoc_manager.py new file mode 100644 index 00000000..8cbbbe23 --- /dev/null +++ b/scodoc_manager.py @@ -0,0 +1,70 @@ +# -*- coding: UTF-8 -* + +""" +Manage departments, databases. + +Each departement `X` has its own database, `SCOX` +and a small configuration file `.../config/depts/X.cfg` +containing the database URI +Old ScoDoc7 installs config files contained `dbname=SCOX` +which translates as +"postgresql://@localhost:5432/" + being given by `SCODOC7_SQL_USER` env variable. +""" +import os +import re +import glob + +from config import Config + +from app.scodoc.sco_exceptions import ScoConfigurationError + + +class ScoDeptDescription: + def __init__(self, filename): + """Read dept description from dept file""" + if os.path.split(filename)[1][-4:] != ".cfg": + raise ScoConfigurationError("Invalid dept config filename: %s" % filename) + self.dept_id = os.path.split(filename)[1][:-4] + if not self.dept_id: + raise ScoConfigurationError("Invalid dept config filename: %s" % filename) + try: + db_uri = open(filename).read().strip() + except: + raise ScoConfigurationError("Department config file missing: %s" % filename) + m = re.match(r"dbname=SCO([a-zA-Z0-9]+$)", db_uri) + if m: + # ScoDoc7 backward compat + dept = m.group(1) # unused in ScoDoc7 + db_name = "SCO" + self.dept_id.upper() + db_uri = "postgresql://%(db_user)s@localhost:%(db_port)s/%(db_name)s" % { + "db_user": Config.SCODOC7_SQL_USER, + "db_name": db_name, + "db_port": Config.DEFAULT_SQL_PORT, + } + self.db_uri = db_uri + + +class ScoDocManager: + def __init__(self): + filenames = glob.glob(Config.SCODOC_VAR_DIR + "/config/depts/*.cfg") + descr_list = [ScoDeptDescription(f) for f in filenames] + self.dept_descriptions = {d.dept_id: d for d in descr_list} + + def get_dept_db_uri(self, dept_id): + "DB URI for this dept id" + return self.dept_descriptions[dept_id].db_uri + + def get_dept_ids(self): + "get (unsorted) dept ids" + return [d.dept_id for d in descr_list] + + def get_db_uri(self): + """ + Returns DB URI for the "current" departement. + Replaces ScoDoc7 GetDBConnexionString() + """ + return self.get_dept_db_uri(g.scodoc_dept) + + +sco_mgr = ScoDocManager()