Edition en ligne des codes Apgee UE/RCUE. Ajout API.

This commit is contained in:
Emmanuel Viennet 2024-06-21 00:53:52 +02:00
parent c41307c637
commit a8703edfc5
11 changed files with 266 additions and 85 deletions

View File

@ -15,12 +15,14 @@ from flask_login import login_required
import app import app
from app import db, log from app import db, log
from app.api import api_bp as bp, api_web_bp from app.api import api_bp as bp, api_web_bp
from app.models import APO_CODE_STR_LEN
from app.scodoc.sco_utils import json_error from app.scodoc.sco_utils import json_error
from app.decorators import scodoc, permission_required from app.decorators import scodoc, permission_required
from app.models import ( from app.models import (
ApcNiveau, ApcNiveau,
ApcParcours, ApcParcours,
Formation, Formation,
Module,
UniteEns, UniteEns,
) )
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -336,3 +338,166 @@ def desassoc_ue_niveau(ue_id: int):
# "usage web" # "usage web"
flash(f"UE {ue.acronyme} dé-associée") flash(f"UE {ue.acronyme} dé-associée")
return {"status": 0} return {"status": 0}
@bp.route("/ue/<int:ue_id>", methods=["GET"])
@api_web_bp.route("/ue/<int:ue_id>", methods=["GET"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
def get_ue(ue_id: int):
"""Renvoie l'UE"""
query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
ue: UniteEns = query.first_or_404()
return ue.to_dict(convert_objects=True)
@bp.route("/module/<int:module_id>", methods=["GET"])
@api_web_bp.route("/module/<int:module_id>", methods=["GET"])
@login_required
@scodoc
@permission_required(Permission.ScoView)
def get_module(module_id: int):
"""Renvoie le module"""
query = Module.query.filter_by(id=module_id)
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
module: Module = query.first_or_404()
return module.to_dict(convert_objects=True)
@bp.route("/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"])
@api_web_bp.route(
"/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
)
@bp.route(
"/ue/<int:ue_id>/set_code_apogee", defaults={"code_apogee": ""}, methods=["POST"]
)
@api_web_bp.route(
"/ue/<int:ue_id>/set_code_apogee", defaults={"code_apogee": ""}, methods=["POST"]
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
def ue_set_code_apogee(ue_id: int, code_apogee: str = ""):
"""Change le code Apogée de l'UE.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules.
(Ce changement peut être fait sur formation verrouillée)
Si code_apogee n'est pas spécifié ou vide,
utilise l'argument value du POST (utilisé par jinplace.js)
Le retour est une chaîne (le code enregistré), pas json.
"""
if not code_apogee:
code_apogee = request.form.get("value", "")
query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
ue: UniteEns = query.first_or_404()
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
log(f"API ue_set_code_apogee: ue_id={ue.id} code_apogee={code_apogee}")
ue.code_apogee = code_apogee
db.session.add(ue)
db.session.commit()
return code_apogee or ""
@bp.route("/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>", methods=["POST"])
@api_web_bp.route(
"/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>", methods=["POST"]
)
@bp.route(
"/ue/<int:ue_id>/set_code_apogee_rcue",
defaults={"code_apogee": ""},
methods=["POST"],
)
@api_web_bp.route(
"/ue/<int:ue_id>/set_code_apogee_rcue",
defaults={"code_apogee": ""},
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""):
"""Change le code Apogée du RCUE de l'UE.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules.
(Ce changement peut être fait sur formation verrouillée)
Si code_apogee n'est pas spécifié ou vide,
utilise l'argument value du POST (utilisé par jinplace.js)
Le retour est une chaîne (le code enregistré), pas json.
"""
if not code_apogee:
code_apogee = request.form.get("value", "")
query = UniteEns.query.filter_by(id=ue_id)
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
ue: UniteEns = query.first_or_404()
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
log(f"API ue_set_code_apogee_rcue: ue_id={ue.id} code_apogee={code_apogee}")
ue.code_apogee_rcue = code_apogee
db.session.add(ue)
db.session.commit()
return code_apogee or ""
@bp.route(
"/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
methods=["POST"],
)
@api_web_bp.route(
"/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
methods=["POST"],
)
@bp.route(
"/module/<int:module_id>/set_code_apogee",
defaults={"code_apogee": ""},
methods=["POST"],
)
@api_web_bp.route(
"/module/<int:module_id>/set_code_apogee",
defaults={"code_apogee": ""},
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
def module_set_code_apogee(module_id: int, code_apogee: str = ""):
"""Change le code Apogée du module.
Le code est une chaîne, avec éventuellement plusieurs valeurs séparées
par des virgules.
(Ce changement peut être fait sur formation verrouillée)
Si code_apogee n'est pas spécifié ou vide,
utilise l'argument value du POST (utilisé par jinplace.js)
Le retour est une chaîne (le code enregistré), pas json.
"""
if not code_apogee:
code_apogee = request.form.get("value", "")
query = Module.query.filter_by(id=module_id)
if g.scodoc_dept:
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
module: Module = query.first_or_404()
code_apogee = code_apogee.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
log(f"API module_set_code_apogee: module_id={module.id} code_apogee={code_apogee}")
module.code_apogee = code_apogee
db.session.add(module)
db.session.commit()
return code_apogee or ""

View File

@ -375,7 +375,7 @@ class ApoEtud(dict):
if module_code_found: if module_code_found:
return VOID_APO_RES return VOID_APO_RES
# RCUE du BUT # RCUE du BUT (validations enregistrées seulement, pas avant jury)
if res.is_apc: if res.is_apc:
for val_rcue in ApcValidationRCUE.query.filter_by( for val_rcue in ApcValidationRCUE.query.filter_by(
etudid=etudid, formsemestre_id=sem["formsemestre_id"] etudid=etudid, formsemestre_id=sem["formsemestre_id"]
@ -909,7 +909,7 @@ class ApoData:
# log('codes_by_sem=%s' % pprint.pformat(codes_by_sem)) # log('codes_by_sem=%s' % pprint.pformat(codes_by_sem))
return codes_by_sem return codes_by_sem
def build_cr_table(self): def build_cr_table(self) -> GenTable:
"""Table compte rendu des décisions""" """Table compte rendu des décisions"""
rows = [] # tableau compte rendu des decisions rows = [] # tableau compte rendu des decisions
for apo_etud in self.etuds: for apo_etud in self.etuds:
@ -931,14 +931,14 @@ class ApoData:
columns_ids = ["NIP", "nom", "prenom"] columns_ids = ["NIP", "nom", "prenom"]
columns_ids.extend(("etape", "etape_note", "est_NAR", "commentaire")) columns_ids.extend(("etape", "etape_note", "est_NAR", "commentaire"))
T = GenTable( table = GenTable(
columns_ids=columns_ids, columns_ids=columns_ids,
titles=dict(zip(columns_ids, columns_ids)), titles=dict(zip(columns_ids, columns_ids)),
rows=rows, rows=rows,
table_id="build_cr_table", table_id="build_cr_table",
xls_sheet_name="Decisions ScoDoc", xls_sheet_name="Decisions ScoDoc",
) )
return T return table
def build_adsup_table(self): def build_adsup_table(self):
"""Construit une table listant les ADSUP émis depuis les formsemestres """Construit une table listant les ADSUP émis depuis les formsemestres

View File

@ -890,23 +890,6 @@ def module_edit(
) )
# Edition en ligne du code Apogee
def edit_module_set_code_apogee(id=None, value=None):
"Set UE code apogee"
module_id = id
value = str(value).strip("-_ \t")
log("edit_module_set_code_apogee: module_id=%s code_apogee=%s" % (module_id, value))
modules = module_list(args={"module_id": module_id})
if not modules:
return "module invalide" # should not occur
do_module_edit({"module_id": module_id, "code_apogee": value})
if not value:
value = scu.APO_MISSING_CODE_STR
return value
def module_table(formation_id): def module_table(formation_id):
"""Liste des modules de la formation """Liste des modules de la formation
(XXX inutile ou a revoir) (XXX inutile ou a revoir)

View File

@ -1124,12 +1124,18 @@ def _ue_table_ues(
klass = "span_apo_edit" klass = "span_apo_edit"
else: else:
klass = "" klass = ""
ue["code_apogee_str"] = ( edit_url = url_for(
""", Apo: <span class="%s" data-url="edit_ue_set_code_apogee" id="%s" data-placeholder="%s">""" "apiweb.ue_set_code_apogee",
% (klass, ue["ue_id"], scu.APO_MISSING_CODE_STR) scodoc_dept=g.scodoc_dept,
+ (ue["code_apogee"] or "") ue_id=ue["ue_id"],
+ "</span>"
) )
ue[
"code_apogee_str"
] = f""", Apo: <span
class="{klass}" data-url="{edit_url}" id="{ue['ue_id']}"
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
ue["code_apogee"] or ""
}</span>"""
if cur_ue_semestre_id != ue["semestre_id"]: if cur_ue_semestre_id != ue["semestre_id"]:
cur_ue_semestre_id = ue["semestre_id"] cur_ue_semestre_id = ue["semestre_id"]
@ -1363,16 +1369,17 @@ def _ue_table_modules(
heurescoef = ( heurescoef = (
"%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod "%(heures_cours)s/%(heures_td)s/%(heures_tp)s, coef. %(coefficient)s" % mod
) )
if mod_editable: edit_url = url_for(
klass = "span_apo_edit" "apiweb.module_set_code_apogee",
else: scodoc_dept=g.scodoc_dept,
klass = "" module_id=mod["module_id"],
heurescoef += (
', Apo: <span class="%s" data-url="edit_module_set_code_apogee" id="%s" data-placeholder="%s">'
% (klass, mod["module_id"], scu.APO_MISSING_CODE_STR)
+ (mod["code_apogee"] or "")
+ "</span>"
) )
heurescoef += f""", Apo: <span
class="{'span_apo_edit' if editable else ''}"
data-url="{edit_url}" id="{mod["module_id"]}"
data-placeholder="{scu.APO_MISSING_CODE_STR}">{
mod["code_apogee"] or ""
}</span>"""
if tag_editable: if tag_editable:
tag_cls = "module_tag_editor" tag_cls = "module_tag_editor"
else: else:
@ -1508,28 +1515,6 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
formation.invalidate_module_coefs() formation.invalidate_module_coefs()
# essai edition en ligne:
def edit_ue_set_code_apogee(id=None, value=None):
"set UE code apogee"
ue_id = id
value = value.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
log(f"edit_ue_set_code_apogee: ue_id={ue_id} code_apogee={value}")
ues = ue_list(args={"ue_id": ue_id})
if not ues:
return "ue invalide"
do_ue_edit(
{"ue_id": ue_id, "code_apogee": value},
bypass_lock=True,
dont_invalidate_cache=False,
)
if not value:
value = scu.APO_MISSING_CODE_STR
return value
def ue_is_locked(ue_id): def ue_is_locked(ue_id):
"""True if UE should not be modified """True if UE should not be modified
(contains modules used in a locked formsemestre) (contains modules used in a locked formsemestre)

View File

@ -56,7 +56,9 @@
({{mod.heures_cours|default("&nbsp;",true)|safe}}/{{mod.heures_td|default("&nbsp;",true)|safe}}/{{mod.heures_tp|default("&nbsp;",true)|safe}}, ({{mod.heures_cours|default("&nbsp;",true)|safe}}/{{mod.heures_td|default("&nbsp;",true)|safe}}/{{mod.heures_tp|default("&nbsp;",true)|safe}},
{% else %} {% else %}
({% endif %}Apo:<span class="{% if editable %}span_apo_edit{% endif %}" ({% endif %}Apo:<span class="{% if editable %}span_apo_edit{% endif %}"
data-url="edit_module_set_code_apogee" id="{{mod.id}}" data-placeholder="{{scu.APO_MISSING_CODE_STR}}"> data-url="{{url_for('apiweb.module_set_code_apogee',
scodoc_dept=g.scodoc_dept, module_id=mod.id)
}}" id="{{mod.id}}" data-placeholder="{{scu.APO_MISSING_CODE_STR}}">
{{mod.code_apogee|default("", true)}}</span>) {{mod.code_apogee|default("", true)}}</span>)
<span class="ue_coefs_list"><a title="coefs vers les UEs"> <span class="ue_coefs_list"><a title="coefs vers les UEs">

View File

@ -53,10 +53,21 @@
}} ECTS }} ECTS
{%- endif -%} {%- endif -%}
{{ virg() }} Apo: {{ virg() }} Apo:
<span class="{% if editable %}span_apo_edit{% endif %}" <span title="code UE dans Apogée"
data-url="edit_ue_set_code_apogee" class="{% if editable %}span_apo_edit{% endif %}"
data-url="{{url_for('apiweb.ue_set_code_apogee',
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}"
id="{{ue.id}}" data-placeholder="{{scu.APO_MISSING_CODE_STR}}"> id="{{ue.id}}" data-placeholder="{{scu.APO_MISSING_CODE_STR}}">
{{ue.code_apogee or '' {{ue.code_apogee or ''
}}</span>
RCUE: <span title="code RCUE dans Apogée"
class="{% if editable %}span_apo_edit{% endif %}"
data-url="{{url_for('apiweb.ue_set_code_apogee_rcue',
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}"
id="{{ue.id}}" data-placeholder="{{scu.APO_MISSING_CODE_STR}}">
{{ue.code_apogee_rcue or ''
}}</span>) }}</span>)
</span> </span>

