diff --git a/app/scodoc/sco_formsemestre_edit.py b/app/scodoc/sco_formsemestre_edit.py
index f90d6857..f1e60163 100644
--- a/app/scodoc/sco_formsemestre_edit.py
+++ b/app/scodoc/sco_formsemestre_edit.py
@@ -1,1838 +1,1845 @@
-# -*- mode: python -*-
-# -*- coding: utf-8 -*-
-
-##############################################################################
-#
-# Gestion scolarite IUT
-#
-# Copyright (c) 1999 - 2022 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
-#
-##############################################################################
-
-"""Form choix modules / responsables et creation formsemestre
-"""
-import flask
-from flask import url_for, flash
-from flask import g, request
-from flask_login import current_user
-
-from app import db
-from app.auth.models import User
-from app.models import APO_CODE_STR_LEN, SHORT_STR_LEN
-from app.models import Module, ModuleImpl, Evaluation, EvaluationUEPoids, UniteEns
-from app.models import ScolarNews
-from app.models.formations import Formation
-from app.models.formsemestre import FormSemestre
-from app.models.but_refcomp import ApcParcours
-import app.scodoc.notesdb as ndb
-import app.scodoc.sco_utils as scu
-from app.scodoc import sco_cache
-from app.scodoc import sco_groups
-from app import log
-from app.scodoc.TrivialFormulator import TrivialFormulator
-from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
-from app.scodoc.sco_permissions import Permission
-from app.scodoc.sco_vdi import ApoEtapeVDI
-from app.scodoc import html_sco_header
-from app.scodoc import sco_codes_parcours
-from app.scodoc import sco_compute_moy
-from app.scodoc import sco_edit_module
-from app.scodoc import sco_etud
-from app.scodoc import sco_evaluation_db
-from app.scodoc import sco_formations
-from app.scodoc import sco_formsemestre
-from app.scodoc import sco_groups_copy
-from app.scodoc import sco_modalites
-from app.scodoc import sco_moduleimpl
-from app.scodoc import sco_cursus_dut
-from app.scodoc import sco_permissions_check
-from app.scodoc import sco_portal_apogee
-from app.scodoc import sco_preferences
-from app.scodoc import sco_users
-
-
-def _default_sem_title(formation):
- """Default title for a semestre in formation"""
- return formation.titre
-
-
-def formsemestre_createwithmodules():
- """Page création d'un semestre"""
- H = [
- html_sco_header.sco_header(
- page_title="Création d'un semestre",
- javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
- cssstyles=["css/autosuggest_inquisitor.css"],
- bodyOnLoad="init_tf_form('')",
- ),
- """
Mise en place d'un semestre de formation
""",
- ]
- r = do_formsemestre_createwithmodules()
- if isinstance(r, str):
- H.append(r)
- else:
- return r # response redirect
- return "\n".join(H) + html_sco_header.sco_footer()
-
-
-def formsemestre_editwithmodules(formsemestre_id):
- """Page modification semestre"""
- # portage from dtml
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- H = [
- html_sco_header.html_sem_header(
- "Modification du semestre",
- javascripts=["libjs/AutoSuggest.js", "js/formsemestre_edit.js"],
- cssstyles=["css/autosuggest_inquisitor.css"],
- bodyOnLoad="init_tf_form('')",
- )
- ]
- if not sem["etat"]:
- H.append(
- f"""
"""
- )
- else:
- r = do_formsemestre_createwithmodules(edit=1)
- if isinstance(r, str):
- H.append(r)
- else:
- return r # response redirect
- vals = scu.get_request_args()
- if not vals.get("tf_submitted", False):
- H.append(
- """
Seuls les modules cochés font partie de ce semestre.
- Pour les retirer, les décocher et appuyer sur le bouton "modifier".
-
-
Attention : s'il y a déjà des évaluations dans un module,
- il ne peut pas être supprimé !
-
Les modules ont toujours un responsable.
- Par défaut, c'est le directeur des études.
-
Un semestre ne peut comporter qu'une seule UE "bonus
- sport/culture"
- """
- )
-
- return "\n".join(H) + html_sco_header.sco_footer()
-
-
-def can_edit_sem(formsemestre_id="", sem=None):
- """Return sem if user can edit it, False otherwise"""
- sem = sem or sco_formsemestre.get_formsemestre(formsemestre_id)
- if not current_user.has_permission(Permission.ScoImplement): # pas chef
- if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
- return False
- return sem
-
-
-resp_fields = [
- "responsable_id",
- "responsable_id2",
- "responsable_id3",
- "responsable_id4",
-]
-
-
-def do_formsemestre_createwithmodules(edit=False):
- "Form choix modules / responsables et creation formsemestre"
- # Fonction accessible à tous, controle acces à la main:
- vals = scu.get_request_args()
- if edit:
- formsemestre_id = int(vals["formsemestre_id"])
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
- if not current_user.has_permission(Permission.ScoImplement):
- if not edit:
- # il faut ScoImplement pour créer un semestre
- raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
- else:
- if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
- raise AccessDenied(
- "vous n'avez pas le droit d'effectuer cette opération"
- )
-
- # Liste des enseignants avec form pour affichage / saisie avec suggestion
- # attention: il faut prendre ici tous les utilisateurs, même inactifs, car
- # les responsables de modules d'anciens semestres peuvent ne plus être actifs.
- # Mais la suggestion utilise get_user_list_xml() qui ne suggérera que les actifs.
- user_list = sco_users.get_user_list(with_inactives=True)
- uid2display = {} # user_name : forme pour affichage = "NOM Prenom (login)"
- for u in user_list:
- uid2display[u.id] = u.get_nomplogin()
- allowed_user_names = list(uid2display.values()) + [""]
- #
- formation_id = int(vals["formation_id"])
- formation = Formation.query.get(formation_id)
- if formation is None:
- raise ScoValueError("Formation inexistante !")
- is_apc = formation.is_apc()
- if not edit:
- initvalues = {"titre": _default_sem_title(formation)}
- semestre_id = int(vals["semestre_id"])
- module_ids_set = set()
- else:
- # setup form init values
- initvalues = sem
- semestre_id = initvalues["semestre_id"]
- # add associated modules to tf-checked:
- module_ids_existing = [modimpl.module.id for modimpl in formsemestre.modimpls]
- module_ids_set = set(module_ids_existing)
- initvalues["tf-checked"] = ["MI" + str(x) for x in module_ids_existing]
- for modimpl in formsemestre.modimpls:
- initvalues[f"MI{modimpl.module.id}"] = uid2display.get(
- modimpl.responsable_id,
- f"inconnu numéro {modimpl.responsable_id} resp. de {modimpl.id} !",
- )
- for index, resp in enumerate(sem["responsables"]):
- initvalues[resp_fields[index]] = uid2display.get(resp)
-
- # Liste des ID de semestres
- if formation.type_parcours is not None:
- parcours = sco_codes_parcours.get_parcours_from_code(formation.type_parcours)
- NB_SEM = parcours.NB_SEM
- else:
- NB_SEM = 10 # fallback, max 10 semestres
- if NB_SEM == 1:
- semestre_id_list = [-1]
- else:
- if edit and is_apc:
- # en APC, ne permet pas de changer de semestre
- semestre_id_list = [formsemestre.semestre_id]
- else:
- semestre_id_list = list(range(1, NB_SEM + 1))
- if not is_apc:
- # propose "pas de semestre" seulement en classique
- semestre_id_list.insert(0, -1)
-
- semestre_id_labels = []
- for sid in semestre_id_list:
- if sid == -1:
- semestre_id_labels.append("pas de semestres")
- else:
- semestre_id_labels.append(f"S{sid}")
- # Liste des modules dans cette formation
- if is_apc:
- # BUT: trie par type (res, sae), parcours, numéro
- modules = sorted(
- formation.modules,
- key=lambda m: m.sort_key_apc(),
- )
- else:
- modules = (
- Module.query.filter(
- Module.formation_id == formation_id, UniteEns.id == Module.ue_id
- )
- .order_by(Module.module_type, UniteEns.numero, Module.numero)
- .all()
- )
- # Pour regroupement des modules par semestres:
- semestre_ids = {}
- for mod in modules:
- semestre_ids[mod.semestre_id] = 1
- semestre_ids = list(semestre_ids.keys())
- semestre_ids.sort()
-
- modalites = sco_modalites.do_modalite_list()
- modalites_abbrv = [m["modalite"] for m in modalites]
- modalites_titles = [m["titre"] for m in modalites]
- #
- modform = [
- ("formsemestre_id", {"input_type": "hidden"}),
- ("formation_id", {"input_type": "hidden", "default": formation_id}),
- (
- "date_debut",
- {
- "title": "Date de début", # j/m/a
- "input_type": "datedmy",
- "explanation": "j/m/a",
- "size": 9,
- "allow_null": False,
- },
- ),
- (
- "date_fin",
- {
- "title": "Date de fin", # j/m/a
- "input_type": "datedmy",
- "explanation": "j/m/a",
- "size": 9,
- "allow_null": False,
- },
- ),
- *[
- (
- field,
- {
- "input_type": "text_suggest",
- "size": 50,
- "title": "(Co-)Directeur(s) des études"
- if index
- else "Directeur des études",
- "explanation": "(facultatif) taper le début du nom et choisir dans le menu"
- if index
- else "(obligatoire) taper le début du nom et choisir dans le menu",
- "allowed_values": allowed_user_names,
- "allow_null": index, # > 0, # il faut au moins un responsable de semestre
- "text_suggest_options": {
- "script": url_for(
- "users.get_user_list_xml", scodoc_dept=g.scodoc_dept
- )
- + "?", # "Users/get_user_list_xml?",
- "varname": "start",
- "json": False,
- "noresults": "Valeur invalide !",
- "timeout": 60000,
- },
- },
- )
- for index, field in enumerate(resp_fields)
- ],
- (
- "titre",
- {
- "size": 40,
- "title": "Nom de ce semestre",
- "explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans
- le titre: ils seront automatiquement ajoutés """,
- },
- ),
- (
- "modalite",
- {
- "input_type": "menu",
- "title": "Modalité",
- "allowed_values": modalites_abbrv,
- "labels": modalites_titles,
- },
- ),
- ]
- modform.append(
- (
- "semestre_id",
- {
- "input_type": "menu",
- "title": "Semestre dans la formation",
- "allowed_values": semestre_id_list,
- "labels": semestre_id_labels,
- "explanation": "en BUT, on ne peut pas modifier le semestre après création"
- if is_apc
- else "",
- "attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
- },
- ),
- )
- etapes = sco_portal_apogee.get_etapes_apogee_dept()
- # Propose les etapes renvoyées par le portail
- # et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement)
- etapes_set = {et[0] for et in etapes}
- if edit:
- for etape_vdi in sem["etapes"]:
- if etape_vdi.etape not in etapes_set:
- etapes.append((etape_vdi.etape, "inconnue"))
- modform.append(
- (
- "elt_help_apo",
- {
- "title": "Codes Apogée nécessaires pour inscrire les étudiants et exporter les notes en fin de semestre:",
- "input_type": "separator",
- },
- )
- )
-
- mf_manual = {
- "size": 12,
- "template": '
%(label)s
%(elem)s',
- "validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
- }
- if etapes:
- mf = {
- "input_type": "menu",
- "allowed_values": [""] + [e[0] for e in etapes],
- "labels": ["(aucune)"] + ["%s (%s)" % (e[1], e[0]) for e in etapes],
- "template": '
Sélectionner les modules, leurs responsables et les étudiants
- à inscrire:
- """
- else:
- formtit = """
Sélectionner les modules et leurs responsables
-
Si vous avez des parcours (options), dans un premier
- ne sélectionnez que les modules du tronc commun, puis après inscriptions,
- revenez ajouter les modules de parcours en sélectionnant les groupes d'étudiants
- à y inscrire.
-
"""
-
- modform += [
- (
- "gestion_compensation_lst",
- {
- "input_type": "checkbox",
- "title": "Jurys",
- "allowed_values": ["X"],
- "explanation": "proposer compensations de semestres (parcours DUT)",
- "labels": [""],
- },
- ),
- (
- "gestion_semestrielle_lst",
- {
- "input_type": "checkbox",
- "title": "",
- "allowed_values": ["X"],
- "explanation": "formation semestrialisée (jurys avec semestres décalés)",
- "labels": [""],
- },
- ),
- ]
- if current_user.has_permission(Permission.ScoImplement):
- modform += [
- (
- "resp_can_edit",
- {
- "input_type": "boolcheckbox",
- "title": "Autorisations",
- "explanation": "Autoriser le directeur des études à modifier ce semestre",
- },
- )
- ]
- modform += [
- (
- "resp_can_change_ens",
- {
- "input_type": "boolcheckbox",
- "title": "",
- "explanation": "Autoriser le directeur des études à modifier les enseignants",
- },
- ),
- (
- "ens_can_edit_eval",
- {
- "input_type": "boolcheckbox",
- "title": "",
- "explanation": """Autoriser tous les enseignants associés
- à un module à y créer des évaluations""",
- },
- ),
- (
- "bul_bgcolor",
- {
- "size": 8,
- "title": "Couleur fond des bulletins",
- "explanation": "version web seulement (ex: #ffeeee)",
- "validator": lambda val, _: len(val) < SHORT_STR_LEN,
- },
- ),
- (
- "bul_publish_xml_lst",
- {
- "input_type": "checkbox",
- "title": "Publication",
- "allowed_values": ["X"],
- "explanation": "publier le bulletin sur le portail étudiants",
- "labels": [""],
- },
- ),
- (
- "block_moyennes",
- {
- "input_type": "boolcheckbox",
- "title": "Bloquer moyennes",
- "explanation": "empêcher le calcul des moyennes d'UE et générale.",
- },
- ),
- ]
- # Choix des parcours
- if is_apc:
- ref_comp = formation.referentiel_competence
- if ref_comp:
- modform += [
- (
- "parcours",
- {
- "input_type": "checkbox",
- "vertical": True,
- "dom_id": "tf_module_parcours",
- "labels": [parcour.libelle for parcour in ref_comp.parcours],
- "allowed_values": [
- str(parcour.id) for parcour in ref_comp.parcours
- ],
- "explanation": """Parcours proposés dans ce semestre.
- S'il s'agit d'un semestre de "tronc commun", ne pas indiquer de parcours.""",
- },
- )
- ]
- if edit:
- sem["parcours"] = [str(parcour.id) for parcour in formsemestre.parcours]
- else:
- modform += [
- (
- "parcours",
- {
- "input_type": "separator",
- "title": f"""{scu.EMO_WARNING }
- Pas de parcours:
- vérifier la formation
- """,
- },
- )
- ]
-
- # Choix des modules
- modform += [
- (
- "sep",
- {
- "input_type": "separator",
- "title": "",
- "template": f"{formtit}
",
- },
- ),
- ]
-
- nbmod = 0
-
- for semestre_id in semestre_ids:
- if is_apc:
- # pour restreindre l'édition aux modules du semestre sélectionné
- tr_class = f'class="sem{semestre_id}"'
- else:
- tr_class = ""
- if edit:
- templ_sep = f"""
%(label)s
Responsable
Inscrire
"""
- else:
- templ_sep = (
- f"""
%(label)s
Responsable
"""
- )
- modform.append(
- (
- "sep",
- {
- "input_type": "separator",
- "title": f"Semestre {semestre_id}",
- "template": templ_sep,
- },
- )
- )
- for mod in modules:
- if mod.semestre_id == semestre_id and (
- (not edit) # creation => tous modules
- or (not is_apc) # pas BUT, on peut mixer les semestres
- or (semestre_id == formsemestre.semestre_id) # module du semestre
- or (mod.id in module_ids_set) # module déjà présent
- ):
- nbmod += 1
- if edit:
- select_name = f"{mod.id}!group_id"
-
- def opt_selected(gid):
- if gid == vals.get(select_name):
- return "selected"
- else:
- return ""
-
- if mod.id in module_ids_set:
- # pas de menu inscription si le module est déjà présent
- disabled = "disabled"
- else:
- disabled = ""
- fcg = f'"
- itemtemplate = f"""
-
%(label)s
-
%(elem)s
-
{fcg}
-
"""
- else:
- itemtemplate = f"""
-
%(label)s
-
%(elem)s
-
"""
- modform.append(
- (
- "MI" + str(mod.id),
- {
- "input_type": "text_suggest",
- "size": 50,
- "withcheckbox": True,
- "title": "%s %s" % (mod.code or "", mod.titre or ""),
- "allowed_values": allowed_user_names,
- "template": itemtemplate,
- "text_suggest_options": {
- "script": url_for(
- "users.get_user_list_xml", scodoc_dept=g.scodoc_dept
- )
- + "?",
- "varname": "start",
- "json": False,
- "noresults": "Valeur invalide !",
- "timeout": 60000,
- },
- },
- )
- )
- if nbmod == 0:
- modform.append(
- (
- "sep",
- {
- "input_type": "separator",
- "title": "aucun module dans cette formation !!!",
- },
- )
- )
- if edit:
- submitlabel = "Modifier ce semestre"
- else:
- submitlabel = "Créer ce semestre de formation"
- #
- # Etapes:
- if edit:
- n = 1
- for etape_vdi in sem["etapes"]:
- initvalues["etape_apo" + str(n)] = etape_vdi.etape
- initvalues["vdi_apo" + str(n)] = etape_vdi.vdi
- n += 1
- #
- initvalues["gestion_compensation"] = initvalues.get("gestion_compensation", False)
- if initvalues["gestion_compensation"]:
- initvalues["gestion_compensation_lst"] = ["X"]
- else:
- initvalues["gestion_compensation_lst"] = []
- if vals.get("tf_submitted", False) and "gestion_compensation_lst" not in vals:
- vals["gestion_compensation_lst"] = []
-
- initvalues["gestion_semestrielle"] = initvalues.get("gestion_semestrielle", False)
- if initvalues["gestion_semestrielle"]:
- initvalues["gestion_semestrielle_lst"] = ["X"]
- else:
- initvalues["gestion_semestrielle_lst"] = []
- if vals.get("tf_submitted", False) and "gestion_semestrielle_lst" not in vals:
- vals["gestion_semestrielle_lst"] = []
-
- initvalues["bul_hide_xml"] = initvalues.get("bul_hide_xml", False)
- if not initvalues["bul_hide_xml"]:
- initvalues["bul_publish_xml_lst"] = ["X"]
- else:
- initvalues["bul_publish_xml_lst"] = []
- if vals.get("tf_submitted", False) and "bul_publish_xml_lst" not in vals:
- vals["bul_publish_xml_lst"] = []
-
- #
- tf = TrivialFormulator(
- request.base_url,
- vals,
- modform,
- submitlabel=submitlabel,
- cancelbutton="Annuler",
- top_buttons=True,
- initvalues=initvalues,
- )
- msg = ""
- if tf[0] == 1:
- # check dates
- if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
- msg = '
Dates de début et fin incompatibles !
'
- if sco_preferences.get_preference("always_require_apo_sem_codes") and not any(
- [tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)]
- ):
- msg = '
"
- else:
- if tf[2]["gestion_compensation_lst"]:
- tf[2]["gestion_compensation"] = True
- else:
- tf[2]["gestion_compensation"] = False
- if tf[2]["gestion_semestrielle_lst"]:
- tf[2]["gestion_semestrielle"] = True
- else:
- tf[2]["gestion_semestrielle"] = False
- if tf[2]["bul_publish_xml_lst"]:
- tf[2]["bul_hide_xml"] = False
- else:
- tf[2]["bul_hide_xml"] = True
- # remap les identifiants de responsables:
- for field in resp_fields:
- tf[2][field] = User.get_user_id_from_nomplogin(tf[2][field])
- tf[2]["responsables"] = []
- for field in resp_fields:
- if tf[2][field]:
- tf[2]["responsables"].append(tf[2][field])
- for module_id in tf[2]["tf-checked"]:
- mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id])
- if mod_resp_id is None:
- # Si un module n'a pas de responsable (ou inconnu),
- # l'affecte au 1er directeur des etudes:
- mod_resp_id = tf[2]["responsable_id"]
- tf[2][module_id] = mod_resp_id
-
- # etapes:
- tf[2]["etapes"] = []
- if etapes: # menus => case supplementaire pour saisie manuelle, indicée 0
- start_i = 0
- else:
- start_i = 1
- for n in range(start_i, scu.EDIT_NB_ETAPES + 1):
- tf[2]["etapes"].append(
- ApoEtapeVDI(
- etape=tf[2]["etape_apo" + str(n)], vdi=tf[2]["vdi_apo" + str(n)]
- )
- )
- # Modules sélectionnés:
- # (retire le "MI" du début du nom de champs)
- module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]]
- _formsemestre_check_ue_bonus_unicity(module_ids_checked)
- if not edit:
- if is_apc:
- _formsemestre_check_module_list(
- module_ids_checked, tf[2]["semestre_id"]
- )
- # création du semestre
- formsemestre_id = sco_formsemestre.do_formsemestre_create(tf[2])
- # création des modules
- for module_id in module_ids_checked:
- modargs = {
- "module_id": module_id,
- "formsemestre_id": formsemestre_id,
- "responsable_id": tf[2][f"MI{module_id}"],
- }
- _ = sco_moduleimpl.do_moduleimpl_create(modargs)
- else:
- # Modification du semestre:
- # on doit creer les modules nouvellement selectionnés
- # modifier ceux à modifier, et DETRUIRE ceux qui ne sont plus selectionnés.
- # Note: la destruction échouera s'il y a des objets dépendants
- # (eg des évaluations définies)
- module_ids_tocreate = [
- x for x in module_ids_checked if not x in module_ids_existing
- ]
- if is_apc:
- _formsemestre_check_module_list(
- module_ids_tocreate, tf[2]["semestre_id"]
- )
- # modules existants à modifier
- module_ids_toedit = [
- x for x in module_ids_checked if x in module_ids_existing
- ]
- # modules à détruire
- module_ids_todelete = [
- x for x in module_ids_existing if not x in module_ids_checked
- ]
- #
- sco_formsemestre.do_formsemestre_edit(tf[2])
- #
- msg = []
- for module_id in module_ids_tocreate:
- modargs = {
- "module_id": module_id,
- "formsemestre_id": formsemestre_id,
- "responsable_id": tf[2]["MI" + str(module_id)],
- }
- moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs)
- mod = sco_edit_module.module_list({"module_id": module_id})[0]
- msg += [
- "création de %s (%s)" % (mod["code"] or "?", mod["titre"] or "?")
- ]
- # INSCRIPTIONS DES ETUDIANTS
- log(
- 'inscription module: %s = "%s"'
- % ("%s!group_id" % module_id, tf[2]["%s!group_id" % module_id])
- )
- group_id = tf[2]["%s!group_id" % module_id]
- if group_id:
- etudids = [
- x["etudid"] for x in sco_groups.get_group_members(group_id)
- ]
- log(
- "inscription module:module_id=%s,moduleimpl_id=%s: %s"
- % (module_id, moduleimpl_id, etudids)
- )
- sco_moduleimpl.do_moduleimpl_inscrit_etuds(
- moduleimpl_id,
- formsemestre_id,
- etudids,
- )
- msg += [
- "inscription de %d étudiants au module %s"
- % (len(etudids), mod["code"] or "(module sans code)")
- ]
- else:
- log(
- "inscription module:module_id=%s,moduleimpl_id=%s: aucun etudiant inscrit"
- % (module_id, moduleimpl_id)
- )
- #
- ok, diag = formsemestre_delete_moduleimpls(
- formsemestre_id, module_ids_todelete
- )
- msg += diag
- for module_id in module_ids_toedit:
- moduleimpl_id = sco_moduleimpl.moduleimpl_list(
- formsemestre_id=formsemestre_id, module_id=module_id
- )[0]["moduleimpl_id"]
- modargs = {
- "moduleimpl_id": moduleimpl_id,
- "module_id": module_id,
- "formsemestre_id": formsemestre_id,
- "responsable_id": tf[2]["MI" + str(module_id)],
- }
- sco_moduleimpl.do_moduleimpl_edit(
- modargs, formsemestre_id=formsemestre_id
- )
- mod = sco_edit_module.module_list({"module_id": module_id})[0]
- # --- Association des parcours
- formsemestre = FormSemestre.query.get(formsemestre_id)
- if "parcours" in tf[2]:
- formsemestre.parcours = [
- ApcParcours.query.get(int(parcour_id_str))
- for parcour_id_str in tf[2]["parcours"]
- ]
- db.session.add(formsemestre)
- db.session.commit()
- # --- Crée ou met à jour les groupes de parcours BUT
- formsemestre.setup_parcours_groups()
- # --- Fin
- if edit:
- if msg:
- msg_html = (
- '
Attention !
'
- + "
".join(msg)
- + "
"
- )
- if ok:
- msg_html += "
Modification effectuée
"
- else:
- msg_html += "
Modules non modifiés
"
- msg_html += (
- 'retour au tableau de bord'
- % formsemestre_id
- )
- return msg_html
- else:
- return flask.redirect(
- "formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié"
- % formsemestre_id
- )
- else:
- flash("Nouveau semestre créé")
- return flask.redirect(
- url_for(
- "notes.formsemestre_status",
- scodoc_dept=g.scodoc_dept,
- formsemestre_id=formsemestre_id,
- )
- )
-
-
-def _formsemestre_check_module_list(module_ids, semestre_idx):
- """En APC: Vérifie que tous les modules de la liste
- sont dans le semestre indiqué.
- Sinon, raise ScoValueError.
- """
- # vérification de la cohérence / modules / semestre
- mod_sems_idx = {
- Module.query.get_or_404(module_id).ue.semestre_idx for module_id in module_ids
- }
- if mod_sems_idx and mod_sems_idx != {semestre_idx}:
- modules = [Module.query.get_or_404(module_id) for module_id in module_ids]
- log(
- f"""_formsemestre_check_module_list:
- {chr(10).join( str(module) + " " + str(module.ue) for module in modules )}
- """
- )
- for module in modules:
- log(
- f"{module.code}\tsemestre_id={module.semestre_id}\tue.semestre_idx={module.ue.semestre_idx}"
- )
- raise ScoValueError(
- f"Les modules sélectionnés ne sont pas tous dans le semestre choisi (S{semestre_idx}) !",
- dest_url="javascript:history.back();",
- )
-
-
-def _formsemestre_check_ue_bonus_unicity(module_ids):
- """Vérifie qu'il n'y a qu'une seule UE bonus associée aux modules choisis"""
- ues = [Module.query.get_or_404(module_id).ue for module_id in module_ids]
- ues_bonus = {ue.id for ue in ues if ue.type == sco_codes_parcours.UE_SPORT}
- if len(ues_bonus) > 1:
- raise ScoValueError(
- """Les modules de bonus sélectionnés ne sont pas tous dans la même UE bonus.
- Changez la sélection ou modifiez la structure du programme de formation.""",
- dest_url="javascript:history.back();",
- )
-
-
-def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
- """Delete moduleimpls
- module_ids_to_del: list of module_id (warning: not moduleimpl)
- Moduleimpls must have no associated evaluations.
- """
- ok = True
- msg = []
- for module_id in module_ids_to_del:
- module = Module.query.get(module_id)
- if module is None:
- continue # ignore invalid ids
- modimpls = ModuleImpl.query.filter_by(
- formsemestre_id=formsemestre_id, module_id=module_id
- )
- for modimpl in modimpls:
- nb_evals = modimpl.evaluations.count()
- if nb_evals > 0:
- msg += [
- f"""impossible de supprimer {module.code} ({module.titre or ""})
- car il y a {nb_evals} évaluations définies
- (supprimez-les d\'abord)"""
- ]
- ok = False
- else:
- msg += [f"""suppression de {module.code} ({module.titre or ""})"""]
- db.session.delete(modimpl)
- if ok:
- db.session.commit()
- else:
- db.session.rollback()
- return ok, msg
-
-
-def formsemestre_clone(formsemestre_id):
- """
- Formulaire clonage d'un semestre
- """
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- # Liste des enseignants avec forme pour affichage / saisie avec suggestion
- user_list = sco_users.get_user_list()
- uid2display = {} # user_name : forme pour affichage = "NOM Prenom (login)"
- for u in user_list:
- uid2display[u.id] = u.get_nomplogin()
- allowed_user_names = list(uid2display.values()) + [""]
-
- initvalues = {
- "formsemestre_id": sem["formsemestre_id"],
- "responsable_id": uid2display.get(
- sem["responsables"][0], sem["responsables"][0]
- ),
- }
-
- H = [
- html_sco_header.html_sem_header(
- "Copie du semestre",
- javascripts=["libjs/AutoSuggest.js"],
- cssstyles=["css/autosuggest_inquisitor.css"],
- bodyOnLoad="init_tf_form('')",
- ),
- """
Cette opération duplique un semestre: on reprend les mêmes modules et responsables. Aucun étudiant n'est inscrit.
""",
- ]
-
- descr = [
- ("formsemestre_id", {"input_type": "hidden"}),
- (
- "date_debut",
- {
- "title": "Date de début", # j/m/a
- "input_type": "datedmy",
- "explanation": "j/m/a",
- "size": 9,
- "allow_null": False,
- },
- ),
- (
- "date_fin",
- {
- "title": "Date de fin", # j/m/a
- "input_type": "datedmy",
- "explanation": "j/m/a",
- "size": 9,
- "allow_null": False,
- },
- ),
- (
- "responsable_id",
- {
- "input_type": "text_suggest",
- "size": 50,
- "title": "Directeur des études",
- "explanation": "taper le début du nom et choisir dans le menu",
- "allowed_values": allowed_user_names,
- "allow_null": False,
- "text_suggest_options": {
- "script": url_for(
- "users.get_user_list_xml", scodoc_dept=g.scodoc_dept
- )
- + "?",
- "varname": "start",
- "json": False,
- "noresults": "Valeur invalide !",
- "timeout": 60000,
- },
- },
- ),
- (
- "clone_evaluations",
- {
- "title": "Copier aussi les évaluations",
- "input_type": "boolcheckbox",
- "explanation": "copie toutes les évaluations, sans les dates (ni les notes!)",
- },
- ),
- (
- "clone_partitions",
- {
- "title": "Copier aussi les partitions",
- "input_type": "boolcheckbox",
- "explanation": "copie toutes les partitions (sans les étudiants!)",
- },
- ),
- ]
- tf = TrivialFormulator(
- request.base_url,
- scu.get_request_args(),
- descr,
- submitlabel="Dupliquer ce semestre",
- cancelbutton="Annuler",
- initvalues=initvalues,
- )
- msg = ""
- if tf[0] == 1:
- # check dates
- if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
- msg = '
Dates de début et fin incompatibles !
'
- if tf[0] == 0 or msg:
- return "".join(H) + msg + tf[1] + html_sco_header.sco_footer()
- elif tf[0] == -1: # cancel
- return flask.redirect(
- "formsemestre_status?formsemestre_id=%s" % formsemestre_id
- )
- else:
- new_formsemestre_id = do_formsemestre_clone(
- formsemestre_id,
- User.get_user_id_from_nomplogin(tf[2]["responsable_id"]),
- tf[2]["date_debut"],
- tf[2]["date_fin"],
- clone_evaluations=tf[2]["clone_evaluations"],
- clone_partitions=tf[2]["clone_partitions"],
- )
- return flask.redirect(
- "formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé"
- % new_formsemestre_id
- )
-
-
-def do_formsemestre_clone(
- orig_formsemestre_id,
- responsable_id, # new resp.
- date_debut,
- date_fin, # 'dd/mm/yyyy'
- clone_evaluations=False,
- clone_partitions=False,
-):
- """Clone a semestre: make copy, same modules, same options, same resps, same partitions.
- New dates, responsable_id
- """
- log("cloning %s" % orig_formsemestre_id)
- orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id)
- cnx = ndb.GetDBConnexion()
- # 1- create sem
- args = orig_sem.copy()
- del args["formsemestre_id"]
- args["responsables"] = [responsable_id]
- args["date_debut"] = date_debut
- args["date_fin"] = date_fin
- args["etat"] = 1 # non verrouillé
- formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
- log("created formsemestre %s" % formsemestre_id)
- # 2- create moduleimpls
- mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
- for mod_orig in mods_orig:
- args = mod_orig.copy()
- args["formsemestre_id"] = formsemestre_id
- mid = sco_moduleimpl.do_moduleimpl_create(args)
- # copy notes_modules_enseignants
- ens = sco_moduleimpl.do_ens_list(
- args={"moduleimpl_id": mod_orig["moduleimpl_id"]}
- )
- for e in ens:
- args = e.copy()
- args["moduleimpl_id"] = mid
- sco_moduleimpl.do_ens_create(args)
- # optionally, copy evaluations
- if clone_evaluations:
- for e in Evaluation.query.filter_by(
- moduleimpl_id=mod_orig["moduleimpl_id"]
- ):
- # copie en enlevant la date
- new_eval = e.clone(not_copying=("jour", "moduleimpl_id"))
- new_eval.moduleimpl_id = mid
- # Copie les poids APC de l'évaluation
- new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
- db.session.commit()
-
- # 3- copy uecoefs
- objs = sco_formsemestre.formsemestre_uecoef_list(
- cnx, args={"formsemestre_id": orig_formsemestre_id}
- )
- for obj in objs:
- args = obj.copy()
- args["formsemestre_id"] = formsemestre_id
- _ = sco_formsemestre.formsemestre_uecoef_create(cnx, args)
-
- # NB: don't copy notes_formsemestre_custommenu (usually specific)
-
- # 4- Copy new style preferences
- prefs = sco_preferences.SemPreferences(orig_formsemestre_id)
-
- if orig_formsemestre_id in prefs.base_prefs.prefs:
- for pname in prefs.base_prefs.prefs[orig_formsemestre_id]:
- if not prefs.is_global(pname):
- pvalue = prefs[pname]
- try:
- prefs.base_prefs.set(formsemestre_id, pname, pvalue)
- except ValueError:
- log(
- "do_formsemestre_clone: ignoring old preference %s=%s for %s"
- % (pname, pvalue, formsemestre_id)
- )
-
- # 5- Copy formules utilisateur
- objs = sco_compute_moy.formsemestre_ue_computation_expr_list(
- cnx, args={"formsemestre_id": orig_formsemestre_id}
- )
- for obj in objs:
- args = obj.copy()
- args["formsemestre_id"] = formsemestre_id
- _ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args)
-
- # 5- Copy partitions and groups
- if clone_partitions:
- sco_groups_copy.clone_partitions_and_groups(
- orig_formsemestre_id, formsemestre_id
- )
-
- return formsemestre_id
-
-
-# ---------------------------------------------------------------------------------------
-
-
-def formsemestre_associate_new_version(
- formsemestre_id,
- other_formsemestre_ids=[],
- dialog_confirmed=False,
-):
- """Formulaire changement formation d'un semestre"""
- formsemestre_id = int(formsemestre_id)
- other_formsemestre_ids = [int(x) for x in other_formsemestre_ids]
- if not dialog_confirmed:
- # dresse le liste des semestres de la meme formation et version
- formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
- othersems = sco_formsemestre.do_formsemestre_list(
- args={
- "formation_id": formsemestre.formation.id,
- "version": formsemestre.formation.version,
- "etat": "1",
- },
- )
- H = []
- for s in othersems:
- if (
- s["formsemestre_id"] == formsemestre_id
- or s["formsemestre_id"] in other_formsemestre_ids
- ):
- checked = 'checked="checked"'
- else:
- checked = ""
- if s["formsemestre_id"] == formsemestre_id:
- disabled = 'disabled="1"'
- else:
- disabled = ""
- H.append(
- f"""
{s['titremois']}
"""
- )
-
- return scu.confirm_dialog(
- f"""
Associer à une nouvelle version de formation non verrouillée ?
-
Le programme pédagogique ("formation") va être dupliqué
- pour que vous puissiez le modifier sans affecter les autres
- semestres. Les autres paramètres (étudiants, notes...) du
- semestre seront inchangés.
-
-
Veillez à ne pas abuser de cette possibilité, car créer
- trop de versions de formations va vous compliquer la gestion
- (à vous de garder trace des différences et à ne pas vous
- tromper par la suite...).
-
-
Si vous souhaitez créer un programme pour de futurs semestres,
- utilisez plutôt Créer une nouvelle version.
-
-
-
Si vous voulez associer aussi d'autres semestres à la nouvelle
- version, cochez-les:
-
"""
- + "".join(H)
- + "
",
- OK="Associer ces semestres à une nouvelle version",
- dest_url="",
- cancel_url=url_for(
- "notes.formsemestre_status",
- scodoc_dept=g.scodoc_dept,
- formsemestre_id=formsemestre_id,
- ),
- parameters={"formsemestre_id": formsemestre_id},
- )
- else:
- do_formsemestres_associate_new_version(
- [formsemestre_id] + other_formsemestre_ids
- )
- flash("Semestre associé à une nouvelle version de la formation")
- return flask.redirect(
- url_for(
- "notes.formsemestre_status",
- scodoc_dept=g.scodoc_dept,
- formsemestre_id=formsemestre_id,
- )
- )
-
-
-def do_formsemestres_associate_new_version(formsemestre_ids):
- """Cree une nouvelle version de la formation du semestre, et y rattache les semestres.
- Tous les moduleimpl sont ré-associés à la nouvelle formation, ainsi que les decisions de jury
- si elles existent (codes d'UE validées).
- Les semestre doivent tous appartenir à la meme version de la formation
- """
- log(f"do_formsemestres_associate_new_version {formsemestre_ids}")
- if not formsemestre_ids:
- return
- # Check: tous de la même formation
- assert isinstance(formsemestre_ids[0], int)
- sem = sco_formsemestre.get_formsemestre(formsemestre_ids[0])
- formation_id = sem["formation_id"]
- for formsemestre_id in formsemestre_ids[1:]:
- assert isinstance(formsemestre_id, int)
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- if formation_id != sem["formation_id"]:
- raise ScoValueError("les semestres ne sont pas tous de la même formation !")
-
- cnx = ndb.GetDBConnexion()
- # New formation:
- (
- formation_id,
- modules_old2new,
- ues_old2new,
- ) = sco_formations.formation_create_new_version(formation_id, redirect=False)
- # Log new ues:
- for ue_id in ues_old2new:
- ue = UniteEns.query.get(ue_id)
- new_ue = UniteEns.query.get(ues_old2new[ue_id])
- assert ue.semestre_idx == new_ue.semestre_idx
- log(f"{ue} -> {new_ue}")
- # Log new modules
- for module_id in modules_old2new:
- mod = Module.query.get(module_id)
- new_mod = Module.query.get(modules_old2new[module_id])
- assert mod.semestre_id == new_mod.semestre_id
- log(f"{mod} -> {new_mod}")
- # re-associate
- for formsemestre_id in formsemestre_ids:
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- sem["formation_id"] = formation_id
- sco_formsemestre.do_formsemestre_edit(sem, cnx=cnx, html_quote=False)
- _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
-
- cnx.commit()
-
-
-def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new):
- """Associe les moduleimpls d'un semestre existant à un autre programme
- et met à jour les décisions de jury (validations d'UE).
- """
- # re-associate moduleimpls to new modules:
- modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
- for mod in modimpls:
- mod["module_id"] = modules_old2new[mod["module_id"]]
- sco_moduleimpl.do_moduleimpl_edit(mod, formsemestre_id=formsemestre_id)
- # Update poids des évaluations
- # les poids associent les évaluations aux UE (qui ont changé d'id)
- for poids in EvaluationUEPoids.query.filter(
- EvaluationUEPoids.evaluation_id == Evaluation.id,
- Evaluation.moduleimpl_id == ModuleImpl.id,
- ModuleImpl.formsemestre_id == formsemestre_id,
- ):
- poids.ue_id = ues_old2new[poids.ue_id]
- db.session.add(poids)
- db.session.commit()
-
- # update decisions:
- events = sco_etud.scolar_events_list(cnx, args={"formsemestre_id": formsemestre_id})
- for e in events:
- if e["ue_id"]:
- e["ue_id"] = ues_old2new[e["ue_id"]]
- sco_etud.scolar_events_edit(cnx, e)
- validations = sco_cursus_dut.scolar_formsemestre_validation_list(
- cnx, args={"formsemestre_id": formsemestre_id}
- )
- for e in validations:
- if e["ue_id"]:
- e["ue_id"] = ues_old2new[e["ue_id"]]
- # log('e=%s' % e )
- sco_cursus_dut.scolar_formsemestre_validation_edit(cnx, e)
-
-
-def formsemestre_delete(formsemestre_id):
- """Delete a formsemestre (affiche avertissements)"""
- formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
- H = [
- html_sco_header.html_sem_header("Suppression du semestre"),
- """
Attention !
-
A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
-un semestre ne doit jamais être supprimé
-(on perd la mémoire des notes et de tous les événements liés à ce semestre !).
-
-
-
Tous les modules de ce semestre seront supprimés.
-Ceci n'est possible que si :
-
-
-
aucune décision de jury n'a été entrée dans ce semestre;
-
et aucun étudiant de ce semestre ne le compense avec un autre semestre.
""",
- dest_url="",
- cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
- parameters={"formsemestre_id": formsemestre_id},
- )
- # Bon, s'il le faut...
- do_formsemestre_delete(formsemestre_id)
- return flask.redirect(scu.ScoURL() + "?head_message=Semestre%20supprimé")
-
-
-def formsemestre_has_decisions_or_compensations(formsemestre_id):
- """True if decision de jury dans ce semestre
- ou bien compensation de ce semestre par d'autre ssemestres.
- """
- r = ndb.SimpleDictFetch(
- """SELECT v.id AS formsemestre_validation_id, v.*
- FROM scolar_formsemestre_validation v
- WHERE v.formsemestre_id = %(formsemestre_id)s
- OR v.compense_formsemestre_id = %(formsemestre_id)s""",
- {"formsemestre_id": formsemestre_id},
- )
- return r
-
-
-def do_formsemestre_delete(formsemestre_id):
- """delete formsemestre, and all its moduleimpls.
- No checks, no warnings: erase all !
- """
- cnx = ndb.GetDBConnexion()
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-
- sco_cache.EvaluationCache.invalidate_sem(formsemestre_id)
-
- # --- Destruction des modules de ce semestre
- mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
- for mod in mods:
- # evaluations
- evals = sco_evaluation_db.do_evaluation_list(
- args={"moduleimpl_id": mod["moduleimpl_id"]}
- )
- for e in evals:
- ndb.SimpleQuery(
- "DELETE FROM notes_notes WHERE evaluation_id=%(evaluation_id)s",
- e,
- )
- ndb.SimpleQuery(
- "DELETE FROM notes_notes_log WHERE evaluation_id=%(evaluation_id)s",
- e,
- )
- ndb.SimpleQuery(
- "DELETE FROM notes_evaluation WHERE id=%(evaluation_id)s",
- e,
- )
-
- sco_moduleimpl.do_moduleimpl_delete(
- mod["moduleimpl_id"], formsemestre_id=formsemestre_id
- )
- # --- Desinscription des etudiants
- cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
- req = "DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Suppression des evenements
- req = "DELETE FROM scolar_events WHERE formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Suppression des appreciations
- req = "DELETE FROM notes_appreciations WHERE formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Supression des validations (!!!)
- req = "DELETE FROM scolar_formsemestre_validation WHERE formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Supression des references a ce semestre dans les compensations:
- req = "UPDATE scolar_formsemestre_validation SET compense_formsemestre_id=NULL WHERE compense_formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Suppression des autorisations
- req = "DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Suppression des coefs d'UE capitalisées
- req = "DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Suppression des item du menu custom
- req = "DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Suppression des formules
- req = "DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Suppression des preferences
- req = "DELETE FROM sco_prefs WHERE formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Suppression des groupes et partitions
- req = """DELETE FROM group_membership
- WHERE group_id IN
- (SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd
- WHERE gm.group_id = gd.id AND gd.partition_id = p.id
- AND p.formsemestre_id=%(formsemestre_id)s)
- """
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- req = """DELETE FROM group_descr
- WHERE id IN
- (SELECT gd.id FROM group_descr gd, partition p
- WHERE gd.partition_id = p.id
- AND p.formsemestre_id=%(formsemestre_id)s)
- """
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- req = "DELETE FROM partition WHERE formsemestre_id=%(formsemestre_id)s"
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Responsables
- req = """DELETE FROM notes_formsemestre_responsables
- WHERE formsemestre_id=%(formsemestre_id)s"""
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
- # --- Etapes
- req = """DELETE FROM notes_formsemestre_etapes
- WHERE formsemestre_id=%(formsemestre_id)s"""
- cursor.execute(req, {"formsemestre_id": formsemestre_id})
-
- # --- Destruction du semestre
- sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)
-
- # news
- ScolarNews.add(
- typ=ScolarNews.NEWS_SEM,
- obj=formsemestre_id,
- text="Suppression du semestre %(titre)s" % sem,
- )
-
-
-# ---------------------------------------------------------------------------------------
-def formsemestre_edit_options(formsemestre_id):
- """dialog to change formsemestre options
- (accessible par ScoImplement ou dir. etudes)
- """
- log("formsemestre_edit_options")
- ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
- if not ok:
- return err
- return sco_preferences.SemPreferences(formsemestre_id).edit(categories=["bul"])
-
-
-def formsemestre_change_lock(formsemestre_id) -> None:
- """Change etat (verrouille si ouvert, déverrouille si fermé)
- nota: etat (1 ouvert, 0 fermé)
- """
- ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
- if not ok:
- return err
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- etat = not sem["etat"]
-
- args = {"formsemestre_id": formsemestre_id, "etat": etat}
- sco_formsemestre.do_formsemestre_edit(args)
-
-
-def formsemestre_change_publication_bul(
- formsemestre_id, dialog_confirmed=False, redirect=True
-):
- """Change etat publication bulletins sur portail"""
- ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
- if not ok:
- return err
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
- etat = not sem["bul_hide_xml"]
-
- if not dialog_confirmed:
- if etat:
- msg = "non"
- else:
- msg = ""
- return scu.confirm_dialog(
- "
Confirmer la %s publication des bulletins ?
" % msg,
- helpmsg="""Il est parfois utile de désactiver la diffusion des bulletins,
- par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
-
- Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc et un portail étudiant.
- """,
- dest_url="",
- cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
- parameters={"bul_hide_xml": etat, "formsemestre_id": formsemestre_id},
- )
-
- args = {"formsemestre_id": formsemestre_id, "bul_hide_xml": etat}
- sco_formsemestre.do_formsemestre_edit(args)
- if redirect:
- return flask.redirect(
- "formsemestre_status?formsemestre_id=%s" % formsemestre_id
- )
- return None
-
-
-def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
- """Changement manuel des coefficients des UE capitalisées."""
- from app.scodoc import notes_table
-
- ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
- if not ok:
- return err
- sem = sco_formsemestre.get_formsemestre(formsemestre_id)
-
- footer = html_sco_header.sco_footer()
- help = """
- Seuls les modules ont un coefficient. Cependant, il est nécessaire d'affecter un coefficient aux UE capitalisée pour pouvoir les prendre en compte dans la moyenne générale.
-
-
ScoDoc calcule normalement le coefficient d'une UE comme la somme des
- coefficients des modules qui la composent.
-
-
Dans certains cas, on n'a pas les mêmes modules dans le semestre antérieur
- (capitalisé) et dans le semestre courant, et le coefficient d'UE est alors variable.
- Il est alors possible de forcer la valeur du coefficient d'UE.
-
-
- Indiquez "auto" (ou laisser vide) pour que ScoDoc calcule automatiquement le coefficient,
- ou bien entrez une valeur (nombre réel).
-
-
Dans le doute, si le mode auto n'est pas applicable et que tous les étudiants sont inscrits aux mêmes modules de ce semestre, prenez comme coefficient la somme indiquée.
- Sinon, référez vous au programme pédagogique. Les lignes en rouge
- sont à changer.
-
-
Les coefficients indiqués ici ne s'appliquent que pour le traitement des UE capitalisées.
-
"""
+ )
+ else:
+ r = do_formsemestre_createwithmodules(edit=1)
+ if isinstance(r, str):
+ H.append(r)
+ else:
+ return r # response redirect
+ vals = scu.get_request_args()
+ if not vals.get("tf_submitted", False):
+ H.append(
+ """
Seuls les modules cochés font partie de ce semestre.
+ Pour les retirer, les décocher et appuyer sur le bouton "modifier".
+
+
Attention : s'il y a déjà des évaluations dans un module,
+ il ne peut pas être supprimé !
+
Les modules ont toujours un responsable.
+ Par défaut, c'est le directeur des études.
+
Un semestre ne peut comporter qu'une seule UE "bonus
+ sport/culture"
+ """
+ )
+
+ return "\n".join(H) + html_sco_header.sco_footer()
+
+
+def can_edit_sem(formsemestre_id="", sem=None):
+ """Return sem if user can edit it, False otherwise"""
+ sem = sem or sco_formsemestre.get_formsemestre(formsemestre_id)
+ if not current_user.has_permission(Permission.ScoImplement): # pas chef
+ if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
+ return False
+ return sem
+
+
+resp_fields = [
+ "responsable_id",
+ "responsable_id2",
+ "responsable_id3",
+ "responsable_id4",
+]
+
+
+def do_formsemestre_createwithmodules(edit=False):
+ "Form choix modules / responsables et creation formsemestre"
+ # Fonction accessible à tous, controle acces à la main:
+ vals = scu.get_request_args()
+ if edit:
+ formsemestre_id = int(vals["formsemestre_id"])
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ if not current_user.has_permission(Permission.ScoImplement):
+ if not edit:
+ # il faut ScoImplement pour créer un semestre
+ raise AccessDenied("vous n'avez pas le droit d'effectuer cette opération")
+ else:
+ if not sem["resp_can_edit"] or current_user.id not in sem["responsables"]:
+ raise AccessDenied(
+ "vous n'avez pas le droit d'effectuer cette opération"
+ )
+
+ # Liste des enseignants avec form pour affichage / saisie avec suggestion
+ # attention: il faut prendre ici tous les utilisateurs, même inactifs, car
+ # les responsables de modules d'anciens semestres peuvent ne plus être actifs.
+ # Mais la suggestion utilise get_user_list_xml() qui ne suggérera que les actifs.
+ user_list = sco_users.get_user_list(with_inactives=True)
+ uid2display = {} # user_name : forme pour affichage = "NOM Prenom (login)"
+ for u in user_list:
+ uid2display[u.id] = u.get_nomplogin()
+ allowed_user_names = list(uid2display.values()) + [""]
+ #
+ formation_id = int(vals["formation_id"])
+ formation = Formation.query.get(formation_id)
+ if formation is None:
+ raise ScoValueError("Formation inexistante !")
+ is_apc = formation.is_apc()
+ if not edit:
+ initvalues = {"titre": _default_sem_title(formation)}
+ semestre_id = int(vals["semestre_id"])
+ module_ids_set = set()
+ else:
+ # setup form init values
+ initvalues = sem
+ semestre_id = initvalues["semestre_id"]
+ # add associated modules to tf-checked:
+ module_ids_existing = [modimpl.module.id for modimpl in formsemestre.modimpls]
+ module_ids_set = set(module_ids_existing)
+ initvalues["tf-checked"] = ["MI" + str(x) for x in module_ids_existing]
+ for modimpl in formsemestre.modimpls:
+ initvalues[f"MI{modimpl.module.id}"] = uid2display.get(
+ modimpl.responsable_id,
+ f"inconnu numéro {modimpl.responsable_id} resp. de {modimpl.id} !",
+ )
+ for index, resp in enumerate(sem["responsables"]):
+ initvalues[resp_fields[index]] = uid2display.get(resp)
+
+ # Liste des ID de semestres
+ if formation.type_parcours is not None:
+ parcours = sco_codes_parcours.get_parcours_from_code(formation.type_parcours)
+ NB_SEM = parcours.NB_SEM
+ else:
+ NB_SEM = 10 # fallback, max 10 semestres
+ if NB_SEM == 1:
+ semestre_id_list = [-1]
+ else:
+ if edit and is_apc:
+ # en APC, ne permet pas de changer de semestre
+ semestre_id_list = [formsemestre.semestre_id]
+ else:
+ semestre_id_list = list(range(1, NB_SEM + 1))
+ if not is_apc:
+ # propose "pas de semestre" seulement en classique
+ semestre_id_list.insert(0, -1)
+
+ semestre_id_labels = []
+ for sid in semestre_id_list:
+ if sid == -1:
+ semestre_id_labels.append("pas de semestres")
+ else:
+ semestre_id_labels.append(f"S{sid}")
+ # Liste des modules dans cette formation
+ if is_apc:
+ # BUT: trie par type (res, sae), parcours, numéro
+ modules = sorted(
+ formation.modules,
+ key=lambda m: m.sort_key_apc(),
+ )
+ else:
+ modules = (
+ Module.query.filter(
+ Module.formation_id == formation_id, UniteEns.id == Module.ue_id
+ )
+ .order_by(Module.module_type, UniteEns.numero, Module.numero)
+ .all()
+ )
+ # Pour regroupement des modules par semestres:
+ semestre_ids = {}
+ for mod in modules:
+ semestre_ids[mod.semestre_id] = 1
+ semestre_ids = list(semestre_ids.keys())
+ semestre_ids.sort()
+
+ modalites = sco_modalites.do_modalite_list()
+ modalites_abbrv = [m["modalite"] for m in modalites]
+ modalites_titles = [m["titre"] for m in modalites]
+ #
+ modform = [
+ ("formsemestre_id", {"input_type": "hidden"}),
+ ("formation_id", {"input_type": "hidden", "default": formation_id}),
+ (
+ "date_debut",
+ {
+ "title": "Date de début", # j/m/a
+ "input_type": "datedmy",
+ "explanation": "j/m/a",
+ "size": 9,
+ "allow_null": False,
+ },
+ ),
+ (
+ "date_fin",
+ {
+ "title": "Date de fin", # j/m/a
+ "input_type": "datedmy",
+ "explanation": "j/m/a",
+ "size": 9,
+ "allow_null": False,
+ },
+ ),
+ *[
+ (
+ field,
+ {
+ "input_type": "text_suggest",
+ "size": 50,
+ "title": "(Co-)Directeur(s) des études"
+ if index
+ else "Directeur des études",
+ "explanation": "(facultatif) taper le début du nom et choisir dans le menu"
+ if index
+ else "(obligatoire) taper le début du nom et choisir dans le menu",
+ "allowed_values": allowed_user_names,
+ "allow_null": index, # > 0, # il faut au moins un responsable de semestre
+ "text_suggest_options": {
+ "script": url_for(
+ "users.get_user_list_xml", scodoc_dept=g.scodoc_dept
+ )
+ + "?", # "Users/get_user_list_xml?",
+ "varname": "start",
+ "json": False,
+ "noresults": "Valeur invalide !",
+ "timeout": 60000,
+ },
+ },
+ )
+ for index, field in enumerate(resp_fields)
+ ],
+ (
+ "titre",
+ {
+ "size": 40,
+ "title": "Nom de ce semestre",
+ "explanation": f"""n'indiquez pas les dates, ni le semestre, ni la modalité dans
+ le titre: ils seront automatiquement ajoutés """,
+ },
+ ),
+ (
+ "modalite",
+ {
+ "input_type": "menu",
+ "title": "Modalité",
+ "allowed_values": modalites_abbrv,
+ "labels": modalites_titles,
+ },
+ ),
+ ]
+ modform.append(
+ (
+ "semestre_id",
+ {
+ "input_type": "menu",
+ "title": "Semestre dans la formation",
+ "allowed_values": semestre_id_list,
+ "labels": semestre_id_labels,
+ "explanation": "en BUT, on ne peut pas modifier le semestre après création"
+ if is_apc
+ else "",
+ "attributes": ['onchange="change_semestre_id();"'] if is_apc else "",
+ },
+ ),
+ )
+ etapes = sco_portal_apogee.get_etapes_apogee_dept()
+ # Propose les etapes renvoyées par le portail
+ # et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement)
+ etapes_set = {et[0] for et in etapes}
+ if edit:
+ for etape_vdi in sem["etapes"]:
+ if etape_vdi.etape not in etapes_set:
+ etapes.append((etape_vdi.etape, "inconnue"))
+ modform.append(
+ (
+ "elt_help_apo",
+ {
+ "title": "Codes Apogée nécessaires pour inscrire les étudiants et exporter les notes en fin de semestre:",
+ "input_type": "separator",
+ },
+ )
+ )
+
+ mf_manual = {
+ "size": 12,
+ "template": '
%(label)s
%(elem)s',
+ "validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
+ }
+ if etapes:
+ mf = {
+ "input_type": "menu",
+ "allowed_values": [""] + [e[0] for e in etapes],
+ "labels": ["(aucune)"] + ["%s (%s)" % (e[1], e[0]) for e in etapes],
+ "template": '
Sélectionner les modules, leurs responsables et les étudiants
+ à inscrire:
+ """
+ else:
+ formtit = """
Sélectionner les modules et leurs responsables
+
Si vous avez des parcours (options), dans un premier
+ ne sélectionnez que les modules du tronc commun, puis après inscriptions,
+ revenez ajouter les modules de parcours en sélectionnant les groupes d'étudiants
+ à y inscrire.
+
"""
+
+ modform += [
+ (
+ "gestion_compensation_lst",
+ {
+ "input_type": "checkbox",
+ "title": "Jurys",
+ "allowed_values": ["X"],
+ "explanation": "proposer compensations de semestres (parcours DUT)",
+ "labels": [""],
+ },
+ ),
+ (
+ "gestion_semestrielle_lst",
+ {
+ "input_type": "checkbox",
+ "title": "",
+ "allowed_values": ["X"],
+ "explanation": "formation semestrialisée (jurys avec semestres décalés)",
+ "labels": [""],
+ },
+ ),
+ ]
+ if current_user.has_permission(Permission.ScoImplement):
+ modform += [
+ (
+ "resp_can_edit",
+ {
+ "input_type": "boolcheckbox",
+ "title": "Autorisations",
+ "explanation": "Autoriser le directeur des études à modifier ce semestre",
+ },
+ )
+ ]
+ modform += [
+ (
+ "resp_can_change_ens",
+ {
+ "input_type": "boolcheckbox",
+ "title": "",
+ "explanation": "Autoriser le directeur des études à modifier les enseignants",
+ },
+ ),
+ (
+ "ens_can_edit_eval",
+ {
+ "input_type": "boolcheckbox",
+ "title": "",
+ "explanation": """Autoriser tous les enseignants associés
+ à un module à y créer des évaluations""",
+ },
+ ),
+ (
+ "bul_bgcolor",
+ {
+ "size": 8,
+ "title": "Couleur fond des bulletins",
+ "explanation": "version web seulement (ex: #ffeeee)",
+ "validator": lambda val, _: len(val) < SHORT_STR_LEN,
+ },
+ ),
+ (
+ "bul_publish_xml_lst",
+ {
+ "input_type": "checkbox",
+ "title": "Publication",
+ "allowed_values": ["X"],
+ "explanation": "publier le bulletin sur le portail étudiants",
+ "labels": [""],
+ },
+ ),
+ (
+ "block_moyennes",
+ {
+ "input_type": "boolcheckbox",
+ "title": "Bloquer moyennes",
+ "explanation": "empêcher le calcul des moyennes d'UE et générale.",
+ },
+ ),
+ ]
+ # Choix des parcours
+ if is_apc:
+ ref_comp = formation.referentiel_competence
+ if ref_comp:
+ modform += [
+ (
+ "parcours",
+ {
+ "input_type": "checkbox",
+ "vertical": True,
+ "dom_id": "tf_module_parcours",
+ "labels": [parcour.libelle for parcour in ref_comp.parcours],
+ "allowed_values": [
+ str(parcour.id) for parcour in ref_comp.parcours
+ ],
+ "explanation": """Parcours proposés dans ce semestre.
+ S'il s'agit d'un semestre de "tronc commun", ne pas indiquer de parcours.""",
+ },
+ )
+ ]
+ if edit:
+ sem["parcours"] = [str(parcour.id) for parcour in formsemestre.parcours]
+ else:
+ modform += [
+ (
+ "parcours",
+ {
+ "input_type": "separator",
+ "title": f"""{scu.EMO_WARNING }
+ Pas de parcours:
+ vérifier la formation
+ """,
+ },
+ )
+ ]
+
+ # Choix des modules
+ modform += [
+ (
+ "sep",
+ {
+ "input_type": "separator",
+ "title": "",
+ "template": f"
{formtit}
",
+ },
+ ),
+ ]
+
+ nbmod = 0
+
+ for semestre_id in semestre_ids:
+ if is_apc:
+ # pour restreindre l'édition aux modules du semestre sélectionné
+ tr_class = f'class="sem{semestre_id}"'
+ else:
+ tr_class = ""
+ if edit:
+ templ_sep = f"""
%(label)s
Responsable
Inscrire
"""
+ else:
+ templ_sep = (
+ f"""
%(label)s
Responsable
"""
+ )
+ modform.append(
+ (
+ "sep",
+ {
+ "input_type": "separator",
+ "title": f"Semestre {semestre_id}",
+ "template": templ_sep,
+ },
+ )
+ )
+ for mod in modules:
+ if mod.semestre_id == semestre_id and (
+ (not edit) # creation => tous modules
+ or (not is_apc) # pas BUT, on peut mixer les semestres
+ or (semestre_id == formsemestre.semestre_id) # module du semestre
+ or (mod.id in module_ids_set) # module déjà présent
+ ):
+ nbmod += 1
+ if edit:
+ select_name = f"{mod.id}!group_id"
+
+ def opt_selected(gid):
+ if gid == vals.get(select_name):
+ return "selected"
+ else:
+ return ""
+
+ if mod.id in module_ids_set:
+ # pas de menu inscription si le module est déjà présent
+ disabled = "disabled"
+ else:
+ disabled = ""
+ fcg = f'"
+ itemtemplate = f"""
+
%(label)s
+
%(elem)s
+
{fcg}
+
"""
+ else:
+ itemtemplate = f"""
+
%(label)s
+
%(elem)s
+
"""
+ modform.append(
+ (
+ "MI" + str(mod.id),
+ {
+ "input_type": "text_suggest",
+ "size": 50,
+ "withcheckbox": True,
+ "title": "%s %s" % (mod.code or "", mod.titre or ""),
+ "allowed_values": allowed_user_names,
+ "template": itemtemplate,
+ "text_suggest_options": {
+ "script": url_for(
+ "users.get_user_list_xml", scodoc_dept=g.scodoc_dept
+ )
+ + "?",
+ "varname": "start",
+ "json": False,
+ "noresults": "Valeur invalide !",
+ "timeout": 60000,
+ },
+ },
+ )
+ )
+ if nbmod == 0:
+ modform.append(
+ (
+ "sep",
+ {
+ "input_type": "separator",
+ "title": "aucun module dans cette formation !!!",
+ },
+ )
+ )
+ if edit:
+ submitlabel = "Modifier ce semestre"
+ else:
+ submitlabel = "Créer ce semestre de formation"
+ #
+ # Etapes:
+ if edit:
+ n = 1
+ for etape_vdi in sem["etapes"]:
+ initvalues["etape_apo" + str(n)] = etape_vdi.etape
+ initvalues["vdi_apo" + str(n)] = etape_vdi.vdi
+ n += 1
+ #
+ initvalues["gestion_compensation"] = initvalues.get("gestion_compensation", False)
+ if initvalues["gestion_compensation"]:
+ initvalues["gestion_compensation_lst"] = ["X"]
+ else:
+ initvalues["gestion_compensation_lst"] = []
+ if vals.get("tf_submitted", False) and "gestion_compensation_lst" not in vals:
+ vals["gestion_compensation_lst"] = []
+
+ initvalues["gestion_semestrielle"] = initvalues.get("gestion_semestrielle", False)
+ if initvalues["gestion_semestrielle"]:
+ initvalues["gestion_semestrielle_lst"] = ["X"]
+ else:
+ initvalues["gestion_semestrielle_lst"] = []
+ if vals.get("tf_submitted", False) and "gestion_semestrielle_lst" not in vals:
+ vals["gestion_semestrielle_lst"] = []
+
+ initvalues["bul_hide_xml"] = initvalues.get("bul_hide_xml", False)
+ if not initvalues["bul_hide_xml"]:
+ initvalues["bul_publish_xml_lst"] = ["X"]
+ else:
+ initvalues["bul_publish_xml_lst"] = []
+ if vals.get("tf_submitted", False) and "bul_publish_xml_lst" not in vals:
+ vals["bul_publish_xml_lst"] = []
+
+ #
+ tf = TrivialFormulator(
+ request.base_url,
+ vals,
+ modform,
+ submitlabel=submitlabel,
+ cancelbutton="Annuler",
+ top_buttons=True,
+ initvalues=initvalues,
+ )
+ msg = ""
+ if tf[0] == 1:
+ # check dates
+ if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
+ msg = '
Dates de début et fin incompatibles !
'
+ if (
+ formsemestre.modalite
+ != "EXT" # n'impose pas d'Apo pour les sem. extérieurs
+ and sco_preferences.get_preference("always_require_apo_sem_codes")
+ and not any(
+ [tf[2]["etape_apo" + str(n)] for n in range(0, scu.EDIT_NB_ETAPES + 1)]
+ )
+ ):
+ msg = '
"
+ else:
+ if tf[2]["gestion_compensation_lst"]:
+ tf[2]["gestion_compensation"] = True
+ else:
+ tf[2]["gestion_compensation"] = False
+ if tf[2]["gestion_semestrielle_lst"]:
+ tf[2]["gestion_semestrielle"] = True
+ else:
+ tf[2]["gestion_semestrielle"] = False
+ if tf[2]["bul_publish_xml_lst"]:
+ tf[2]["bul_hide_xml"] = False
+ else:
+ tf[2]["bul_hide_xml"] = True
+ # remap les identifiants de responsables:
+ for field in resp_fields:
+ tf[2][field] = User.get_user_id_from_nomplogin(tf[2][field])
+ tf[2]["responsables"] = []
+ for field in resp_fields:
+ if tf[2][field]:
+ tf[2]["responsables"].append(tf[2][field])
+ for module_id in tf[2]["tf-checked"]:
+ mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id])
+ if mod_resp_id is None:
+ # Si un module n'a pas de responsable (ou inconnu),
+ # l'affecte au 1er directeur des etudes:
+ mod_resp_id = tf[2]["responsable_id"]
+ tf[2][module_id] = mod_resp_id
+
+ # etapes:
+ tf[2]["etapes"] = []
+ if etapes: # menus => case supplementaire pour saisie manuelle, indicée 0
+ start_i = 0
+ else:
+ start_i = 1
+ for n in range(start_i, scu.EDIT_NB_ETAPES + 1):
+ tf[2]["etapes"].append(
+ ApoEtapeVDI(
+ etape=tf[2]["etape_apo" + str(n)], vdi=tf[2]["vdi_apo" + str(n)]
+ )
+ )
+ # Modules sélectionnés:
+ # (retire le "MI" du début du nom de champs)
+ module_ids_checked = [int(x[2:]) for x in tf[2]["tf-checked"]]
+ _formsemestre_check_ue_bonus_unicity(module_ids_checked)
+ if not edit:
+ if is_apc:
+ _formsemestre_check_module_list(
+ module_ids_checked, tf[2]["semestre_id"]
+ )
+ # création du semestre
+ formsemestre_id = sco_formsemestre.do_formsemestre_create(tf[2])
+ # création des modules
+ for module_id in module_ids_checked:
+ modargs = {
+ "module_id": module_id,
+ "formsemestre_id": formsemestre_id,
+ "responsable_id": tf[2][f"MI{module_id}"],
+ }
+ _ = sco_moduleimpl.do_moduleimpl_create(modargs)
+ else:
+ # Modification du semestre:
+ # on doit creer les modules nouvellement selectionnés
+ # modifier ceux à modifier, et DETRUIRE ceux qui ne sont plus selectionnés.
+ # Note: la destruction échouera s'il y a des objets dépendants
+ # (eg des évaluations définies)
+ module_ids_tocreate = [
+ x for x in module_ids_checked if not x in module_ids_existing
+ ]
+ if is_apc:
+ _formsemestre_check_module_list(
+ module_ids_tocreate, tf[2]["semestre_id"]
+ )
+ # modules existants à modifier
+ module_ids_toedit = [
+ x for x in module_ids_checked if x in module_ids_existing
+ ]
+ # modules à détruire
+ module_ids_todelete = [
+ x for x in module_ids_existing if not x in module_ids_checked
+ ]
+ #
+ sco_formsemestre.do_formsemestre_edit(tf[2])
+ #
+ msg = []
+ for module_id in module_ids_tocreate:
+ modargs = {
+ "module_id": module_id,
+ "formsemestre_id": formsemestre_id,
+ "responsable_id": tf[2]["MI" + str(module_id)],
+ }
+ moduleimpl_id = sco_moduleimpl.do_moduleimpl_create(modargs)
+ mod = sco_edit_module.module_list({"module_id": module_id})[0]
+ msg += [
+ "création de %s (%s)" % (mod["code"] or "?", mod["titre"] or "?")
+ ]
+ # INSCRIPTIONS DES ETUDIANTS
+ log(
+ 'inscription module: %s = "%s"'
+ % ("%s!group_id" % module_id, tf[2]["%s!group_id" % module_id])
+ )
+ group_id = tf[2]["%s!group_id" % module_id]
+ if group_id:
+ etudids = [
+ x["etudid"] for x in sco_groups.get_group_members(group_id)
+ ]
+ log(
+ "inscription module:module_id=%s,moduleimpl_id=%s: %s"
+ % (module_id, moduleimpl_id, etudids)
+ )
+ sco_moduleimpl.do_moduleimpl_inscrit_etuds(
+ moduleimpl_id,
+ formsemestre_id,
+ etudids,
+ )
+ msg += [
+ "inscription de %d étudiants au module %s"
+ % (len(etudids), mod["code"] or "(module sans code)")
+ ]
+ else:
+ log(
+ "inscription module:module_id=%s,moduleimpl_id=%s: aucun etudiant inscrit"
+ % (module_id, moduleimpl_id)
+ )
+ #
+ ok, diag = formsemestre_delete_moduleimpls(
+ formsemestre_id, module_ids_todelete
+ )
+ msg += diag
+ for module_id in module_ids_toedit:
+ moduleimpl_id = sco_moduleimpl.moduleimpl_list(
+ formsemestre_id=formsemestre_id, module_id=module_id
+ )[0]["moduleimpl_id"]
+ modargs = {
+ "moduleimpl_id": moduleimpl_id,
+ "module_id": module_id,
+ "formsemestre_id": formsemestre_id,
+ "responsable_id": tf[2]["MI" + str(module_id)],
+ }
+ sco_moduleimpl.do_moduleimpl_edit(
+ modargs, formsemestre_id=formsemestre_id
+ )
+ mod = sco_edit_module.module_list({"module_id": module_id})[0]
+ # --- Association des parcours
+ formsemestre = FormSemestre.query.get(formsemestre_id)
+ if "parcours" in tf[2]:
+ formsemestre.parcours = [
+ ApcParcours.query.get(int(parcour_id_str))
+ for parcour_id_str in tf[2]["parcours"]
+ ]
+ db.session.add(formsemestre)
+ db.session.commit()
+ # --- Crée ou met à jour les groupes de parcours BUT
+ formsemestre.setup_parcours_groups()
+ # --- Fin
+ if edit:
+ if msg:
+ msg_html = (
+ '
Attention !
'
+ + "
".join(msg)
+ + "
"
+ )
+ if ok:
+ msg_html += "
Modification effectuée
"
+ else:
+ msg_html += "
Modules non modifiés
"
+ msg_html += (
+ 'retour au tableau de bord'
+ % formsemestre_id
+ )
+ return msg_html
+ else:
+ return flask.redirect(
+ "formsemestre_status?formsemestre_id=%s&head_message=Semestre modifié"
+ % formsemestre_id
+ )
+ else:
+ flash("Nouveau semestre créé")
+ return flask.redirect(
+ url_for(
+ "notes.formsemestre_status",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=formsemestre_id,
+ )
+ )
+
+
+def _formsemestre_check_module_list(module_ids, semestre_idx):
+ """En APC: Vérifie que tous les modules de la liste
+ sont dans le semestre indiqué.
+ Sinon, raise ScoValueError.
+ """
+ # vérification de la cohérence / modules / semestre
+ mod_sems_idx = {
+ Module.query.get_or_404(module_id).ue.semestre_idx for module_id in module_ids
+ }
+ if mod_sems_idx and mod_sems_idx != {semestre_idx}:
+ modules = [Module.query.get_or_404(module_id) for module_id in module_ids]
+ log(
+ f"""_formsemestre_check_module_list:
+ {chr(10).join( str(module) + " " + str(module.ue) for module in modules )}
+ """
+ )
+ for module in modules:
+ log(
+ f"{module.code}\tsemestre_id={module.semestre_id}\tue.semestre_idx={module.ue.semestre_idx}"
+ )
+ raise ScoValueError(
+ f"Les modules sélectionnés ne sont pas tous dans le semestre choisi (S{semestre_idx}) !",
+ dest_url="javascript:history.back();",
+ )
+
+
+def _formsemestre_check_ue_bonus_unicity(module_ids):
+ """Vérifie qu'il n'y a qu'une seule UE bonus associée aux modules choisis"""
+ ues = [Module.query.get_or_404(module_id).ue for module_id in module_ids]
+ ues_bonus = {ue.id for ue in ues if ue.type == sco_codes_parcours.UE_SPORT}
+ if len(ues_bonus) > 1:
+ raise ScoValueError(
+ """Les modules de bonus sélectionnés ne sont pas tous dans la même UE bonus.
+ Changez la sélection ou modifiez la structure du programme de formation.""",
+ dest_url="javascript:history.back();",
+ )
+
+
+def formsemestre_delete_moduleimpls(formsemestre_id, module_ids_to_del):
+ """Delete moduleimpls
+ module_ids_to_del: list of module_id (warning: not moduleimpl)
+ Moduleimpls must have no associated evaluations.
+ """
+ ok = True
+ msg = []
+ for module_id in module_ids_to_del:
+ module = Module.query.get(module_id)
+ if module is None:
+ continue # ignore invalid ids
+ modimpls = ModuleImpl.query.filter_by(
+ formsemestre_id=formsemestre_id, module_id=module_id
+ )
+ for modimpl in modimpls:
+ nb_evals = modimpl.evaluations.count()
+ if nb_evals > 0:
+ msg += [
+ f"""impossible de supprimer {module.code} ({module.titre or ""})
+ car il y a {nb_evals} évaluations définies
+ (supprimez-les d\'abord)"""
+ ]
+ ok = False
+ else:
+ msg += [f"""suppression de {module.code} ({module.titre or ""})"""]
+ db.session.delete(modimpl)
+ if ok:
+ db.session.commit()
+ else:
+ db.session.rollback()
+ return ok, msg
+
+
+def formsemestre_clone(formsemestre_id):
+ """
+ Formulaire clonage d'un semestre
+ """
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ # Liste des enseignants avec forme pour affichage / saisie avec suggestion
+ user_list = sco_users.get_user_list()
+ uid2display = {} # user_name : forme pour affichage = "NOM Prenom (login)"
+ for u in user_list:
+ uid2display[u.id] = u.get_nomplogin()
+ allowed_user_names = list(uid2display.values()) + [""]
+
+ initvalues = {
+ "formsemestre_id": sem["formsemestre_id"],
+ "responsable_id": uid2display.get(
+ sem["responsables"][0], sem["responsables"][0]
+ ),
+ }
+
+ H = [
+ html_sco_header.html_sem_header(
+ "Copie du semestre",
+ javascripts=["libjs/AutoSuggest.js"],
+ cssstyles=["css/autosuggest_inquisitor.css"],
+ bodyOnLoad="init_tf_form('')",
+ ),
+ """
Cette opération duplique un semestre: on reprend les mêmes modules et responsables. Aucun étudiant n'est inscrit.
""",
+ ]
+
+ descr = [
+ ("formsemestre_id", {"input_type": "hidden"}),
+ (
+ "date_debut",
+ {
+ "title": "Date de début", # j/m/a
+ "input_type": "datedmy",
+ "explanation": "j/m/a",
+ "size": 9,
+ "allow_null": False,
+ },
+ ),
+ (
+ "date_fin",
+ {
+ "title": "Date de fin", # j/m/a
+ "input_type": "datedmy",
+ "explanation": "j/m/a",
+ "size": 9,
+ "allow_null": False,
+ },
+ ),
+ (
+ "responsable_id",
+ {
+ "input_type": "text_suggest",
+ "size": 50,
+ "title": "Directeur des études",
+ "explanation": "taper le début du nom et choisir dans le menu",
+ "allowed_values": allowed_user_names,
+ "allow_null": False,
+ "text_suggest_options": {
+ "script": url_for(
+ "users.get_user_list_xml", scodoc_dept=g.scodoc_dept
+ )
+ + "?",
+ "varname": "start",
+ "json": False,
+ "noresults": "Valeur invalide !",
+ "timeout": 60000,
+ },
+ },
+ ),
+ (
+ "clone_evaluations",
+ {
+ "title": "Copier aussi les évaluations",
+ "input_type": "boolcheckbox",
+ "explanation": "copie toutes les évaluations, sans les dates (ni les notes!)",
+ },
+ ),
+ (
+ "clone_partitions",
+ {
+ "title": "Copier aussi les partitions",
+ "input_type": "boolcheckbox",
+ "explanation": "copie toutes les partitions (sans les étudiants!)",
+ },
+ ),
+ ]
+ tf = TrivialFormulator(
+ request.base_url,
+ scu.get_request_args(),
+ descr,
+ submitlabel="Dupliquer ce semestre",
+ cancelbutton="Annuler",
+ initvalues=initvalues,
+ )
+ msg = ""
+ if tf[0] == 1:
+ # check dates
+ if ndb.DateDMYtoISO(tf[2]["date_debut"]) > ndb.DateDMYtoISO(tf[2]["date_fin"]):
+ msg = '
Dates de début et fin incompatibles !
'
+ if tf[0] == 0 or msg:
+ return "".join(H) + msg + tf[1] + html_sco_header.sco_footer()
+ elif tf[0] == -1: # cancel
+ return flask.redirect(
+ "formsemestre_status?formsemestre_id=%s" % formsemestre_id
+ )
+ else:
+ new_formsemestre_id = do_formsemestre_clone(
+ formsemestre_id,
+ User.get_user_id_from_nomplogin(tf[2]["responsable_id"]),
+ tf[2]["date_debut"],
+ tf[2]["date_fin"],
+ clone_evaluations=tf[2]["clone_evaluations"],
+ clone_partitions=tf[2]["clone_partitions"],
+ )
+ return flask.redirect(
+ "formsemestre_status?formsemestre_id=%s&head_message=Nouveau%%20semestre%%20créé"
+ % new_formsemestre_id
+ )
+
+
+def do_formsemestre_clone(
+ orig_formsemestre_id,
+ responsable_id, # new resp.
+ date_debut,
+ date_fin, # 'dd/mm/yyyy'
+ clone_evaluations=False,
+ clone_partitions=False,
+):
+ """Clone a semestre: make copy, same modules, same options, same resps, same partitions.
+ New dates, responsable_id
+ """
+ log("cloning %s" % orig_formsemestre_id)
+ orig_sem = sco_formsemestre.get_formsemestre(orig_formsemestre_id)
+ cnx = ndb.GetDBConnexion()
+ # 1- create sem
+ args = orig_sem.copy()
+ del args["formsemestre_id"]
+ args["responsables"] = [responsable_id]
+ args["date_debut"] = date_debut
+ args["date_fin"] = date_fin
+ args["etat"] = 1 # non verrouillé
+ formsemestre_id = sco_formsemestre.do_formsemestre_create(args)
+ log("created formsemestre %s" % formsemestre_id)
+ # 2- create moduleimpls
+ mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
+ for mod_orig in mods_orig:
+ args = mod_orig.copy()
+ args["formsemestre_id"] = formsemestre_id
+ mid = sco_moduleimpl.do_moduleimpl_create(args)
+ # copy notes_modules_enseignants
+ ens = sco_moduleimpl.do_ens_list(
+ args={"moduleimpl_id": mod_orig["moduleimpl_id"]}
+ )
+ for e in ens:
+ args = e.copy()
+ args["moduleimpl_id"] = mid
+ sco_moduleimpl.do_ens_create(args)
+ # optionally, copy evaluations
+ if clone_evaluations:
+ for e in Evaluation.query.filter_by(
+ moduleimpl_id=mod_orig["moduleimpl_id"]
+ ):
+ # copie en enlevant la date
+ new_eval = e.clone(not_copying=("jour", "moduleimpl_id"))
+ new_eval.moduleimpl_id = mid
+ # Copie les poids APC de l'évaluation
+ new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
+ db.session.commit()
+
+ # 3- copy uecoefs
+ objs = sco_formsemestre.formsemestre_uecoef_list(
+ cnx, args={"formsemestre_id": orig_formsemestre_id}
+ )
+ for obj in objs:
+ args = obj.copy()
+ args["formsemestre_id"] = formsemestre_id
+ _ = sco_formsemestre.formsemestre_uecoef_create(cnx, args)
+
+ # NB: don't copy notes_formsemestre_custommenu (usually specific)
+
+ # 4- Copy new style preferences
+ prefs = sco_preferences.SemPreferences(orig_formsemestre_id)
+
+ if orig_formsemestre_id in prefs.base_prefs.prefs:
+ for pname in prefs.base_prefs.prefs[orig_formsemestre_id]:
+ if not prefs.is_global(pname):
+ pvalue = prefs[pname]
+ try:
+ prefs.base_prefs.set(formsemestre_id, pname, pvalue)
+ except ValueError:
+ log(
+ "do_formsemestre_clone: ignoring old preference %s=%s for %s"
+ % (pname, pvalue, formsemestre_id)
+ )
+
+ # 5- Copy formules utilisateur
+ objs = sco_compute_moy.formsemestre_ue_computation_expr_list(
+ cnx, args={"formsemestre_id": orig_formsemestre_id}
+ )
+ for obj in objs:
+ args = obj.copy()
+ args["formsemestre_id"] = formsemestre_id
+ _ = sco_compute_moy.formsemestre_ue_computation_expr_create(cnx, args)
+
+ # 5- Copy partitions and groups
+ if clone_partitions:
+ sco_groups_copy.clone_partitions_and_groups(
+ orig_formsemestre_id, formsemestre_id
+ )
+
+ return formsemestre_id
+
+
+# ---------------------------------------------------------------------------------------
+
+
+def formsemestre_associate_new_version(
+ formsemestre_id,
+ other_formsemestre_ids=[],
+ dialog_confirmed=False,
+):
+ """Formulaire changement formation d'un semestre"""
+ formsemestre_id = int(formsemestre_id)
+ other_formsemestre_ids = [int(x) for x in other_formsemestre_ids]
+ if not dialog_confirmed:
+ # dresse le liste des semestres de la meme formation et version
+ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ othersems = sco_formsemestre.do_formsemestre_list(
+ args={
+ "formation_id": formsemestre.formation.id,
+ "version": formsemestre.formation.version,
+ "etat": "1",
+ },
+ )
+ H = []
+ for s in othersems:
+ if (
+ s["formsemestre_id"] == formsemestre_id
+ or s["formsemestre_id"] in other_formsemestre_ids
+ ):
+ checked = 'checked="checked"'
+ else:
+ checked = ""
+ if s["formsemestre_id"] == formsemestre_id:
+ disabled = 'disabled="1"'
+ else:
+ disabled = ""
+ H.append(
+ f"""
{s['titremois']}
"""
+ )
+
+ return scu.confirm_dialog(
+ f"""
Associer à une nouvelle version de formation non verrouillée ?
+
Le programme pédagogique ("formation") va être dupliqué
+ pour que vous puissiez le modifier sans affecter les autres
+ semestres. Les autres paramètres (étudiants, notes...) du
+ semestre seront inchangés.
+
+
Veillez à ne pas abuser de cette possibilité, car créer
+ trop de versions de formations va vous compliquer la gestion
+ (à vous de garder trace des différences et à ne pas vous
+ tromper par la suite...).
+
+
Si vous souhaitez créer un programme pour de futurs semestres,
+ utilisez plutôt Créer une nouvelle version.
+
+
+
Si vous voulez associer aussi d'autres semestres à la nouvelle
+ version, cochez-les:
+
"""
+ + "".join(H)
+ + "
",
+ OK="Associer ces semestres à une nouvelle version",
+ dest_url="",
+ cancel_url=url_for(
+ "notes.formsemestre_status",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=formsemestre_id,
+ ),
+ parameters={"formsemestre_id": formsemestre_id},
+ )
+ else:
+ do_formsemestres_associate_new_version(
+ [formsemestre_id] + other_formsemestre_ids
+ )
+ flash("Semestre associé à une nouvelle version de la formation")
+ return flask.redirect(
+ url_for(
+ "notes.formsemestre_status",
+ scodoc_dept=g.scodoc_dept,
+ formsemestre_id=formsemestre_id,
+ )
+ )
+
+
+def do_formsemestres_associate_new_version(formsemestre_ids):
+ """Cree une nouvelle version de la formation du semestre, et y rattache les semestres.
+ Tous les moduleimpl sont ré-associés à la nouvelle formation, ainsi que les decisions de jury
+ si elles existent (codes d'UE validées).
+ Les semestre doivent tous appartenir à la meme version de la formation
+ """
+ log(f"do_formsemestres_associate_new_version {formsemestre_ids}")
+ if not formsemestre_ids:
+ return
+ # Check: tous de la même formation
+ assert isinstance(formsemestre_ids[0], int)
+ sem = sco_formsemestre.get_formsemestre(formsemestre_ids[0])
+ formation_id = sem["formation_id"]
+ for formsemestre_id in formsemestre_ids[1:]:
+ assert isinstance(formsemestre_id, int)
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ if formation_id != sem["formation_id"]:
+ raise ScoValueError("les semestres ne sont pas tous de la même formation !")
+
+ cnx = ndb.GetDBConnexion()
+ # New formation:
+ (
+ formation_id,
+ modules_old2new,
+ ues_old2new,
+ ) = sco_formations.formation_create_new_version(formation_id, redirect=False)
+ # Log new ues:
+ for ue_id in ues_old2new:
+ ue = UniteEns.query.get(ue_id)
+ new_ue = UniteEns.query.get(ues_old2new[ue_id])
+ assert ue.semestre_idx == new_ue.semestre_idx
+ log(f"{ue} -> {new_ue}")
+ # Log new modules
+ for module_id in modules_old2new:
+ mod = Module.query.get(module_id)
+ new_mod = Module.query.get(modules_old2new[module_id])
+ assert mod.semestre_id == new_mod.semestre_id
+ log(f"{mod} -> {new_mod}")
+ # re-associate
+ for formsemestre_id in formsemestre_ids:
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ sem["formation_id"] = formation_id
+ sco_formsemestre.do_formsemestre_edit(sem, cnx=cnx, html_quote=False)
+ _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new)
+
+ cnx.commit()
+
+
+def _reassociate_moduleimpls(cnx, formsemestre_id, ues_old2new, modules_old2new):
+ """Associe les moduleimpls d'un semestre existant à un autre programme
+ et met à jour les décisions de jury (validations d'UE).
+ """
+ # re-associate moduleimpls to new modules:
+ modimpls = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
+ for mod in modimpls:
+ mod["module_id"] = modules_old2new[mod["module_id"]]
+ sco_moduleimpl.do_moduleimpl_edit(mod, formsemestre_id=formsemestre_id)
+ # Update poids des évaluations
+ # les poids associent les évaluations aux UE (qui ont changé d'id)
+ for poids in EvaluationUEPoids.query.filter(
+ EvaluationUEPoids.evaluation_id == Evaluation.id,
+ Evaluation.moduleimpl_id == ModuleImpl.id,
+ ModuleImpl.formsemestre_id == formsemestre_id,
+ ):
+ poids.ue_id = ues_old2new[poids.ue_id]
+ db.session.add(poids)
+ db.session.commit()
+
+ # update decisions:
+ events = sco_etud.scolar_events_list(cnx, args={"formsemestre_id": formsemestre_id})
+ for e in events:
+ if e["ue_id"]:
+ e["ue_id"] = ues_old2new[e["ue_id"]]
+ sco_etud.scolar_events_edit(cnx, e)
+ validations = sco_cursus_dut.scolar_formsemestre_validation_list(
+ cnx, args={"formsemestre_id": formsemestre_id}
+ )
+ for e in validations:
+ if e["ue_id"]:
+ e["ue_id"] = ues_old2new[e["ue_id"]]
+ # log('e=%s' % e )
+ sco_cursus_dut.scolar_formsemestre_validation_edit(cnx, e)
+
+
+def formsemestre_delete(formsemestre_id):
+ """Delete a formsemestre (affiche avertissements)"""
+ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id)
+ H = [
+ html_sco_header.html_sem_header("Suppression du semestre"),
+ """
Attention !
+
A n'utiliser qu'en cas d'erreur lors de la saisie d'une formation. Normalement,
+un semestre ne doit jamais être supprimé
+(on perd la mémoire des notes et de tous les événements liés à ce semestre !).
+
+
+
Tous les modules de ce semestre seront supprimés.
+Ceci n'est possible que si :
+
+
+
aucune décision de jury n'a été entrée dans ce semestre;
+
et aucun étudiant de ce semestre ne le compense avec un autre semestre.
""",
+ dest_url="",
+ cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
+ parameters={"formsemestre_id": formsemestre_id},
+ )
+ # Bon, s'il le faut...
+ do_formsemestre_delete(formsemestre_id)
+ return flask.redirect(scu.ScoURL() + "?head_message=Semestre%20supprimé")
+
+
+def formsemestre_has_decisions_or_compensations(formsemestre_id):
+ """True if decision de jury dans ce semestre
+ ou bien compensation de ce semestre par d'autre ssemestres.
+ """
+ r = ndb.SimpleDictFetch(
+ """SELECT v.id AS formsemestre_validation_id, v.*
+ FROM scolar_formsemestre_validation v
+ WHERE v.formsemestre_id = %(formsemestre_id)s
+ OR v.compense_formsemestre_id = %(formsemestre_id)s""",
+ {"formsemestre_id": formsemestre_id},
+ )
+ return r
+
+
+def do_formsemestre_delete(formsemestre_id):
+ """delete formsemestre, and all its moduleimpls.
+ No checks, no warnings: erase all !
+ """
+ cnx = ndb.GetDBConnexion()
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+
+ sco_cache.EvaluationCache.invalidate_sem(formsemestre_id)
+
+ # --- Destruction des modules de ce semestre
+ mods = sco_moduleimpl.moduleimpl_list(formsemestre_id=formsemestre_id)
+ for mod in mods:
+ # evaluations
+ evals = sco_evaluation_db.do_evaluation_list(
+ args={"moduleimpl_id": mod["moduleimpl_id"]}
+ )
+ for e in evals:
+ ndb.SimpleQuery(
+ "DELETE FROM notes_notes WHERE evaluation_id=%(evaluation_id)s",
+ e,
+ )
+ ndb.SimpleQuery(
+ "DELETE FROM notes_notes_log WHERE evaluation_id=%(evaluation_id)s",
+ e,
+ )
+ ndb.SimpleQuery(
+ "DELETE FROM notes_evaluation WHERE id=%(evaluation_id)s",
+ e,
+ )
+
+ sco_moduleimpl.do_moduleimpl_delete(
+ mod["moduleimpl_id"], formsemestre_id=formsemestre_id
+ )
+ # --- Desinscription des etudiants
+ cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
+ req = "DELETE FROM notes_formsemestre_inscription WHERE formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Suppression des evenements
+ req = "DELETE FROM scolar_events WHERE formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Suppression des appreciations
+ req = "DELETE FROM notes_appreciations WHERE formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Supression des validations (!!!)
+ req = "DELETE FROM scolar_formsemestre_validation WHERE formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Supression des references a ce semestre dans les compensations:
+ req = "UPDATE scolar_formsemestre_validation SET compense_formsemestre_id=NULL WHERE compense_formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Suppression des autorisations
+ req = "DELETE FROM scolar_autorisation_inscription WHERE origin_formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Suppression des coefs d'UE capitalisées
+ req = "DELETE FROM notes_formsemestre_uecoef WHERE formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Suppression des item du menu custom
+ req = "DELETE FROM notes_formsemestre_custommenu WHERE formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Suppression des formules
+ req = "DELETE FROM notes_formsemestre_ue_computation_expr WHERE formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Suppression des preferences
+ req = "DELETE FROM sco_prefs WHERE formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Suppression des groupes et partitions
+ req = """DELETE FROM group_membership
+ WHERE group_id IN
+ (SELECT gm.group_id FROM group_membership gm, partition p, group_descr gd
+ WHERE gm.group_id = gd.id AND gd.partition_id = p.id
+ AND p.formsemestre_id=%(formsemestre_id)s)
+ """
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ req = """DELETE FROM group_descr
+ WHERE id IN
+ (SELECT gd.id FROM group_descr gd, partition p
+ WHERE gd.partition_id = p.id
+ AND p.formsemestre_id=%(formsemestre_id)s)
+ """
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ req = "DELETE FROM partition WHERE formsemestre_id=%(formsemestre_id)s"
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Responsables
+ req = """DELETE FROM notes_formsemestre_responsables
+ WHERE formsemestre_id=%(formsemestre_id)s"""
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+ # --- Etapes
+ req = """DELETE FROM notes_formsemestre_etapes
+ WHERE formsemestre_id=%(formsemestre_id)s"""
+ cursor.execute(req, {"formsemestre_id": formsemestre_id})
+
+ # --- Destruction du semestre
+ sco_formsemestre._formsemestreEditor.delete(cnx, formsemestre_id)
+
+ # news
+ ScolarNews.add(
+ typ=ScolarNews.NEWS_SEM,
+ obj=formsemestre_id,
+ text="Suppression du semestre %(titre)s" % sem,
+ )
+
+
+# ---------------------------------------------------------------------------------------
+def formsemestre_edit_options(formsemestre_id):
+ """dialog to change formsemestre options
+ (accessible par ScoImplement ou dir. etudes)
+ """
+ log("formsemestre_edit_options")
+ ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
+ if not ok:
+ return err
+ return sco_preferences.SemPreferences(formsemestre_id).edit(categories=["bul"])
+
+
+def formsemestre_change_lock(formsemestre_id) -> None:
+ """Change etat (verrouille si ouvert, déverrouille si fermé)
+ nota: etat (1 ouvert, 0 fermé)
+ """
+ ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
+ if not ok:
+ return err
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ etat = not sem["etat"]
+
+ args = {"formsemestre_id": formsemestre_id, "etat": etat}
+ sco_formsemestre.do_formsemestre_edit(args)
+
+
+def formsemestre_change_publication_bul(
+ formsemestre_id, dialog_confirmed=False, redirect=True
+):
+ """Change etat publication bulletins sur portail"""
+ ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
+ if not ok:
+ return err
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+ etat = not sem["bul_hide_xml"]
+
+ if not dialog_confirmed:
+ if etat:
+ msg = "non"
+ else:
+ msg = ""
+ return scu.confirm_dialog(
+ "
Confirmer la %s publication des bulletins ?
" % msg,
+ helpmsg="""Il est parfois utile de désactiver la diffusion des bulletins,
+ par exemple pendant la tenue d'un jury ou avant harmonisation des notes.
+
+ Ce réglage n'a d'effet que si votre établissement a interfacé ScoDoc et un portail étudiant.
+ """,
+ dest_url="",
+ cancel_url="formsemestre_status?formsemestre_id=%s" % formsemestre_id,
+ parameters={"bul_hide_xml": etat, "formsemestre_id": formsemestre_id},
+ )
+
+ args = {"formsemestre_id": formsemestre_id, "bul_hide_xml": etat}
+ sco_formsemestre.do_formsemestre_edit(args)
+ if redirect:
+ return flask.redirect(
+ "formsemestre_status?formsemestre_id=%s" % formsemestre_id
+ )
+ return None
+
+
+def formsemestre_edit_uecoefs(formsemestre_id, err_ue_id=None):
+ """Changement manuel des coefficients des UE capitalisées."""
+ from app.scodoc import notes_table
+
+ ok, err = sco_permissions_check.check_access_diretud(formsemestre_id)
+ if not ok:
+ return err
+ sem = sco_formsemestre.get_formsemestre(formsemestre_id)
+
+ footer = html_sco_header.sco_footer()
+ help = """
+ Seuls les modules ont un coefficient. Cependant, il est nécessaire d'affecter un coefficient aux UE capitalisée pour pouvoir les prendre en compte dans la moyenne générale.
+
+
ScoDoc calcule normalement le coefficient d'une UE comme la somme des
+ coefficients des modules qui la composent.
+
+
Dans certains cas, on n'a pas les mêmes modules dans le semestre antérieur
+ (capitalisé) et dans le semestre courant, et le coefficient d'UE est alors variable.
+ Il est alors possible de forcer la valeur du coefficient d'UE.
+
+
+ Indiquez "auto" (ou laisser vide) pour que ScoDoc calcule automatiquement le coefficient,
+ ou bien entrez une valeur (nombre réel).
+
+
Dans le doute, si le mode auto n'est pas applicable et que tous les étudiants sont inscrits aux mêmes modules de ce semestre, prenez comme coefficient la somme indiquée.
+ Sinon, référez vous au programme pédagogique. Les lignes en rouge
+ sont à changer.
+
+
Les coefficients indiqués ici ne s'appliquent que pour le traitement des UE capitalisées.
+