ScoDoc-Lille/app/scodoc/sco_formations.py

470 lines
17 KiB
Python
Raw Normal View History

2020-09-26 16:19:37 +02:00
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
2022-01-01 14:49:42 +01:00
# Copyright (c) 1999 - 2022 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
#
##############################################################################
"""Import / Export de formations
"""
from operator import itemgetter
2021-02-03 22:00:41 +01:00
import xml.dom.minidom
2020-09-26 16:19:37 +02:00
2021-08-01 10:16:16 +02:00
import flask
2022-07-13 19:23:55 +02:00
from flask import flash, g, url_for, request
from flask_login import current_user
from app.models.but_refcomp import ApcParcours
2021-08-01 10:16:16 +02:00
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import db
2021-08-29 19:57:32 +02:00
from app import log
from app.models import Formation, Module
2022-04-12 17:12:51 +02:00
from app.models import ScolarNews
from app.scodoc import sco_codes_parcours
2021-06-21 10:17:16 +02:00
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_formsemestre
from app.scodoc import sco_preferences
2021-06-21 10:17:16 +02:00
from app.scodoc import sco_tag_module
2021-07-13 09:38:31 +02:00
from app.scodoc import sco_xml
2021-08-21 17:07:44 +02:00
import sco_version
from app.scodoc.gen_tables import GenTable
2021-12-03 14:13:49 +01:00
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
from app.scodoc.sco_permissions import Permission
2020-09-26 16:19:37 +02:00
2021-06-13 18:29:53 +02:00
_formationEditor = ndb.EditableTable(
"notes_formations",
"formation_id",
(
"formation_id",
"acronyme",
"titre",
"titre_officiel",
"version",
"formation_code",
"type_parcours",
"code_specialite",
2022-07-13 19:23:55 +02:00
"referentiel_competence_id",
2021-06-13 18:29:53 +02:00
),
2021-08-13 00:34:58 +02:00
filter_dept=True,
2021-06-13 18:29:53 +02:00
sortkey="acronyme",
)
2020-09-26 16:19:37 +02:00
2021-08-19 10:28:35 +02:00
def formation_list(formation_id=None, args={}):
2021-06-16 18:18:32 +02:00
"""List formation(s) with given id, or matching args
(when args is given, formation_id is ignored).
"""
if not args:
if formation_id is None:
args = {}
else:
args = {"formation_id": formation_id}
cnx = ndb.GetDBConnexion()
r = _formationEditor.list(cnx, args=args)
# log('%d formations found' % len(r))
return r
def formation_has_locked_sems(formation_id): # XXX to remove
"backward compat: True if there is a locked formsemestre in this formation"
formation = Formation.query.get(formation_id)
2022-07-11 21:39:30 +02:00
if formation is None:
return False
return formation.has_locked_sems()
2021-06-16 18:18:32 +02:00
def formation_export(
formation_id,
export_ids=False,
export_tags=True,
export_external_ues=False,
format=None,
):
2020-09-26 16:19:37 +02:00
"""Get a formation, with UE, matieres, modules
in desired format
"""
2022-07-13 19:23:55 +02:00
formation: Formation = Formation.query.get_or_404(formation_id)
F = formation.to_dict()
selector = {"formation_id": formation_id}
if not export_external_ues:
selector["is_external"] = False
ues = sco_edit_ue.ue_list(selector)
2020-09-26 16:19:37 +02:00
F["ue"] = ues
for ue in ues:
ue_id = ue["ue_id"]
ue["reference"] = ue_id # pour les coefficients
2020-09-26 16:19:37 +02:00
if not export_ids:
del ue["id"]
2020-09-26 16:19:37 +02:00
del ue["ue_id"]
del ue["formation_id"]
if ue["ects"] is None:
del ue["ects"]
2021-10-17 23:19:26 +02:00
mats = sco_edit_matiere.matiere_list({"ue_id": ue_id})
2020-09-26 16:19:37 +02:00
ue["matiere"] = mats
for mat in mats:
matiere_id = mat["matiere_id"]
if not export_ids:
del mat["id"]
2020-09-26 16:19:37 +02:00
del mat["matiere_id"]
del mat["ue_id"]
2021-10-16 19:20:36 +02:00
mods = sco_edit_module.module_list({"matiere_id": matiere_id})
2020-09-26 16:19:37 +02:00
mat["module"] = mods
for mod in mods:
module_id = mod["module_id"]
2020-09-26 16:19:37 +02:00
if export_tags:
2021-08-20 01:09:55 +02:00
tags = sco_tag_module.module_tag_list(module_id=mod["module_id"])
2020-09-26 16:19:37 +02:00
if tags:
mod["tags"] = [{"name": x} for x in tags]
#
module = Module.query.get(module_id)
if module.is_apc():
# Exporte les coefficients
mod["coefficients"] = [
{"ue_reference": str(ue_id), "coef": str(coef)}
for (ue_id, coef) in module.get_ue_coef_dict().items()
]
# Et les parcours
mod["parcours"] = [
p.to_dict(with_annees=False) for p in module.parcours
]
# Et les AC
mod["app_critiques"] = {
x.code: x.to_dict() for x in module.app_critiques
}
2020-09-26 16:19:37 +02:00
if not export_ids:
del mod["id"]
2020-09-26 16:19:37 +02:00
del mod["ue_id"]
del mod["matiere_id"]
del mod["module_id"]
del mod["formation_id"]
if mod["ects"] is None:
del mod["ects"]
2022-03-01 09:48:37 +01:00
filename = f"scodoc_formation_{formation.departement.acronym}_{formation.acronyme or ''}_v{formation.version}"
2021-10-16 19:20:36 +02:00
return scu.sendResult(
2022-03-01 09:48:37 +01:00
F,
name="formation",
format=format,
force_outer_xml_tag=False,
attached=True,
filename=filename,
2021-10-16 19:20:36 +02:00
)
2020-09-26 16:19:37 +02:00
2021-08-20 01:09:55 +02:00
def formation_import_xml(doc: str, import_tags=True):
2020-09-26 16:19:37 +02:00
"""Create a formation from XML representation
(format dumped by formation_export( format='xml' ))
2021-08-01 10:16:16 +02:00
XML may contain object (UE, modules) ids: this function returns two
dicts mapping these ids to the created ids.
Args:
doc: str, xml data
import_tags: if false, does not import tags on modules.
Returns:
formation_id, modules_old2new, ues_old2new
2020-09-26 16:19:37 +02:00
"""
2021-06-21 10:17:16 +02:00
from app.scodoc import sco_edit_formation
2021-08-01 10:16:16 +02:00
# log("formation_import_xml: doc=%s" % doc)
2020-09-26 16:19:37 +02:00
try:
dom = xml.dom.minidom.parseString(doc)
2022-08-05 07:03:35 +02:00
except Exception as exc:
2020-09-26 16:19:37 +02:00
log("formation_import_xml: invalid XML data")
2022-08-05 07:03:35 +02:00
raise ScoValueError("Fichier XML invalide") from exc
2020-09-26 16:19:37 +02:00
2021-12-03 10:46:14 +01:00
try:
f = dom.getElementsByTagName("formation")[0] # or dom.documentElement
D = sco_xml.xml_to_dicts(f)
2022-08-05 07:03:35 +02:00
except Exception as exc:
2021-12-03 14:13:49 +01:00
raise ScoFormatError(
"""Ce document xml ne correspond pas à un programme exporté par ScoDoc.
(élément 'formation' inexistant par exemple)."""
2022-08-05 07:03:35 +02:00
) from exc
2020-09-26 16:19:37 +02:00
assert D[0] == "formation"
F = D[1]
2021-09-24 20:20:45 +02:00
F["dept_id"] = g.scodoc_dept_id
referentiel_competence_id = F.get("referentiel_competence_id")
2020-09-26 16:19:37 +02:00
# find new version number
2021-06-15 13:59:56 +02:00
cnx = ndb.GetDBConnexion()
2021-02-03 22:00:41 +01:00
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
2020-09-26 16:19:37 +02:00
cursor.execute(
2021-09-24 20:20:45 +02:00
"""SELECT max(version)
FROM notes_formations
WHERE acronyme=%(acronyme)s and titre=%(titre)s and dept_id=%(dept_id)s
""",
F,
2020-09-26 16:19:37 +02:00
)
res = cursor.fetchall()
try:
version = int(res[0][0]) + 1
2022-08-05 07:03:35 +02:00
except (ValueError, IndexError, TypeError):
2020-09-26 16:19:37 +02:00
version = 1
F["version"] = version
# create formation
# F_unquoted = F.copy()
# unescape_html_dict(F_unquoted)
2021-08-20 01:09:55 +02:00
formation_id = sco_edit_formation.do_formation_create(F)
2022-08-05 07:03:35 +02:00
log(f"formation {formation_id} created")
2020-09-26 16:19:37 +02:00
ues_old2new = {} # xml ue_id : new ue_id
modules_old2new = {} # xml module_id : new module_id
# (nb: mecanisme utilise pour cloner semestres seulement, pas pour I/O XML)
ue_reference_to_id = {} # pour les coefs APC (map reference -> ue_id)
modules_a_coefficienter = [] # Liste des modules avec coefs APC
2020-09-26 16:19:37 +02:00
# -- create UEs
for ue_info in D[2]:
assert ue_info[0] == "ue"
ue_info[1]["formation_id"] = formation_id
if "ue_id" in ue_info[1]:
2021-09-24 20:20:45 +02:00
xml_ue_id = int(ue_info[1]["ue_id"])
2020-09-26 16:19:37 +02:00
del ue_info[1]["ue_id"]
else:
xml_ue_id = None
2021-08-20 01:09:55 +02:00
ue_id = sco_edit_ue.do_ue_create(ue_info[1])
2020-09-26 16:19:37 +02:00
if xml_ue_id:
ues_old2new[xml_ue_id] = ue_id
# élément optionnel présent dans les exports BUT:
ue_reference = ue_info[1].get("reference")
if ue_reference:
ue_reference_to_id[int(ue_reference)] = ue_id
2020-09-26 16:19:37 +02:00
# -- create matieres
for mat_info in ue_info[2]:
assert mat_info[0] == "matiere"
mat_info[1]["ue_id"] = ue_id
2021-08-20 01:09:55 +02:00
mat_id = sco_edit_matiere.do_matiere_create(mat_info[1])
2020-09-26 16:19:37 +02:00
# -- create modules
for mod_info in mat_info[2]:
assert mod_info[0] == "module"
if "module_id" in mod_info[1]:
2021-09-24 20:20:45 +02:00
xml_module_id = int(mod_info[1]["module_id"])
2020-09-26 16:19:37 +02:00
del mod_info[1]["module_id"]
else:
xml_module_id = None
mod_info[1]["formation_id"] = formation_id
mod_info[1]["matiere_id"] = mat_id
mod_info[1]["ue_id"] = ue_id
2022-05-10 21:19:12 +02:00
if not "module_type" in mod_info[1]:
mod_info[1]["module_type"] = scu.ModuleType.STANDARD
2021-08-20 01:09:55 +02:00
mod_id = sco_edit_module.do_module_create(mod_info[1])
2020-09-26 16:19:37 +02:00
if xml_module_id:
modules_old2new[int(xml_module_id)] = mod_id
if len(mod_info) > 2:
module: Module = Module.query.get(mod_id)
tag_names = []
ue_coef_dict = {}
for child in mod_info[2]:
if child[0] == "tags" and import_tags:
tag_names.append(child[1]["name"])
elif child[0] == "coefficients":
ue_reference = int(child[1]["ue_reference"])
coef = float(child[1]["coef"])
ue_coef_dict[ue_reference] = coef
elif child[0] == "parcours":
# associe les parcours de ce module (BUT)
code_parcours = child[1]["code"]
parcours = ApcParcours.query.filter_by(
code=code_parcours,
referentiel_id=referentiel_competence_id,
).first()
if parcours:
module.parcours.append(parcours)
db.session.add(module)
else:
log(f"Warning: parcours {code_parcours} inexistant !")
if import_tags and tag_names:
2021-08-20 01:09:55 +02:00
sco_tag_module.module_tag_set(mod_id, tag_names)
if module.is_apc() and ue_coef_dict:
modules_a_coefficienter.append((module, ue_coef_dict))
# Fixe les coefs APC (à la fin pour que les UE soient crées)
for module, ue_coef_dict_ref in modules_a_coefficienter:
# remap ue ids:
ue_coef_dict = {ue_reference_to_id[k]: v for (k, v) in ue_coef_dict_ref.items()}
module.set_ue_coef_dict(ue_coef_dict)
db.session.commit()
2020-09-26 16:19:37 +02:00
return formation_id, modules_old2new, ues_old2new
2021-09-24 20:20:45 +02:00
def formation_list_table(formation_id=None, args={}):
2020-09-26 16:19:37 +02:00
"""List formation, grouped by titre and sorted by versions
and listing associated semestres
returns a table
"""
2021-08-20 01:09:55 +02:00
formations = formation_list(formation_id=formation_id, args=args)
2020-09-26 16:19:37 +02:00
title = "Programmes pédagogiques"
2021-02-04 20:02:44 +01:00
lockicon = scu.icontag(
2020-09-26 16:19:37 +02:00
"lock32_img", title="Comporte des semestres verrouillés", border="0"
)
2021-02-04 20:02:44 +01:00
suppricon = scu.icontag(
2020-09-26 16:19:37 +02:00
"delete_small_img", border="0", alt="supprimer", title="Supprimer"
)
2021-02-04 20:02:44 +01:00
editicon = scu.icontag(
2020-09-26 16:19:37 +02:00
"edit_img", border="0", alt="modifier", title="Modifier titres et code"
)
editable = current_user.has_permission(Permission.ScoChangeFormation)
2020-09-26 16:19:37 +02:00
# Traduit/ajoute des champs à afficher:
for f in formations:
try:
f["parcours_name"] = sco_codes_parcours.get_parcours_from_code(
f["type_parcours"]
).NAME
except:
f["parcours_name"] = ""
f["_titre_target"] = url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(f["formation_id"]),
)
2020-09-26 16:19:37 +02:00
f["_titre_link_class"] = "stdlink"
f["_titre_id"] = "titre-%s" % f["acronyme"].lower().replace(" ", "-")
2020-09-26 16:19:37 +02:00
# Ajoute les semestres associés à chaque formation:
f["sems"] = sco_formsemestre.do_formsemestre_list(
2021-08-19 10:28:35 +02:00
args={"formation_id": f["formation_id"]}
2020-09-26 16:19:37 +02:00
)
f["sems_list_txt"] = ", ".join([s["session_id"] for s in f["sems"]])
f["_sems_list_txt_html"] = ", ".join(
[
'<a class="discretelink" href="formsemestre_status?formsemestre_id=%(formsemestre_id)s">%('
"session_id)s<a> " % s
2020-09-26 16:19:37 +02:00
for s in f["sems"]
]
2022-03-15 23:09:41 +01:00
+ (
[
'<a class="stdlink" id="add-semestre-%s" '
'href="formsemestre_createwithmodules?formation_id=%s&semestre_id=1">ajouter</a> '
% (f["acronyme"].lower().replace(" ", "-"), f["formation_id"])
]
if current_user.has_permission(Permission.ScoImplement)
else []
)
2020-09-26 16:19:37 +02:00
)
if f["sems"]:
f["date_fin_dernier_sem"] = max([s["date_fin_iso"] for s in f["sems"]])
f["annee_dernier_sem"] = f["date_fin_dernier_sem"].split("-")[0]
else:
f["date_fin_dernier_sem"] = ""
f["annee_dernier_sem"] = ""
2021-08-20 01:09:55 +02:00
locked = formation_has_locked_sems(f["formation_id"])
2020-09-26 16:19:37 +02:00
#
if locked:
but_locked = lockicon
else:
but_locked = '<span class="but_placeholder"></span>'
if editable and not locked:
2022-04-12 17:12:51 +02:00
but_suppr = (
'<a class="stdlink" href="formation_delete?formation_id=%s" id="delete-formation-%s">%s</a>'
% (
f["formation_id"],
f["acronyme"].lower().replace(" ", "-"),
suppricon,
)
2020-09-26 16:19:37 +02:00
)
else:
but_suppr = '<span class="but_placeholder"></span>'
if editable:
but_edit = (
'<a class="stdlink" href="formation_edit?formation_id=%s" id="edit-formation-%s">%s</a>'
% (f["formation_id"], f["acronyme"].lower().replace(" ", "-"), editicon)
2020-09-26 16:19:37 +02:00
)
else:
but_edit = '<span class="but_placeholder"></span>'
f["buttons"] = ""
f["_buttons_html"] = but_locked + but_suppr + but_edit
# Tri par annee_denier_sem, type, acronyme, titre, version décroissante
formations.sort(key=itemgetter("version"), reverse=True)
formations.sort(key=itemgetter("titre"))
formations.sort(key=itemgetter("acronyme"))
formations.sort(key=itemgetter("parcours_name"))
formations.sort(
key=itemgetter("annee_dernier_sem"), reverse=True
) # plus recemments utilises en tete
#
columns_ids = (
"buttons",
"acronyme",
"parcours_name",
"formation_code",
"version",
"titre",
"sems_list_txt",
)
titles = {
"buttons": "",
"acronyme": "Acro.",
"parcours_name": "Type",
"titre": "Titre",
"version": "Version",
"formation_code": "Code",
"sems_list_txt": "Semestres",
}
return GenTable(
columns_ids=columns_ids,
rows=formations,
titles=titles,
2021-08-21 17:07:44 +02:00
origin="Généré par %s le " % sco_version.SCONAME
+ scu.timedate_human_repr()
+ "",
2020-09-26 16:19:37 +02:00
caption=title,
html_caption=title,
table_id="formation_list_table",
html_class="formation_list_table table_leftalign",
html_with_td_classes=True,
html_sortable=True,
base_url="%s?formation_id=%s" % (request.base_url, formation_id),
2020-09-26 16:19:37 +02:00
page_title=title,
pdf_title=title,
preferences=sco_preferences.SemPreferences(),
2020-09-26 16:19:37 +02:00
)
2021-06-21 10:17:16 +02:00
2021-09-24 20:20:45 +02:00
def formation_create_new_version(formation_id, redirect=True):
2021-06-21 10:17:16 +02:00
"duplicate formation, with new version number"
2022-07-13 19:23:55 +02:00
formation = Formation.query.get_or_404(formation_id)
2021-09-24 20:20:45 +02:00
resp = formation_export(formation_id, export_ids=True, format="xml")
xml_data = resp.get_data(as_text=True)
new_id, modules_old2new, ues_old2new = formation_import_xml(xml_data)
2021-06-21 10:17:16 +02:00
# news
2022-04-12 17:12:51 +02:00
ScolarNews.add(
typ=ScolarNews.NEWS_FORM,
obj=new_id,
2022-07-13 19:23:55 +02:00
text=f"Nouvelle version de la formation {formation.acronyme}",
2021-06-21 10:17:16 +02:00
)
if redirect:
2022-07-13 19:23:55 +02:00
flash("Nouvelle version !")
2021-07-31 18:01:10 +02:00
return flask.redirect(
2021-08-09 10:09:04 +02:00
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,
formation_id=new_id,
msg="Nouvelle version !",
)
2021-06-21 10:17:16 +02:00
)
else:
return new_id, modules_old2new, ues_old2new