2022-07-19 22:17:10 +02:00
|
|
|
##############################################################################
|
|
|
|
# ScoDoc
|
2023-12-31 23:04:06 +01:00
|
|
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
2022-07-19 22:17:10 +02:00
|
|
|
# See LICENSE
|
|
|
|
##############################################################################
|
|
|
|
|
|
|
|
"""
|
|
|
|
ScoDoc 9 API : accès aux formsemestres
|
|
|
|
"""
|
2023-04-03 17:46:31 +02:00
|
|
|
from operator import attrgetter, itemgetter
|
|
|
|
|
2023-05-15 23:39:08 +02:00
|
|
|
from flask import g, make_response, request
|
2023-04-06 16:10:32 +02:00
|
|
|
from flask_json import as_json
|
2024-01-20 17:37:24 +01:00
|
|
|
from flask_login import current_user, login_required
|
2024-03-19 18:22:02 +01:00
|
|
|
import sqlalchemy as sa
|
2022-04-20 15:50:02 +02:00
|
|
|
import app
|
2024-06-24 03:37:40 +02:00
|
|
|
from app import db, log
|
2023-02-22 02:13:06 +01:00
|
|
|
from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR
|
2024-07-17 12:03:08 +02:00
|
|
|
from app.api import api_permission_required as permission_required
|
|
|
|
from app.decorators import scodoc
|
2022-08-07 19:56:25 +02:00
|
|
|
from app.scodoc.sco_utils import json_error
|
2022-05-31 16:08:57 +02:00
|
|
|
from app.comp import res_sem
|
|
|
|
from app.comp.moy_mod import ModuleImplResults
|
|
|
|
from app.comp.res_compat import NotesTableCompat
|
2022-07-27 16:03:14 +02:00
|
|
|
from app.models import (
|
|
|
|
Departement,
|
|
|
|
Evaluation,
|
|
|
|
FormSemestre,
|
|
|
|
FormSemestreEtape,
|
2022-10-28 15:14:33 +02:00
|
|
|
FormSemestreInscription,
|
|
|
|
Identite,
|
2022-07-27 16:03:14 +02:00
|
|
|
ModuleImpl,
|
2022-07-29 16:19:40 +02:00
|
|
|
NotesNotes,
|
2022-07-27 16:03:14 +02:00
|
|
|
)
|
2023-05-15 23:39:08 +02:00
|
|
|
from app.models.formsemestre import GROUPS_AUTO_ASSIGNMENT_DATA_MAX
|
2022-04-26 17:12:30 +02:00
|
|
|
from app.scodoc.sco_bulletins import get_formsemestre_bulletin_etud_json
|
2023-11-11 18:13:18 +01:00
|
|
|
from app.scodoc import sco_edt_cal
|
2022-07-21 16:46:07 +02:00
|
|
|
from app.scodoc import sco_groups
|
2022-03-04 17:16:08 +01:00
|
|
|
from app.scodoc.sco_permissions import Permission
|
2022-05-09 16:26:23 +02:00
|
|
|
from app.scodoc.sco_utils import ModuleType
|
2022-05-31 16:08:57 +02:00
|
|
|
import app.scodoc.sco_utils as scu
|
2024-03-24 14:34:55 +01:00
|
|
|
from app.tables.recap import TableRecap, RowRecap
|
2022-03-02 16:45:47 +01:00
|
|
|
|
|
|
|
|
2022-07-27 16:03:14 +02:00
|
|
|
@bp.route("/formsemestre/<int:formsemestre_id>")
|
|
|
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>")
|
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
2023-04-06 16:10:32 +02:00
|
|
|
@as_json
|
2022-07-19 22:17:10 +02:00
|
|
|
def formsemestre_infos(formsemestre_id: int):
|
2022-03-02 16:45:47 +01:00
|
|
|
"""
|
2022-05-09 16:26:23 +02:00
|
|
|
Information sur le formsemestre indiqué.
|
2022-03-02 16:45:47 +01:00
|
|
|
|
2022-05-09 16:26:23 +02:00
|
|
|
formsemestre_id : l'id du formsemestre
|
2022-03-02 16:45:47 +01:00
|
|
|
|
2022-04-27 14:11:06 +02:00
|
|
|
Exemple de résultat :
|
|
|
|
{
|
2022-05-09 16:26:23 +02:00
|
|
|
"block_moyennes": false,
|
|
|
|
"bul_bgcolor": "white",
|
|
|
|
"bul_hide_xml": false,
|
|
|
|
"date_debut_iso": "2021-09-01",
|
|
|
|
"date_debut": "01/09/2021",
|
|
|
|
"date_fin_iso": "2022-08-31",
|
|
|
|
"date_fin": "31/08/2022",
|
|
|
|
"dept_id": 1,
|
|
|
|
"elt_annee_apo": null,
|
2024-06-24 03:37:40 +02:00
|
|
|
"elt_passage_apo" : null,
|
2022-05-09 16:26:23 +02:00
|
|
|
"elt_sem_apo": null,
|
|
|
|
"ens_can_edit_eval": false,
|
|
|
|
"etat": true,
|
|
|
|
"formation_id": 1,
|
|
|
|
"formsemestre_id": 1,
|
|
|
|
"gestion_compensation": false,
|
|
|
|
"gestion_semestrielle": false,
|
|
|
|
"id": 1,
|
|
|
|
"modalite": "FI",
|
|
|
|
"resp_can_change_ens": true,
|
|
|
|
"resp_can_edit": false,
|
|
|
|
"responsables": [1, 99], // uids
|
|
|
|
"scodoc7_id": null,
|
|
|
|
"semestre_id": 1,
|
|
|
|
"titre_formation" : "BUT GEA",
|
|
|
|
"titre_num": "BUT GEA semestre 1",
|
|
|
|
"titre": "BUT GEA",
|
|
|
|
}
|
|
|
|
|
2022-03-02 16:45:47 +01:00
|
|
|
"""
|
2022-07-27 16:03:14 +02:00
|
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
2023-04-06 16:10:32 +02:00
|
|
|
return formsemestre.to_dict_api()
|
2022-03-02 16:45:47 +01:00
|
|
|
|
|
|
|
|
2022-07-27 16:03:14 +02:00
|
|
|
@bp.route("/formsemestres/query")
|
|
|
|
@api_web_bp.route("/formsemestres/query")
|
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
2023-04-06 16:10:32 +02:00
|
|
|
@as_json
|
2022-07-21 08:53:44 +02:00
|
|
|
def formsemestres_query():
|
2022-04-27 09:24:20 +02:00
|
|
|
"""
|
2022-07-21 08:53:44 +02:00
|
|
|
Retourne les formsemestres filtrés par
|
2023-08-11 18:44:51 +02:00
|
|
|
étape Apogée ou année scolaire ou département (acronyme ou id) ou état ou code étudiant
|
2022-04-27 09:24:20 +02:00
|
|
|
|
2022-05-03 13:35:17 +02:00
|
|
|
etape_apo : un code étape apogée
|
2022-07-21 08:53:44 +02:00
|
|
|
annee_scolaire : année de début de l'année scolaire
|
|
|
|
dept_acronym : acronyme du département (eg "RT")
|
|
|
|
dept_id : id du département
|
2022-10-28 15:14:33 +02:00
|
|
|
ine ou nip: code d'un étudiant: ramène alors tous les semestres auxquels il est inscrit.
|
2023-08-11 18:44:51 +02:00
|
|
|
etat: 0 si verrouillé, 1 sinon
|
2024-06-23 17:40:48 +02:00
|
|
|
|
|
|
|
QUERY
|
|
|
|
-----
|
|
|
|
etape_apo:<string:etape_apo>
|
|
|
|
annee_scolaire:<string:annee_scolaire>
|
|
|
|
dept_acronym:<string:dept_acronym>
|
|
|
|
dept_id:<int:dept_id>
|
|
|
|
etat:<int:etat>
|
|
|
|
nip:<string:nip>
|
|
|
|
ine:<string:ine>
|
|
|
|
|
2022-04-27 09:24:20 +02:00
|
|
|
"""
|
2022-07-21 08:53:44 +02:00
|
|
|
etape_apo = request.args.get("etape_apo")
|
|
|
|
annee_scolaire = request.args.get("annee_scolaire")
|
|
|
|
dept_acronym = request.args.get("dept_acronym")
|
|
|
|
dept_id = request.args.get("dept_id")
|
2023-08-11 18:44:51 +02:00
|
|
|
etat = request.args.get("etat")
|
2022-10-28 15:14:33 +02:00
|
|
|
nip = request.args.get("nip")
|
2022-10-28 15:48:27 +02:00
|
|
|
ine = request.args.get("ine")
|
2022-07-21 08:53:44 +02:00
|
|
|
formsemestres = FormSemestre.query
|
2022-07-27 16:03:14 +02:00
|
|
|
if g.scodoc_dept:
|
2022-07-29 16:19:40 +02:00
|
|
|
formsemestres = formsemestres.filter_by(dept_id=g.scodoc_dept_id)
|
2022-07-21 08:53:44 +02:00
|
|
|
if annee_scolaire is not None:
|
|
|
|
try:
|
|
|
|
annee_scolaire_int = int(annee_scolaire)
|
|
|
|
except ValueError:
|
2023-02-22 02:13:06 +01:00
|
|
|
return json_error(API_CLIENT_ERROR, "invalid annee_scolaire: not int")
|
2023-12-29 13:57:44 +01:00
|
|
|
debut_annee = scu.date_debut_annee_scolaire(annee_scolaire_int)
|
|
|
|
fin_annee = scu.date_fin_annee_scolaire(annee_scolaire_int)
|
2022-07-21 08:53:44 +02:00
|
|
|
formsemestres = formsemestres.filter(
|
|
|
|
FormSemestre.date_fin >= debut_annee, FormSemestre.date_debut <= fin_annee
|
|
|
|
)
|
2023-08-11 18:44:51 +02:00
|
|
|
if etat is not None:
|
|
|
|
try:
|
|
|
|
etat = bool(int(etat))
|
|
|
|
except ValueError:
|
|
|
|
return json_error(404, "invalid etat: integer expected")
|
|
|
|
formsemestres = formsemestres.filter_by(etat=etat)
|
2022-07-21 08:53:44 +02:00
|
|
|
if dept_acronym is not None:
|
2022-07-27 16:03:14 +02:00
|
|
|
formsemestres = formsemestres.join(Departement).filter_by(acronym=dept_acronym)
|
2022-07-21 08:53:44 +02:00
|
|
|
if dept_id is not None:
|
|
|
|
try:
|
|
|
|
dept_id = int(dept_id)
|
|
|
|
except ValueError:
|
2023-02-22 02:13:06 +01:00
|
|
|
return json_error(404, "invalid dept_id: integer expected")
|
2022-07-21 08:53:44 +02:00
|
|
|
formsemestres = formsemestres.filter_by(dept_id=dept_id)
|
2022-07-29 16:19:40 +02:00
|
|
|
if etape_apo is not None:
|
|
|
|
formsemestres = formsemestres.join(FormSemestreEtape).filter(
|
|
|
|
FormSemestreEtape.etape_apo == etape_apo
|
|
|
|
)
|
2022-10-28 15:48:27 +02:00
|
|
|
inscr_joined = False
|
2022-10-28 15:14:33 +02:00
|
|
|
if nip is not None:
|
|
|
|
formsemestres = (
|
|
|
|
formsemestres.join(FormSemestreInscription)
|
|
|
|
.join(Identite)
|
|
|
|
.filter_by(code_nip=nip)
|
|
|
|
)
|
2022-10-28 15:48:27 +02:00
|
|
|
inscr_joined = True
|
2022-10-28 15:14:33 +02:00
|
|
|
if ine is not None:
|
2022-10-28 15:48:27 +02:00
|
|
|
if not inscr_joined:
|
|
|
|
formsemestres = formsemestres.join(FormSemestreInscription).join(Identite)
|
|
|
|
formsemestres = formsemestres.filter_by(code_ine=ine)
|
|
|
|
|
2023-08-11 18:44:51 +02:00
|
|
|
return [
|
|
|
|
formsemestre.to_dict_api()
|
|
|
|
for formsemestre in formsemestres.order_by(
|
|
|
|
FormSemestre.date_debut.desc(),
|
|
|
|
FormSemestre.modalite,
|
|
|
|
FormSemestre.semestre_id,
|
|
|
|
FormSemestre.titre,
|
|
|
|
)
|
|
|
|
]
|
2022-04-27 09:24:20 +02:00
|
|
|
|
|
|
|
|
2024-03-19 18:22:02 +01:00
|
|
|
@bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
|
|
|
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edit", methods=["POST"])
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.EditFormSemestre)
|
|
|
|
@as_json
|
|
|
|
def formsemestre_edit(formsemestre_id: int):
|
|
|
|
"""Modifie les champs d'un formsemestre."""
|
|
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
|
|
args = request.get_json(force=True) # may raise 400 Bad Request
|
|
|
|
editable_keys = {
|
|
|
|
"semestre_id",
|
|
|
|
"titre",
|
|
|
|
"date_debut",
|
|
|
|
"date_fin",
|
|
|
|
"edt_id",
|
|
|
|
"etat",
|
|
|
|
"modalite",
|
|
|
|
"gestion_compensation",
|
|
|
|
"bul_hide_xml",
|
|
|
|
"block_moyennes",
|
|
|
|
"block_moyenne_generale",
|
|
|
|
"mode_calcul_moyennes",
|
|
|
|
"gestion_semestrielle",
|
|
|
|
"bul_bgcolor",
|
|
|
|
"resp_can_edit",
|
|
|
|
"resp_can_change_ens",
|
|
|
|
"ens_can_edit_eval",
|
|
|
|
"elt_sem_apo",
|
|
|
|
"elt_annee_apo",
|
|
|
|
}
|
|
|
|
formsemestre.from_dict({k: v for (k, v) in args.items() if k in editable_keys})
|
|
|
|
try:
|
|
|
|
db.session.commit()
|
|
|
|
except sa.exc.StatementError as exc:
|
|
|
|
return json_error(404, f"invalid argument(s): {exc.args[0]}")
|
|
|
|
return formsemestre.to_dict_api()
|
|
|
|
|
|
|
|
|
2024-06-24 03:37:40 +02:00
|
|
|
@bp.route("/formsemestre/apo/set_etapes", methods=["POST"])
|
|
|
|
@api_web_bp.route("/formsemestre/apo/set_etapes", methods=["POST"])
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.EditApogee)
|
|
|
|
def formsemestre_set_apo_etapes():
|
|
|
|
"""Change les codes étapes du semestre indiqué.
|
|
|
|
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
|
|
|
par des virgules.
|
|
|
|
(Ce changement peut être fait sur un semestre verrouillé)
|
|
|
|
|
|
|
|
Args:
|
|
|
|
oid=int, le formsemestre_id
|
|
|
|
value=chaine "V1RT, V1RT2", codes séparés par des virgules
|
|
|
|
"""
|
|
|
|
formsemestre_id = int(request.form.get("oid"))
|
|
|
|
etapes_apo_str = request.form.get("value")
|
|
|
|
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
|
|
|
|
|
|
|
|
current_etapes = {e.etape_apo for e in formsemestre.etapes}
|
|
|
|
new_etapes = {s.strip() for s in etapes_apo_str.split(",")}
|
|
|
|
|
|
|
|
if new_etapes != current_etapes:
|
|
|
|
formsemestre.etapes = []
|
|
|
|
for etape_apo in new_etapes:
|
|
|
|
etape = FormSemestreEtape(
|
|
|
|
formsemestre_id=formsemestre_id, etape_apo=etape_apo
|
|
|
|
)
|
|
|
|
formsemestre.etapes.append(etape)
|
|
|
|
db.session.add(formsemestre)
|
|
|
|
db.session.commit()
|
|
|
|
log(
|
|
|
|
f"""API formsemestre_set_apo_etapes: formsemestre_id={
|
|
|
|
formsemestre.id} code_apogee={etapes_apo_str}"""
|
|
|
|
)
|
|
|
|
return ("", 204)
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/formsemestre/apo/set_elt_sem", methods=["POST"])
|
|
|
|
@api_web_bp.route("/formsemestre/apo/set_elt_sem", methods=["POST"])
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.EditApogee)
|
|
|
|
def formsemestre_set_elt_sem_apo():
|
|
|
|
"""Change les codes étapes du semestre indiqué.
|
|
|
|
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
|
|
|
par des virgules.
|
|
|
|
(Ce changement peut être fait sur un semestre verrouillé)
|
|
|
|
|
|
|
|
Args:
|
|
|
|
oid=int, le formsemestre_id
|
|
|
|
value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
|
|
|
|
"""
|
|
|
|
oid = int(request.form.get("oid"))
|
|
|
|
value = (request.form.get("value") or "").strip()
|
|
|
|
formsemestre = FormSemestre.get_formsemestre(oid)
|
|
|
|
if value != formsemestre.elt_sem_apo:
|
|
|
|
formsemestre.elt_sem_apo = value
|
|
|
|
db.session.add(formsemestre)
|
|
|
|
db.session.commit()
|
|
|
|
log(
|
|
|
|
f"""API formsemestre_set_elt_sem_apo: formsemestre_id={
|
|
|
|
formsemestre.id} code_apogee={value}"""
|
|
|
|
)
|
|
|
|
return ("", 204)
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/formsemestre/apo/set_elt_annee", methods=["POST"])
|
|
|
|
@api_web_bp.route("/formsemestre/apo/set_elt_annee", methods=["POST"])
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.EditApogee)
|
|
|
|
def formsemestre_set_elt_annee_apo():
|
|
|
|
"""Change les codes étapes du semestre indiqué (par le champ oid).
|
|
|
|
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
|
|
|
par des virgules.
|
|
|
|
(Ce changement peut être fait sur un semestre verrouillé)
|
|
|
|
|
|
|
|
Args:
|
|
|
|
oid=int, le formsemestre_id
|
|
|
|
value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
|
|
|
|
"""
|
|
|
|
oid = int(request.form.get("oid"))
|
|
|
|
value = (request.form.get("value") or "").strip()
|
|
|
|
formsemestre = FormSemestre.get_formsemestre(oid)
|
|
|
|
if value != formsemestre.elt_annee_apo:
|
|
|
|
formsemestre.elt_annee_apo = value
|
|
|
|
db.session.add(formsemestre)
|
|
|
|
db.session.commit()
|
|
|
|
log(
|
|
|
|
f"""API formsemestre_set_elt_annee_apo: formsemestre_id={
|
|
|
|
formsemestre.id} code_apogee={value}"""
|
|
|
|
)
|
|
|
|
return ("", 204)
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/formsemestre/apo/set_elt_passage", methods=["POST"])
|
|
|
|
@api_web_bp.route("/formsemestre/apo/set_elt_passage", methods=["POST"])
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.EditApogee)
|
|
|
|
def formsemestre_set_elt_passage_apo():
|
|
|
|
"""Change les codes apogée de passage du semestre indiqué (par le champ oid).
|
|
|
|
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
|
|
|
|
par des virgules.
|
|
|
|
(Ce changement peut être fait sur un semestre verrouillé)
|
|
|
|
|
|
|
|
Args:
|
|
|
|
oid=int, le formsemestre_id
|
|
|
|
value=chaine "V3ONM, V3ONM1, V3ONM2", codes séparés par des virgules
|
|
|
|
"""
|
|
|
|
oid = int(request.form.get("oid"))
|
|
|
|
value = (request.form.get("value") or "").strip()
|
|
|
|
formsemestre = FormSemestre.get_formsemestre(oid)
|
|
|
|
if value != formsemestre.elt_annee_apo:
|
|
|
|
formsemestre.elt_passage_apo = value
|
|
|
|
db.session.add(formsemestre)
|
|
|
|
db.session.commit()
|
|
|
|
log(
|
|
|
|
f"""API formsemestre_set_elt_passage_apo: formsemestre_id={
|
|
|
|
formsemestre.id} code_apogee={value}"""
|
|
|
|
)
|
|
|
|
return ("", 204)
|
|
|
|
|
|
|
|
|
2022-07-27 16:03:14 +02:00
|
|
|
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
2022-11-28 19:13:57 +01:00
|
|
|
@bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
2022-07-27 16:03:14 +02:00
|
|
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins")
|
2022-11-28 19:13:57 +01:00
|
|
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/bulletins/<string:version>")
|
2022-07-27 16:03:14 +02:00
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
2023-04-06 16:10:32 +02:00
|
|
|
@as_json
|
2022-11-28 19:13:57 +01:00
|
|
|
def bulletins(formsemestre_id: int, version: str = "long"):
|
2022-03-02 16:45:47 +01:00
|
|
|
"""
|
|
|
|
Retourne les bulletins d'un formsemestre donné
|
|
|
|
|
|
|
|
formsemestre_id : l'id d'un formesemestre
|
2022-04-27 14:11:06 +02:00
|
|
|
|
2022-07-19 22:17:10 +02:00
|
|
|
Exemple de résultat : liste, voir https://scodoc.org/ScoDoc9API/#bulletin
|
2022-03-02 16:45:47 +01:00
|
|
|
"""
|
2022-07-27 16:03:14 +02:00
|
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
2022-11-28 19:13:57 +01:00
|
|
|
formsemestre: FormSemestre = query.first()
|
|
|
|
if formsemestre is None:
|
|
|
|
return json_error(404, "formsemestre non trouve")
|
2022-07-19 22:17:10 +02:00
|
|
|
app.set_sco_dept(formsemestre.departement.acronym)
|
2022-04-26 17:12:30 +02:00
|
|
|
|
|
|
|
data = []
|
2022-05-03 13:35:17 +02:00
|
|
|
for etu in formsemestre.etuds:
|
2022-11-28 19:13:57 +01:00
|
|
|
bul_etu = get_formsemestre_bulletin_etud_json(
|
|
|
|
formsemestre, etu, version=version
|
|
|
|
)
|
2022-04-26 17:12:30 +02:00
|
|
|
data.append(bul_etu.json)
|
2022-03-02 16:45:47 +01:00
|
|
|
|
2023-04-06 16:10:32 +02:00
|
|
|
return data
|
2022-03-02 16:45:47 +01:00
|
|
|
|
|
|
|
|
2022-07-27 16:03:14 +02:00
|
|
|
@bp.route("/formsemestre/<int:formsemestre_id>/programme")
|
|
|
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/programme")
|
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
2023-04-06 16:10:32 +02:00
|
|
|
@as_json
|
2022-05-09 16:26:23 +02:00
|
|
|
def formsemestre_programme(formsemestre_id: int):
|
|
|
|
"""
|
2023-08-26 16:34:56 +02:00
|
|
|
Retourne la liste des UEs, ressources et SAEs d'un semestre
|
2022-05-09 16:26:23 +02:00
|
|
|
|
|
|
|
formsemestre_id : l'id d'un formsemestre
|
|
|
|
|
|
|
|
Exemple de résultat :
|
|
|
|
{
|
|
|
|
"ues": [
|
|
|
|
{
|
|
|
|
"type": 0,
|
|
|
|
"formation_id": 1,
|
|
|
|
"ue_code": "UCOD11",
|
|
|
|
"id": 1,
|
|
|
|
"ects": 12.0,
|
|
|
|
"acronyme": "RT1.1",
|
|
|
|
"is_external": false,
|
|
|
|
"numero": 1,
|
|
|
|
"code_apogee": "",
|
|
|
|
"titre": "Administrer les r\u00e9seaux et l\u2019Internet",
|
|
|
|
"coefficient": 0.0,
|
|
|
|
"semestre_idx": 1,
|
|
|
|
"color": "#B80004",
|
|
|
|
"ue_id": 1
|
|
|
|
},
|
|
|
|
...
|
|
|
|
],
|
|
|
|
"ressources": [
|
|
|
|
{
|
|
|
|
"ens": [ 10, 18 ],
|
|
|
|
"formsemestre_id": 1,
|
|
|
|
"id": 15,
|
|
|
|
"module": {
|
|
|
|
"abbrev": "Programmer",
|
|
|
|
"code": "SAE15",
|
|
|
|
"code_apogee": "V7GOP",
|
|
|
|
"coefficient": 1.0,
|
|
|
|
"formation_id": 1,
|
|
|
|
"heures_cours": 0.0,
|
|
|
|
"heures_td": 0.0,
|
|
|
|
"heures_tp": 0.0,
|
|
|
|
"id": 15,
|
|
|
|
"matiere_id": 3,
|
|
|
|
"module_id": 15,
|
|
|
|
"module_type": 3,
|
|
|
|
"numero": 50,
|
|
|
|
"semestre_id": 1,
|
|
|
|
"titre": "Programmer en Python",
|
|
|
|
"ue_id": 3
|
|
|
|
},
|
|
|
|
"module_id": 15,
|
|
|
|
"moduleimpl_id": 15,
|
|
|
|
"responsable_id": 2
|
|
|
|
},
|
|
|
|
...
|
|
|
|
],
|
|
|
|
"saes": [
|
|
|
|
{
|
|
|
|
...
|
|
|
|
},
|
|
|
|
...
|
|
|
|
],
|
|
|
|
"modules" : [ ... les modules qui ne sont ni des SAEs ni des ressources ... ]
|
|
|
|
}
|
|
|
|
"""
|
2022-07-27 16:03:14 +02:00
|
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
2023-04-03 17:46:31 +02:00
|
|
|
ues = formsemestre.get_ues()
|
2022-05-09 16:26:23 +02:00
|
|
|
m_list = {
|
|
|
|
ModuleType.RESSOURCE: [],
|
|
|
|
ModuleType.SAE: [],
|
|
|
|
ModuleType.STANDARD: [],
|
2022-08-25 18:22:04 +02:00
|
|
|
ModuleType.MALUS: [],
|
2022-05-09 16:26:23 +02:00
|
|
|
}
|
|
|
|
for modimpl in formsemestre.modimpls_sorted:
|
2022-07-21 14:21:06 +02:00
|
|
|
d = modimpl.to_dict(convert_objects=True)
|
2022-05-09 16:26:23 +02:00
|
|
|
m_list[modimpl.module.module_type].append(d)
|
2023-04-06 16:10:32 +02:00
|
|
|
return {
|
|
|
|
"ues": [ue.to_dict(convert_objects=True) for ue in ues],
|
|
|
|
"ressources": m_list[ModuleType.RESSOURCE],
|
|
|
|
"saes": m_list[ModuleType.SAE],
|
|
|
|
"modules": m_list[ModuleType.STANDARD],
|
|
|
|
"malus": m_list[ModuleType.MALUS],
|
|
|
|
}
|
2022-05-13 14:37:02 +02:00
|
|
|
|
|
|
|
|
|
|
|
@bp.route(
|
2022-05-13 15:56:54 +02:00
|
|
|
"/formsemestre/<int:formsemestre_id>/etudiants",
|
2022-08-30 19:13:21 +02:00
|
|
|
defaults={"with_query": False, "long": False},
|
|
|
|
)
|
|
|
|
@bp.route(
|
|
|
|
"/formsemestre/<int:formsemestre_id>/etudiants/long",
|
|
|
|
defaults={"with_query": False, "long": True},
|
2022-08-08 10:06:42 +02:00
|
|
|
)
|
|
|
|
@bp.route(
|
2022-08-10 07:16:34 +02:00
|
|
|
"/formsemestre/<int:formsemestre_id>/etudiants/query",
|
2022-08-30 19:13:21 +02:00
|
|
|
defaults={"with_query": True, "long": False},
|
|
|
|
)
|
|
|
|
@bp.route(
|
|
|
|
"/formsemestre/<int:formsemestre_id>/etudiants/long/query",
|
|
|
|
defaults={"with_query": True, "long": True},
|
2022-05-13 14:37:02 +02:00
|
|
|
)
|
2022-07-27 16:03:14 +02:00
|
|
|
@api_web_bp.route(
|
|
|
|
"/formsemestre/<int:formsemestre_id>/etudiants",
|
2022-08-30 19:13:21 +02:00
|
|
|
defaults={"with_query": False, "long": False},
|
|
|
|
)
|
|
|
|
@api_web_bp.route(
|
|
|
|
"/formsemestre/<int:formsemestre_id>/etudiants/long",
|
|
|
|
defaults={"with_query": False, "long": True},
|
2022-07-27 16:03:14 +02:00
|
|
|
)
|
|
|
|
@api_web_bp.route(
|
2022-08-10 07:16:34 +02:00
|
|
|
"/formsemestre/<int:formsemestre_id>/etudiants/query",
|
2022-08-30 19:13:21 +02:00
|
|
|
defaults={"with_query": True, "long": False},
|
|
|
|
)
|
|
|
|
@api_web_bp.route(
|
|
|
|
"/formsemestre/<int:formsemestre_id>/etudiants/long/query",
|
|
|
|
defaults={"with_query": True, "long": True},
|
2022-07-27 16:03:14 +02:00
|
|
|
)
|
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
2023-04-06 16:10:32 +02:00
|
|
|
@as_json
|
2022-08-30 19:13:21 +02:00
|
|
|
def formsemestre_etudiants(
|
|
|
|
formsemestre_id: int, with_query: bool = False, long: bool = False
|
|
|
|
):
|
2024-06-23 17:40:48 +02:00
|
|
|
"""Étudiants d'un formsemestre.
|
|
|
|
|
|
|
|
Si l'état est spécifié, ne renvoie que les inscrits (`I`), les
|
|
|
|
démissionnaires (`D`) ou les défaillants (`DEF`)
|
|
|
|
|
|
|
|
QUERY
|
|
|
|
-----
|
|
|
|
etat:<string:etat>
|
|
|
|
|
|
|
|
"""
|
2022-07-27 16:03:14 +02:00
|
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
2022-08-10 07:16:34 +02:00
|
|
|
if with_query:
|
|
|
|
etat = request.args.get("etat")
|
|
|
|
if etat is not None:
|
|
|
|
etat = {
|
|
|
|
"actifs": scu.INSCRIT,
|
|
|
|
"demissionnaires": scu.DEMISSION,
|
|
|
|
"defaillants": scu.DEF,
|
|
|
|
}.get(etat, etat)
|
|
|
|
inscriptions = [
|
|
|
|
ins for ins in formsemestre.inscriptions if ins.etat == etat
|
|
|
|
]
|
|
|
|
else:
|
|
|
|
inscriptions = formsemestre.inscriptions
|
2022-08-08 10:06:42 +02:00
|
|
|
else:
|
2022-08-10 07:16:34 +02:00
|
|
|
inscriptions = formsemestre.inscriptions
|
2022-05-13 14:37:02 +02:00
|
|
|
|
2022-08-30 19:13:21 +02:00
|
|
|
if long:
|
2024-01-20 17:37:24 +01:00
|
|
|
restrict = not current_user.has_permission(Permission.ViewEtudData)
|
|
|
|
etuds = [ins.etud.to_dict_api(restrict=restrict) for ins in inscriptions]
|
2022-08-30 19:13:21 +02:00
|
|
|
else:
|
|
|
|
etuds = [ins.etud.to_dict_short() for ins in inscriptions]
|
2022-07-19 22:17:10 +02:00
|
|
|
# Ajout des groupes de chaque étudiants
|
2022-07-21 16:46:07 +02:00
|
|
|
# XXX A REVOIR: trop inefficace !
|
2022-07-19 22:17:10 +02:00
|
|
|
for etud in etuds:
|
2022-08-17 18:24:18 +02:00
|
|
|
etud["groups"] = sco_groups.get_etud_groups(
|
2022-12-07 13:21:24 +01:00
|
|
|
etud["id"], formsemestre_id, exclude_default=True
|
2022-08-17 18:24:18 +02:00
|
|
|
)
|
2022-05-13 14:37:02 +02:00
|
|
|
|
2023-04-06 16:10:32 +02:00
|
|
|
return sorted(etuds, key=itemgetter("sort_key"))
|
2022-05-30 16:08:04 +02:00
|
|
|
|
|
|
|
|
2024-02-05 00:10:56 +01:00
|
|
|
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
|
|
|
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
|
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
|
|
|
@as_json
|
|
|
|
def etat_evals(formsemestre_id: int):
|
|
|
|
"""
|
|
|
|
Informations sur l'état des évaluations d'un formsemestre.
|
|
|
|
|
|
|
|
formsemestre_id : l'id d'un semestre
|
|
|
|
|
|
|
|
Exemple de résultat :
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"id": 1, // moduleimpl_id
|
|
|
|
"titre": "Initiation aux réseaux informatiques",
|
|
|
|
"evaluations": [
|
|
|
|
{
|
|
|
|
"id": 1,
|
|
|
|
"description": null,
|
|
|
|
"datetime_epreuve": null,
|
|
|
|
"heure_fin": "09:00:00",
|
|
|
|
"coefficient": "02.00"
|
|
|
|
"is_complete": true,
|
|
|
|
"nb_inscrits": 16,
|
|
|
|
"nb_manquantes": 0,
|
|
|
|
"ABS": 0,
|
|
|
|
"ATT": 0,
|
|
|
|
"EXC": 0,
|
|
|
|
"saisie_notes": {
|
|
|
|
"datetime_debut": "2021-09-11T00:00:00+02:00",
|
|
|
|
"datetime_fin": "2022-08-25T00:00:00+02:00",
|
|
|
|
"datetime_mediane": "2022-03-19T00:00:00+01:00"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
...
|
|
|
|
]
|
|
|
|
},
|
|
|
|
]
|
|
|
|
"""
|
|
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
|
|
|
app.set_sco_dept(formsemestre.departement.acronym)
|
|
|
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
|
|
|
|
|
|
|
result = []
|
|
|
|
for modimpl_id in nt.modimpls_results:
|
|
|
|
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
|
|
|
|
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
|
|
|
|
modimpl_dict = modimpl.to_dict(convert_objects=True, with_module=False)
|
|
|
|
|
|
|
|
list_eval = []
|
|
|
|
for evaluation_id in modimpl_results.evaluations_etat:
|
|
|
|
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
|
|
|
|
evaluation = Evaluation.query.get_or_404(evaluation_id)
|
|
|
|
eval_dict = evaluation.to_dict_api()
|
|
|
|
eval_dict["etat"] = eval_etat.to_dict()
|
|
|
|
|
|
|
|
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
|
|
|
|
eval_dict["nb_notes_manquantes"] = len(
|
|
|
|
modimpl_results.evals_etudids_sans_note[evaluation.id]
|
|
|
|
)
|
|
|
|
eval_dict["nb_notes_abs"] = sum(
|
|
|
|
modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE
|
|
|
|
)
|
|
|
|
eval_dict["nb_notes_att"] = eval_etat.nb_attente
|
|
|
|
eval_dict["nb_notes_exc"] = sum(
|
|
|
|
modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE
|
|
|
|
)
|
|
|
|
|
|
|
|
# Récupération de toutes les notes de l'évaluation
|
|
|
|
# eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id)
|
|
|
|
|
|
|
|
notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all()
|
|
|
|
|
|
|
|
date_debut = None
|
|
|
|
date_fin = None
|
|
|
|
date_mediane = None
|
|
|
|
|
|
|
|
# Si il y a plus d'une note saisie pour l'évaluation
|
|
|
|
if len(notes) >= 1:
|
|
|
|
# Tri des notes en fonction de leurs dates
|
|
|
|
notes_sorted = sorted(notes, key=attrgetter("date"))
|
|
|
|
|
|
|
|
date_debut = notes_sorted[0].date
|
|
|
|
date_fin = notes_sorted[-1].date
|
|
|
|
|
|
|
|
# Note médiane
|
|
|
|
date_mediane = notes_sorted[len(notes_sorted) // 2].date
|
|
|
|
|
|
|
|
eval_dict["saisie_notes"] = {
|
2024-03-19 18:22:02 +01:00
|
|
|
"datetime_debut": (
|
|
|
|
date_debut.isoformat() if date_debut is not None else None
|
|
|
|
),
|
2024-02-05 00:10:56 +01:00
|
|
|
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
|
2024-03-19 18:22:02 +01:00
|
|
|
"datetime_mediane": (
|
|
|
|
date_mediane.isoformat() if date_mediane is not None else None
|
|
|
|
),
|
2024-02-05 00:10:56 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
list_eval.append(eval_dict)
|
|
|
|
|
|
|
|
modimpl_dict["evaluations"] = list_eval
|
|
|
|
result.append(modimpl_dict)
|
|
|
|
return result
|
2022-07-21 16:46:07 +02:00
|
|
|
|
|
|
|
|
2022-07-27 16:03:14 +02:00
|
|
|
@bp.route("/formsemestre/<int:formsemestre_id>/resultats")
|
|
|
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/resultats")
|
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
2023-04-06 16:10:32 +02:00
|
|
|
@as_json
|
2022-07-21 16:46:07 +02:00
|
|
|
def formsemestre_resultat(formsemestre_id: int):
|
|
|
|
"""Tableau récapitulatif des résultats
|
|
|
|
Pour chaque étudiant, son état, ses groupes, ses moyennes d'UE et de modules.
|
2024-06-23 17:40:48 +02:00
|
|
|
|
|
|
|
Si `format=raw`, ne converti pas les valeurs.
|
|
|
|
|
|
|
|
QUERY
|
|
|
|
-----
|
|
|
|
format:<string:format>
|
|
|
|
|
2022-07-21 16:46:07 +02:00
|
|
|
"""
|
2022-07-23 08:46:00 +02:00
|
|
|
format_spec = request.args.get("format", None)
|
|
|
|
if format_spec is not None and format_spec != "raw":
|
2023-02-22 02:13:06 +01:00
|
|
|
return json_error(API_CLIENT_ERROR, "invalid format specification")
|
2022-07-23 08:46:00 +02:00
|
|
|
convert_values = format_spec != "raw"
|
|
|
|
|
2022-07-27 16:03:14 +02:00
|
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
2022-07-21 16:46:07 +02:00
|
|
|
app.set_sco_dept(formsemestre.departement.acronym)
|
|
|
|
res: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
2024-03-24 14:34:55 +01:00
|
|
|
# Ajoute le groupe de chaque partition,
|
|
|
|
etud_groups = sco_groups.get_formsemestre_etuds_groups(formsemestre_id)
|
|
|
|
|
|
|
|
class RowRecapAPI(RowRecap):
|
|
|
|
"""Pour table avec partitions et sort_key"""
|
|
|
|
|
|
|
|
def add_etud_cols(self):
|
|
|
|
"""Ajoute colonnes étudiant: codes, noms"""
|
|
|
|
super().add_etud_cols()
|
|
|
|
self.add_cell("partitions", "partitions", etud_groups.get(self.etud.id, {}))
|
|
|
|
self.add_cell("sort_key", "sort_key", self.etud.sort_key)
|
|
|
|
|
2023-02-04 02:17:30 +01:00
|
|
|
table = TableRecap(
|
2024-03-24 14:34:55 +01:00
|
|
|
res,
|
|
|
|
convert_values=convert_values,
|
|
|
|
include_evaluations=False,
|
|
|
|
mode_jury=False,
|
|
|
|
row_class=RowRecapAPI,
|
2022-07-21 16:46:07 +02:00
|
|
|
)
|
2024-03-24 14:34:55 +01:00
|
|
|
|
2023-01-29 21:52:39 +01:00
|
|
|
rows = table.to_list()
|
|
|
|
|
2024-03-24 14:34:55 +01:00
|
|
|
# for row in rows:
|
|
|
|
# row["partitions"] = etud_groups.get(row["etudid"], {})
|
2023-04-06 16:10:32 +02:00
|
|
|
return rows
|
2023-05-15 23:39:08 +02:00
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
|
|
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/get_groups_auto_assignment")
|
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
|
|
|
@as_json
|
|
|
|
def get_groups_auto_assignment(formsemestre_id: int):
|
|
|
|
"""rend les données"""
|
|
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
|
|
|
response = make_response(formsemestre.groups_auto_assignment_data or b"")
|
|
|
|
response.headers["Content-Type"] = scu.JSON_MIMETYPE
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
@bp.route(
|
|
|
|
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
|
|
|
)
|
|
|
|
@api_web_bp.route(
|
|
|
|
"/formsemestre/<int:formsemestre_id>/save_groups_auto_assignment", methods=["POST"]
|
|
|
|
)
|
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
|
|
|
@as_json
|
|
|
|
def save_groups_auto_assignment(formsemestre_id: int):
|
|
|
|
"""enregistre les données"""
|
|
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
|
|
|
|
|
|
|
if len(request.data) > GROUPS_AUTO_ASSIGNMENT_DATA_MAX:
|
|
|
|
return json_error(413, "data too large")
|
|
|
|
formsemestre.groups_auto_assignment_data = request.data
|
|
|
|
db.session.add(formsemestre)
|
|
|
|
db.session.commit()
|
2023-11-11 18:13:18 +01:00
|
|
|
|
|
|
|
|
|
|
|
@bp.route("/formsemestre/<int:formsemestre_id>/edt")
|
|
|
|
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/edt")
|
|
|
|
@login_required
|
|
|
|
@scodoc
|
|
|
|
@permission_required(Permission.ScoView)
|
|
|
|
@as_json
|
|
|
|
def formsemestre_edt(formsemestre_id: int):
|
2023-11-16 23:34:47 +01:00
|
|
|
"""l'emploi du temps du semestre.
|
2023-11-14 14:06:47 +01:00
|
|
|
Si ok, une liste d'évènements. Sinon, une chaine indiquant un message d'erreur.
|
2023-11-16 23:34:47 +01:00
|
|
|
|
|
|
|
group_ids permet de filtrer sur les groupes ScoDoc.
|
2023-12-24 16:09:07 +01:00
|
|
|
show_modules_titles affiche le titre complet du module (défaut), sinon juste le code.
|
2024-06-23 17:40:48 +02:00
|
|
|
|
|
|
|
QUERY
|
|
|
|
-----
|
|
|
|
group_ids:<string:group_ids>
|
|
|
|
show_modules_titles:<bool:show_modules_titles>
|
|
|
|
|
2023-11-14 14:06:47 +01:00
|
|
|
"""
|
2023-11-11 18:13:18 +01:00
|
|
|
query = FormSemestre.query.filter_by(id=formsemestre_id)
|
|
|
|
if g.scodoc_dept:
|
|
|
|
query = query.filter_by(dept_id=g.scodoc_dept_id)
|
|
|
|
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
|
2023-11-16 23:34:47 +01:00
|
|
|
group_ids = request.args.getlist("group_ids", int)
|
2023-12-24 16:09:07 +01:00
|
|
|
show_modules_titles = scu.to_bool(request.args.get("show_modules_titles", False))
|
|
|
|
return sco_edt_cal.formsemestre_edt_dict(
|
|
|
|
formsemestre, group_ids=group_ids, show_modules_titles=show_modules_titles
|
|
|
|
)
|