View File

@ -551,12 +551,6 @@ def ue_sharing_code():
) )
sco_publish(
"/edit_ue_set_code_apogee",
sco_edit_ue.edit_ue_set_code_apogee,
Permission.EditFormation,
methods=["POST"],
)
sco_publish( sco_publish(
"/formsemestre_edit_uecoefs", "/formsemestre_edit_uecoefs",
sco_formsemestre_edit.formsemestre_edit_uecoefs, sco_formsemestre_edit.formsemestre_edit_uecoefs,
@ -619,12 +613,6 @@ sco_publish(
Permission.EditFormation, Permission.EditFormation,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
sco_publish(
"/edit_module_set_code_apogee",
sco_edit_module.edit_module_set_code_apogee,
Permission.EditFormation,
methods=["GET", "POST"],
)
sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView) sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView)
sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView)

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.6.978" SCOVERSION = "9.6.979"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -119,8 +119,12 @@ def GET(path: str, headers: dict = None, errmsg=None, dept=None, raw=False):
raise APIError("Unknown returned content {r.headers.get('Content-Type', None} !\n") raise APIError("Unknown returned content {r.headers.get('Content-Type', None} !\n")
def POST_JSON(path: str, data: dict = {}, headers: dict = None, errmsg=None, dept=None): def POST_JSON(
"""Post""" path: str, data: dict = {}, headers: dict = None, errmsg=None, dept=None, raw=False
):
"""Post
Decode réponse en json, sauf si raw.
"""
if dept: if dept:
url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path
else: else:
@ -134,7 +138,7 @@ def POST_JSON(path: str, data: dict = {}, headers: dict = None, errmsg=None, dep
) )
if r.status_code != 200: if r.status_code != 200:
raise APIError(errmsg or f"erreur status={r.status_code} !", r.json()) raise APIError(errmsg or f"erreur status={r.status_code} !", r.json())
return r.json() # decode la reponse JSON return r if raw else r.json() # decode la reponse JSON
def check_fields(data: dict, fields: dict = None): def check_fields(data: dict, fields: dict = None):

