2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2023-12-31 23:04:06 +01:00
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
2020-09-26 16:19:37 +02:00
#
# 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
#
##############################################################################
""" Ajout/Modification/Supression formations
( portage from DTML )
"""
2021-08-01 10:16:16 +02:00
import flask
2024-08-24 08:06:46 +02:00
from flask import flash , g , url_for , render_template , request
2023-02-20 16:25:23 +01:00
import sqlalchemy
2021-08-01 10:16:16 +02:00
2021-12-09 11:52:46 +01:00
from app import db
2024-10-10 00:41:20 +02:00
from app . formations import edit_ue
2021-12-29 19:30:49 +01:00
from app . models import SHORT_STR_LEN
2021-12-16 16:27:35 +01:00
from app . models . formations import Formation
2021-12-09 11:52:46 +01:00
from app . models . modules import Module
2022-01-06 21:23:29 +01:00
from app . models . ues import UniteEns
2022-04-12 17:12:51 +02:00
from app . models import ScolarNews
2021-12-09 11:52:46 +01:00
2021-06-19 23:21:37 +02:00
import app . scodoc . sco_utils as scu
from app . scodoc . TrivialFormulator import TrivialFormulator , tf_error_message
2022-07-13 18:52:07 +02:00
from app . scodoc . sco_exceptions import ScoValueError , ScoNonEmptyFormationObject
2021-07-19 19:53:01 +02:00
2022-10-23 23:28:24 +02:00
from app . scodoc import sco_cache
2023-02-12 13:36:47 +01:00
from app . scodoc import codes_cursus
2020-09-26 16:19:37 +02:00
2021-09-24 20:35:50 +02:00
def formation_delete ( formation_id = None , dialog_confirmed = False ) :
2021-01-01 18:40:47 +01:00
""" Delete a formation """
2024-10-29 19:18:36 +01:00
formation : Formation = Formation . get_or_404 ( formation_id )
2020-09-26 16:19:37 +02:00
H = [
2023-02-18 00:13:00 +01:00
f """ <h2>Suppression de la formation { formation . titre } ( { formation . acronyme } )</h2> """ ,
2020-09-26 16:19:37 +02:00
]
2024-04-08 18:56:43 +02:00
formsemestres = formation . formsemestres . all ( )
if formsemestres :
2020-09-26 16:19:37 +02:00
H . append (
2022-06-08 17:42:52 +02:00
""" <p class= " warning " >Impossible de supprimer cette formation,
car les sessions suivantes l ' utilisent:</p>
< ul > """
2020-09-26 16:19:37 +02:00
)
2024-04-08 18:56:43 +02:00
for formsemestre in formsemestres :
H . append ( f """ <li> { formsemestre . html_link_status ( ) } </li> """ )
2023-02-23 21:19:57 +01:00
H . append (
2024-04-08 18:56:43 +02:00
f """ </ul>
< p > < a class = " stdlink " href = " {
url_for ( " notes.index_html " , scodoc_dept = g . scodoc_dept )
} " >Revenir</a></p> " " "
2023-02-23 21:19:57 +01:00
)
2020-09-26 16:19:37 +02:00
else :
if not dialog_confirmed :
2021-06-02 22:40:34 +02:00
return scu . confirm_dialog (
2023-02-18 00:13:00 +01:00
f """ <h2>Confirmer la suppression de la formation
{ formation . titre } ( { formation . acronyme } ) ?
< / h2 >
2023-11-19 22:06:36 +01:00
< p > < b > Attention : < / b > la suppression d ' une formation est <b>irréversible</b>
2023-02-18 00:13:00 +01:00
et implique la supression de toutes les UE , matières et modules de la formation !
< / p >
""" ,
2020-09-26 16:19:37 +02:00
OK = " Supprimer cette formation " ,
2024-04-08 18:56:43 +02:00
cancel_url = url_for ( " notes.index_html " , scodoc_dept = g . scodoc_dept ) ,
2020-09-26 16:19:37 +02:00
parameters = { " formation_id " : formation_id } ,
2024-08-24 08:06:46 +02:00
template = " sco_page_dept.j2 " ,
2020-09-26 16:19:37 +02:00
)
else :
2023-02-18 00:13:00 +01:00
do_formation_delete ( formation_id )
2020-09-26 16:19:37 +02:00
H . append (
2023-02-18 00:13:00 +01:00
f """ <p>OK, formation supprimée.</p>
2024-04-08 18:56:43 +02:00
< p > < a class = " stdlink " href = " {
url_for ( " notes.index_html " , scodoc_dept = g . scodoc_dept )
} " >continuer</a></p> " " "
2020-09-26 16:19:37 +02:00
)
2024-08-24 08:06:46 +02:00
return render_template (
2024-08-24 15:27:56 +02:00
" sco_page_dept.j2 " , content = " \n " . join ( H ) , title = " Suppression d ' une formation "
2024-08-24 08:06:46 +02:00
)
2020-09-26 16:19:37 +02:00
2022-07-13 18:52:07 +02:00
def do_formation_delete ( formation_id ) :
2021-06-16 18:18:32 +02:00
""" delete a formation (and all its UE, matieres, modules)
2022-07-13 18:52:07 +02:00
Warning : delete all ues , will ask if there are validations !
2021-06-16 18:18:32 +02:00
"""
2023-07-11 06:57:38 +02:00
formation : Formation = db . session . get ( Formation , formation_id )
2022-07-13 18:52:07 +02:00
if formation is None :
return
acronyme = formation . acronyme
if formation . formsemestres . count ( ) :
raise ScoNonEmptyFormationObject (
type_objet = " formation " ,
msg = formation . titre ,
dest_url = url_for (
" notes.ue_table " , scodoc_dept = g . scodoc_dept , formation_id = formation . id
) ,
)
2022-10-23 23:28:24 +02:00
with sco_cache . DeferredSemCacheManager ( ) :
# Suppression des modules
for module in formation . modules :
db . session . delete ( module )
db . session . flush ( )
# Suppression des UEs
for ue in formation . ues :
2024-10-10 00:41:20 +02:00
edit_ue . do_ue_delete ( ue , force = True )
2021-06-16 18:18:32 +02:00
2022-10-23 23:28:24 +02:00
db . session . delete ( formation )
2021-06-16 18:18:32 +02:00
# news
2022-04-12 17:12:51 +02:00
ScolarNews . add (
typ = ScolarNews . NEWS_FORM ,
2022-07-13 18:52:07 +02:00
obj = formation_id ,
text = f " Suppression de la formation { acronyme } " ,
2023-06-23 10:37:32 +02:00
max_frequency = 0 ,
2021-06-16 18:18:32 +02:00
)
2021-09-27 10:20:10 +02:00
def formation_create ( ) :
2021-01-01 18:40:47 +01:00
""" Creation d ' une formation """
2021-09-27 10:20:10 +02:00
return formation_edit ( create = True )
2020-09-26 16:19:37 +02:00
2021-09-27 10:20:10 +02:00
def formation_edit ( formation_id = None , create = False ) :
2021-01-01 18:40:47 +01:00
""" Edit or create a formation """
2020-09-26 16:19:37 +02:00
if create :
H = [
""" <h2>Création d ' une formation</h2>
< p class = " help " > Une " formation " décrit une filière , comme un DUT ou une Licence . La formation se subdivise en unités pédagogiques ( UE , matières , modules ) . Elle peut se diviser en plusieurs semestres ( ou sessions ) , qui seront mis en place séparément .
< / p >
< p > Le < tt > titre < / tt > est le nom complet , parfois adapté pour mieux distinguer les modalités ou versions de programme pédagogique . Le < tt > titre_officiel < / tt > est le nom complet du diplôme , qui apparaitra sur certains PV de jury de délivrance du diplôme .
< / p >
""" ,
]
submitlabel = " Créer cette formation "
2023-02-12 13:36:47 +01:00
initvalues = { " type_parcours " : codes_cursus . DEFAULT_TYPE_CURSUS }
2020-09-26 16:19:37 +02:00
is_locked = False
else :
# edit an existing formation
2024-10-29 19:18:36 +01:00
formation : Formation = Formation . get_or_404 ( formation_id )
2023-02-17 22:07:03 +01:00
form_dict = formation . to_dict ( )
form_dict [ " commentaire " ] = form_dict [ " commentaire " ] or " "
2023-02-20 13:37:16 +01:00
initvalues = form_dict
2023-02-17 22:07:03 +01:00
is_locked = formation . has_locked_sems ( formation_id )
2020-09-26 16:19:37 +02:00
submitlabel = " Modifier les valeurs "
H = [
2023-02-20 16:25:23 +01:00
f """ <h2>Modification de la formation { formation . acronyme }
version { formation . version }
< / h2 > """ ,
2020-09-26 16:19:37 +02:00
]
if is_locked :
H . append (
' <p class= " warning " >Attention: Formation verrouillée, le type de parcours ne peut être modifié.</p> '
)
tf = TrivialFormulator (
2021-09-18 10:10:02 +02:00
request . base_url ,
2021-09-27 16:42:14 +02:00
scu . get_request_args ( ) ,
2020-09-26 16:19:37 +02:00
(
( " formation_id " , { " default " : formation_id , " input_type " : " hidden " } ) ,
(
" acronyme " ,
{
" size " : 12 ,
2024-07-19 10:11:02 +02:00
" explanation " : " identifiant de la formation (par ex. BUT R&T) " ,
2020-09-26 16:19:37 +02:00
" allow_null " : False ,
} ,
) ,
(
" titre " ,
{
" size " : 80 ,
2024-07-19 10:11:02 +02:00
" explanation " : " nom de la formation (ex: BUT Réseaux et Télécommunications) " ,
2020-09-26 16:19:37 +02:00
" allow_null " : False ,
} ,
) ,
(
" titre_officiel " ,
{
" size " : 80 ,
" explanation " : " nom officiel (pour les PV de jury) " ,
" allow_null " : False ,
} ,
) ,
(
" type_parcours " ,
{
" input_type " : " menu " ,
" title " : " Type de parcours " ,
" type " : " int " ,
2023-02-12 13:36:47 +01:00
" allowed_values " : codes_cursus . FORMATION_CURSUS_TYPES ,
" labels " : codes_cursus . FORMATION_CURSUS_DESCRS ,
2020-09-26 16:19:37 +02:00
" explanation " : " détermine notamment le nombre de semestres et les règles de validation d ' UE et de semestres (barres) " ,
" readonly " : is_locked ,
} ,
) ,
(
" formation_code " ,
{
" size " : 12 ,
" title " : " Code formation " ,
" explanation " : " code interne. Toutes les formations partageant le même code sont compatibles (compensation de semestres, capitalisation d ' UE). Laisser vide si vous ne savez pas, ou entrer le code d ' une formation existante. " ,
2021-12-29 19:30:49 +01:00
" validator " : lambda val , _ : len ( val ) < SHORT_STR_LEN ,
2020-09-26 16:19:37 +02:00
} ,
) ,
(
" code_specialite " ,
{
" size " : 12 ,
" title " : " Code spécialité " ,
" explanation " : " optionel: code utilisé pour échanger avec d ' autres logiciels et identifiant la filière ou spécialité (exemple: ASUR). N ' est utilisé que s ' il n ' y a pas de numéro de semestre. " ,
} ,
) ,
2023-01-30 18:08:40 +01:00
(
" commentaire " ,
{
" input_type " : " textarea " ,
" rows " : 3 ,
" cols " : 77 ,
" title " : " Commentaire " ,
" explanation " : " commentaire libre. " ,
} ,
) ,
2020-09-26 16:19:37 +02:00
) ,
initvalues = initvalues ,
submitlabel = submitlabel ,
)
if tf [ 0 ] == 0 :
2024-08-24 18:26:51 +02:00
return render_template (
" sco_page_dept.j2 " ,
title = " Modification d ' une formation " ,
content = " \n " . join ( H ) + tf [ 1 ] ,
)
if tf [ 0 ] == - 1 :
2024-04-08 18:56:43 +02:00
return flask . redirect ( url_for ( " notes.index_html " , scodoc_dept = g . scodoc_dept ) )
2024-08-24 18:26:51 +02:00
# check unicity : constraint UNIQUE(acronyme,titre,version)
if create :
version = 1
2020-09-26 16:19:37 +02:00
else :
2024-08-24 18:26:51 +02:00
version = initvalues [ " version " ]
args = {
" acronyme " : tf [ 2 ] [ " acronyme " ] ,
" titre " : tf [ 2 ] [ " titre " ] ,
" version " : version ,
" dept_id " : g . scodoc_dept_id ,
}
other_formations : list [ Formation ] = Formation . query . filter_by ( * * args ) . all ( )
if other_formations and (
( len ( other_formations ) > 1 ) or other_formations [ 0 ] . id != formation_id
) :
return render_template (
" sco_page_dept.j2 " ,
title = " Modification d ' une formation " ,
content = (
2020-09-26 16:19:37 +02:00
" \n " . join ( H )
+ tf_error_message (
2022-05-26 03:55:03 +02:00
f """ Valeurs incorrectes: il existe déjà <a href= " {
2024-08-24 18:26:51 +02:00
url_for ( ' notes.ue_table ' ,
scodoc_dept = g . scodoc_dept , formation_id = other_formations [ 0 ] . id )
} " >une formation</a> avec même titre,
acronyme et version .
"""
2020-09-26 16:19:37 +02:00
)
+ tf [ 1 ]
2024-08-24 18:26:51 +02:00
) ,
2021-08-09 10:09:04 +02:00
)
2024-08-24 18:26:51 +02:00
#
if create :
formation = do_formation_create ( tf [ 2 ] )
else :
if do_formation_edit ( tf [ 2 ] ) :
flash (
f """ Modification de la formation {
formation . titre } ( { formation . acronyme } ) version { formation . version } """
)
return flask . redirect (
url_for ( " notes.ue_table " , scodoc_dept = g . scodoc_dept , formation_id = formation . id )
)
2020-09-26 16:19:37 +02:00
2023-02-20 21:04:29 +01:00
def do_formation_create ( args : dict ) - > Formation :
2021-06-12 22:43:22 +02:00
" create a formation "
2023-02-20 21:04:29 +01:00
formation = Formation (
dept_id = g . scodoc_dept_id ,
acronyme = args [ " acronyme " ] . strip ( ) ,
titre = args [ " titre " ] . strip ( ) ,
titre_officiel = args [ " titre_officiel " ] . strip ( ) ,
version = args . get ( " version " ) ,
2023-02-21 11:08:41 +01:00
commentaire = scu . strip_str ( args . get ( " commentaire " , " " ) ) or None ,
2023-02-20 21:04:29 +01:00
formation_code = args . get ( " formation_code " , " " ) . strip ( ) or None ,
type_parcours = args . get ( " type_parcours " ) ,
2023-02-21 11:08:41 +01:00
code_specialite = args . get ( " code_specialite " , " " ) . strip ( ) or None ,
2023-02-20 21:04:29 +01:00
referentiel_competence_id = args . get ( " referentiel_competence_id " ) ,
)
db . session . add ( formation )
try :
db . session . commit ( )
except sqlalchemy . exc . IntegrityError as exc :
db . session . rollback ( )
raise ScoValueError (
" On ne peut pas créer deux formations avec mêmes acronymes, titres et versions ! " ,
dest_url = url_for (
" notes.formation_edit " ,
scodoc_dept = g . scodoc_dept ,
formation_id = formation . id ,
) ,
2024-05-20 10:46:36 +02:00
safe = True ,
2023-02-20 21:04:29 +01:00
) from exc
2021-06-12 22:43:22 +02:00
2022-04-12 17:12:51 +02:00
ScolarNews . add (
typ = ScolarNews . NEWS_FORM ,
2023-02-20 21:04:29 +01:00
text = f """ Création de la formation {
formation . titre } ( { formation . acronyme } ) version { formation . version } """ ,
2023-06-23 10:37:32 +02:00
max_frequency = 0 ,
2021-06-12 22:43:22 +02:00
)
2023-02-20 21:04:29 +01:00
return formation
2021-06-12 22:43:22 +02:00
2023-11-19 22:06:36 +01:00
def do_formation_edit ( args ) - > bool :
" edit a formation, returns True if modified "
2020-09-26 16:19:37 +02:00
2022-07-08 23:58:27 +02:00
# On ne peut jamais supprimer le code formation:
if " formation_code " in args and not args [ " formation_code " ] :
del args [ " formation_code " ]
2024-10-29 19:18:36 +01:00
formation : Formation = Formation . get_or_404 ( args [ " formation_id " ] )
2020-09-26 16:19:37 +02:00
# On autorise la modif de la formation meme si elle est verrouillee
# car cela ne change que du cosmetique, (sauf eventuellement le code formation ?)
# mais si verrouillée on ne peut changer le type de parcours
2022-07-08 23:58:27 +02:00
if formation . has_locked_sems ( ) :
2021-07-09 17:47:06 +02:00
if " type_parcours " in args :
2020-09-26 16:19:37 +02:00
del args [ " type_parcours " ]
2023-11-19 22:06:36 +01:00
modified = False
2022-07-08 23:58:27 +02:00
for field in formation . __dict__ :
2023-02-20 16:25:23 +01:00
if field in args :
value = args [ field ] . strip ( ) if isinstance ( args [ field ] , str ) else args [ field ]
2023-11-19 22:06:36 +01:00
if field and field [ 0 ] != " _ " and getattr ( formation , field , None ) != value :
2023-02-20 16:25:23 +01:00
setattr ( formation , field , value )
2023-11-19 22:06:36 +01:00
modified = True
if not modified :
return False
2022-07-08 23:58:27 +02:00
db . session . add ( formation )
2023-02-20 16:25:23 +01:00
try :
db . session . commit ( )
except sqlalchemy . exc . IntegrityError as exc :
db . session . rollback ( )
raise ScoValueError (
" On ne peut pas créer deux formations avec mêmes acronymes, titres et versions ! " ,
dest_url = url_for (
" notes.formation_edit " ,
scodoc_dept = g . scodoc_dept ,
formation_id = formation . id ,
) ,
) from exc
2021-12-16 16:27:35 +01:00
formation . invalidate_cached_sems ( )
2023-11-19 22:06:36 +01:00
return True
2021-06-17 00:08:37 +02:00
2021-12-09 11:52:46 +01:00
def module_move ( module_id , after = 0 , redirect = True ) :
2021-06-17 00:08:37 +02:00
""" Move before/after previous one (decrement/increment numero) """
2021-12-09 11:52:46 +01:00
redirect = bool ( redirect )
2024-10-29 19:18:36 +01:00
module = Module . get_or_404 ( module_id )
2021-06-17 00:08:37 +02:00
after = int ( after ) # 0: deplace avant, 1 deplace apres
if after not in ( 0 , 1 ) :
2021-12-09 11:52:46 +01:00
raise ValueError ( f ' invalid value for " after " ( { after } ) ' )
if module . formation . is_apc ( ) :
# pas de matières, mais on prend tous les modules de même type de la formation
query = Module . query . filter_by (
semestre_id = module . semestre_id ,
formation = module . formation ,
module_type = module . module_type ,
)
else :
query = Module . query . filter_by ( matiere = module . matiere )
modules = query . order_by ( Module . numero , Module . code ) . all ( )
if len ( { o . numero for o in modules } ) != len ( modules ) :
# il y a des numeros identiques !
2021-12-17 13:42:39 +01:00
scu . objects_renumber ( db , modules )
2021-12-09 11:52:46 +01:00
if len ( modules ) > 1 :
idx = [ m . id for m in modules ] . index ( module . id )
2021-06-17 00:08:37 +02:00
neigh = None # object to swap with
if after == 0 and idx > 0 :
2021-12-09 11:52:46 +01:00
neigh = modules [ idx - 1 ]
elif after == 1 and idx < len ( modules ) - 1 :
neigh = modules [ idx + 1 ]
if neigh : # échange les numéros
module . numero , neigh . numero = neigh . numero , module . numero
db . session . add ( module )
db . session . add ( neigh )
db . session . commit ( )
2022-01-06 21:23:29 +01:00
module . formation . invalidate_cached_sems ( )
2024-02-14 21:45:58 +01:00
# redirect to ue_table page:
2021-06-17 00:08:37 +02:00
if redirect :
2021-08-09 10:09:04 +02:00
return flask . redirect (
url_for (
2021-12-09 11:52:46 +01:00
" notes.ue_table " ,
scodoc_dept = g . scodoc_dept ,
formation_id = module . formation . id ,
2021-12-17 14:30:38 +01:00
semestre_idx = module . ue . semestre_idx ,
2021-08-09 10:09:04 +02:00
)
)
2021-06-17 00:08:37 +02:00
2021-08-30 23:28:15 +02:00
def ue_move ( ue_id , after = 0 , redirect = 1 ) :
2021-06-17 00:08:37 +02:00
""" Move UE before/after previous one (decrement/increment numero) """
2024-10-29 19:18:36 +01:00
ue = UniteEns . get_or_404 ( ue_id )
2021-06-17 00:08:37 +02:00
redirect = int ( redirect )
after = int ( after ) # 0: deplace avant, 1 deplace apres
if after not in ( 0 , 1 ) :
raise ValueError ( ' invalid value for " after " ' )
2022-09-29 22:39:54 +02:00
others_q = ue . formation . ues . order_by ( UniteEns . numero )
if ue . formation . is_apc ( ) :
others_q = others_q . filter_by ( semestre_idx = ue . semestre_idx )
others = others_q . all ( )
2022-01-06 21:23:29 +01:00
if len ( { o . numero for o in others } ) != len ( others ) :
# il y a des numeros identiques !
scu . objects_renumber ( db , others )
2022-02-14 18:33:36 +01:00
ue . formation . invalidate_cached_sems ( )
2021-06-17 00:08:37 +02:00
if len ( others ) > 1 :
2022-01-06 21:23:29 +01:00
idx = [ u . id for u in others ] . index ( ue . id )
2021-06-17 00:08:37 +02:00
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
2022-01-06 21:23:29 +01:00
ue . numero , neigh . numero = neigh . numero , ue . numero
db . session . add ( ue )
db . session . add ( neigh )
db . session . commit ( )
ue . formation . invalidate_cached_sems ( )
2024-02-14 21:45:58 +01:00
# redirect to ue_table page
2021-06-17 00:08:37 +02:00
if redirect :
2021-08-09 10:09:04 +02:00
return flask . redirect (
url_for (
2021-10-17 23:19:26 +02:00
" notes.ue_table " ,
2021-08-09 10:09:04 +02:00
scodoc_dept = g . scodoc_dept ,
2022-01-06 21:23:29 +01:00
formation_id = ue . formation_id ,
semestre_idx = ue . semestre_idx ,
2021-08-09 10:09:04 +02:00
)
)