View File

@ -20,7 +20,14 @@ Utilisation :
import requests import requests
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from tests.api.setup_test_api import API_URL, CHECK_CERTIFICATE, api_headers from tests.api.setup_test_api import (
api_admin_headers,
api_headers,
API_URL,
CHECK_CERTIFICATE,
GET,
POST_JSON,
)
from tests.api.tools_test_api import ( from tests.api.tools_test_api import (
verify_fields, verify_fields,
FORMATION_EXPORT_FIELDS, FORMATION_EXPORT_FIELDS,
@ -320,3 +327,40 @@ def test_referentiel_competences(api_headers):
timeout=scu.SCO_TEST_API_TIMEOUT, timeout=scu.SCO_TEST_API_TIMEOUT,
) )
assert r_error.status_code == 404 assert r_error.status_code == 404
def test_api_ue_apo(api_admin_headers):
"""Routes
/ue/<int:ue_id>
/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>
/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>
"""
ue_id = 1
ue = GET(path=f"/ue/{ue_id}", headers=api_admin_headers)
assert ue["id"] == ue_id
r = POST_JSON(f"/ue/{ue_id}/set_code_apogee/APOUE", {}, api_admin_headers, raw=True)
assert r.text == "APOUE"
r = POST_JSON(
f"/ue/{ue_id}/set_code_apogee_rcue/APORCUE", {}, api_admin_headers, raw=True
)
assert r.text == "APORCUE"
ue = GET(path=f"/ue/{ue_id}", headers=api_admin_headers)
assert ue["code_apogee"] == "APOUE"
assert ue["code_apogee_rcue"] == "APORCUE"
def test_api_module_apo(api_admin_headers):
"""Routes
/module/<int:module_id>
/module/<int:module_id>/set_code_apogee/<string:code_apogee>
"""
module_id = 1
module = GET(path=f"/module/{module_id}", headers=api_admin_headers)
assert module["id"] == module_id
assert module["code_apogee"] == ""
r = POST_JSON(
f"/module/{module_id}/set_code_apogee/APOMOD", {}, api_admin_headers, raw=True
)
assert r.text == "APOMOD"
module = GET(path=f"/module/{module_id}", headers=api_admin_headers)
assert module["code_apogee"] == "APOMOD"

View File

@ -28,7 +28,6 @@ from tests.api.setup_test_api import (
API_URL, API_URL,
CHECK_CERTIFICATE, CHECK_CERTIFICATE,
GET, GET,
POST_JSON,
api_headers, api_headers,
) )