1
0
forked from ScoDoc/ScoDoc

Recherche étudiant: réparations et améliorations

This commit is contained in:
Emmanuel Viennet 2024-07-03 21:32:33 +02:00
parent 03cea5daf7
commit 5ec598e693
10 changed files with 347 additions and 197 deletions

View File

@ -250,8 +250,8 @@ def referentiel_competences(formation_id: int):
return formation.referentiel_competence.to_dict() return formation.referentiel_competence.to_dict()
@bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"]) @bp.route("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
@api_web_bp.route("/set_ue_parcours/<int:ue_id>", methods=["POST"]) @api_web_bp.route("/formation/ue/<int:ue_id>/set_parcours", methods=["POST"])
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.EditFormation) @permission_required(Permission.EditFormation)
@ -280,11 +280,11 @@ def set_ue_parcours(ue_id: int):
@bp.route( @bp.route(
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>", "/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
methods=["POST"], methods=["POST"],
) )
@api_web_bp.route( @api_web_bp.route(
"/assoc_ue_niveau/<int:ue_id>/<int:niveau_id>", "/formation/ue/<int:ue_id>/assoc_niveau/<int:niveau_id>",
methods=["POST"], methods=["POST"],
) )
@login_required @login_required
@ -309,11 +309,11 @@ def assoc_ue_niveau(ue_id: int, niveau_id: int):
@bp.route( @bp.route(
"/desassoc_ue_niveau/<int:ue_id>", "/formation/ue/<int:ue_id>/desassoc_niveau",
methods=["POST"], methods=["POST"],
) )
@api_web_bp.route( @api_web_bp.route(
"/desassoc_ue_niveau/<int:ue_id>", "/formation/ue/<int:ue_id>/desassoc_niveau",
methods=["POST"], methods=["POST"],
) )
@login_required @login_required
@ -340,8 +340,8 @@ def desassoc_ue_niveau(ue_id: int):
return {"status": 0} return {"status": 0}
@bp.route("/ue/<int:ue_id>", methods=["GET"]) @bp.route("/formation/ue/<int:ue_id>", methods=["GET"])
@api_web_bp.route("/ue/<int:ue_id>", methods=["GET"]) @api_web_bp.route("/formation/ue/<int:ue_id>", methods=["GET"])
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@ -354,8 +354,8 @@ def get_ue(ue_id: int):
return ue.to_dict(convert_objects=True) return ue.to_dict(convert_objects=True)
@bp.route("/module/<int:module_id>", methods=["GET"]) @bp.route("/formation/module/<int:module_id>", methods=["GET"])
@api_web_bp.route("/module/<int:module_id>", methods=["GET"]) @api_web_bp.route("/formation/module/<int:module_id>", methods=["GET"])
@login_required @login_required
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@ -368,17 +368,23 @@ def get_module(module_id: int):
return module.to_dict(convert_objects=True) return module.to_dict(convert_objects=True)
@bp.route("/ue/set_code_apogee", methods=["POST"]) @bp.route("/formation/ue/set_code_apogee", methods=["POST"])
@api_web_bp.route("/ue/set_code_apogee", methods=["POST"]) @api_web_bp.route("/ue/set_code_apogee", methods=["POST"])
@bp.route("/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]) @bp.route(
"/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
)
@api_web_bp.route( @api_web_bp.route(
"/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"] "/formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>", methods=["POST"]
) )
@bp.route( @bp.route(
"/ue/<int:ue_id>/set_code_apogee", defaults={"code_apogee": ""}, methods=["POST"] "/formation/ue/<int:ue_id>/set_code_apogee",
defaults={"code_apogee": ""},
methods=["POST"],
) )
@api_web_bp.route( @api_web_bp.route(
"/ue/<int:ue_id>/set_code_apogee", defaults={"code_apogee": ""}, methods=["POST"] "/formation/ue/<int:ue_id>/set_code_apogee",
defaults={"code_apogee": ""},
methods=["POST"],
) )
@login_required @login_required
@scodoc @scodoc
@ -416,17 +422,21 @@ def ue_set_code_apogee(ue_id: int | None = None, code_apogee: str = ""):
return code_apogee or "" return code_apogee or ""
@bp.route("/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>", methods=["POST"]) @bp.route(
"/formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
methods=["POST"],
)
@api_web_bp.route( @api_web_bp.route(
"/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>", methods=["POST"] "/formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>",
methods=["POST"],
) )
@bp.route( @bp.route(
"/ue/<int:ue_id>/set_code_apogee_rcue", "/formation/ue/<int:ue_id>/set_code_apogee_rcue",
defaults={"code_apogee": ""}, defaults={"code_apogee": ""},
methods=["POST"], methods=["POST"],
) )
@api_web_bp.route( @api_web_bp.route(
"/ue/<int:ue_id>/set_code_apogee_rcue", "/formation/ue/<int:ue_id>/set_code_apogee_rcue",
defaults={"code_apogee": ""}, defaults={"code_apogee": ""},
methods=["POST"], methods=["POST"],
) )
@ -461,23 +471,23 @@ def ue_set_code_apogee_rcue(ue_id: int, code_apogee: str = ""):
return code_apogee or "" return code_apogee or ""
@bp.route("/module/set_code_apogee", methods=["POST"]) @bp.route("/formation/module/set_code_apogee", methods=["POST"])
@api_web_bp.route("/module/set_code_apogee", methods=["POST"]) @api_web_bp.route("/formation/module/set_code_apogee", methods=["POST"])
@bp.route( @bp.route(
"/module/<int:module_id>/set_code_apogee/<string:code_apogee>", "/formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
methods=["POST"], methods=["POST"],
) )
@api_web_bp.route( @api_web_bp.route(
"/module/<int:module_id>/set_code_apogee/<string:code_apogee>", "/formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>",
methods=["POST"], methods=["POST"],
) )
@bp.route( @bp.route(
"/module/<int:module_id>/set_code_apogee", "/formation/module/<int:module_id>/set_code_apogee",
defaults={"code_apogee": ""}, defaults={"code_apogee": ""},
methods=["POST"], methods=["POST"],
) )
@api_web_bp.route( @api_web_bp.route(
"/module/<int:module_id>/set_code_apogee", "/formation/module/<int:module_id>/set_code_apogee",
defaults={"code_apogee": ""}, defaults={"code_apogee": ""},
methods=["POST"], methods=["POST"],
) )
@ -515,3 +525,54 @@ def module_set_code_apogee(module_id: int | None = None, code_apogee: str = ""):
db.session.add(module) db.session.add(module)
db.session.commit() db.session.commit()
return code_apogee or "" return code_apogee or ""
@bp.route(
"/formation/module/<int:module_id>/edit",
methods=["POST"],
)
@api_web_bp.route(
"/formation/module/<int:module_id>/edit",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
@as_json
def module_edit(module_id: int):
"""Édition d'un module. Renvoie le module en json."""
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()
args = request.get_json(force=True) # may raise 400 Bad Request
module.from_dict(args)
db.session.commit()
db.session.refresh(module)
log(f"API module_edit: module_id={module.id} args={args}")
r = module.to_dict(convert_objects=True, with_parcours_ids=True)
return r
@bp.route(
"/formation/ue/<int:ue_id>/edit",
methods=["POST"],
)
@api_web_bp.route(
"/formation/ue/<int:ue_id>/edit",
methods=["POST"],
)
@login_required
@scodoc
@permission_required(Permission.EditFormation)
@as_json
def ue_edit(ue_id: int):
"""Édition d'une UE. Renvoie l'UE en json."""
ue = UniteEns.get_ue(ue_id)
args = request.get_json(force=True) # may raise 400 Bad Request
ue.from_dict(args)
db.session.commit()
db.session.refresh(ue)
log(f"API ue_edit: ue_id={ue.id} args={args}")
r = ue.to_dict(convert_objects=True)
return r

View File

@ -106,31 +106,74 @@ class Module(models.ScoDocModel):
return args_dict return args_dict
@classmethod @classmethod
def filter_model_attributes(cls, data: dict, excluded: set[str] = None) -> dict: def filter_model_attributes(cls, args: dict, excluded: set[str] = None) -> dict:
"""Returns a copy of dict with only the keys belonging to the Model and not in excluded. """Returns a copy of dict with only the keys belonging to the Model and not in excluded.
Add 'id' to excluded.""" Add 'id' to excluded."""
# on ne peut pas affecter directement parcours # on ne peut pas affecter directement parcours
return super().filter_model_attributes(data, (excluded or set()) | {"parcours"}) return super().filter_model_attributes(args, (excluded or set()) | {"parcours"})
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
"""Update object's fields given in dict. Add to session but don't commit.
True if modification.
- can't change ue nor formation
- can change matiere_id, iff new matiere in same ue
- can change parcours: parcours list of ApcParcour id or instances.
"""
# Vérifie les changements de matiere
new_matiere_id = args.get("matiere_id", self.matiere_id)
if new_matiere_id != self.matiere_id:
# exists ?
from app.models import Matiere
matiere = db.session.get(Matiere, new_matiere_id)
if matiere is None or matiere.ue_id != self.ue_id:
raise ScoValueError("invalid matiere")
modified = super().from_dict(
args, excluded=(excluded or set()) | {"formation_id", "ue_id"}
)
existing_parcours = {p.id for p in self.parcours}
new_parcours = args.get("parcours", []) or []
if existing_parcours != set(new_parcours):
self._set_parcours_from_list(new_parcours)
return True
return modified
@classmethod @classmethod
def create_from_dict(cls, data: dict) -> "Module": def create_from_dict(cls, data: dict) -> "Module":
"""Create from given dict, add parcours""" """Create from given dict, add parcours"""
mod = super().create_from_dict(data) module = super().create_from_dict(data)
for p in data.get("parcours", []) or []: module._set_parcours_from_list(data.get("parcours", []) or [])
return module
def _set_parcours_from_list(self, parcours: list[ApcParcours | int]):
"""Ajoute ces parcours à la liste des parcours du module.
Chaque élément est soit un objet parcours soit un id.
S'assure que chaque parcours est dans le référentiel de compétence
associé à la formation du module.
"""
for p in parcours:
if isinstance(p, ApcParcours): if isinstance(p, ApcParcours):
parcour: ApcParcours = p parcour: ApcParcours = p
if p.referentiel_id != self.formation.referentiel_competence.id:
raise ScoValueError("Parcours hors référentiel du module")
else: else:
pid = int(p) try:
query = ApcParcours.query.filter_by(id=pid) pid = int(p)
except ValueError as exc:
raise ScoValueError("id de parcours invalide") from exc
query = (
ApcParcours.query.filter_by(id=pid)
.join(ApcReferentielCompetences)
.filter_by(id=self.formation.referentiel_competence.id)
)
if g.scodoc_dept: if g.scodoc_dept:
query = query.join(ApcReferentielCompetences).filter_by( query = query.filter_by(dept_id=g.scodoc_dept_id)
dept_id=g.scodoc_dept_id
)
parcour: ApcParcours = query.first() parcour: ApcParcours = query.first()
if parcour is None: if parcour is None:
raise ScoValueError("Parcours invalide") raise ScoValueError("Parcours invalide")
mod.parcours.append(parcour) self.parcours.append(parcour)
return mod
def clone(self): def clone(self):
"""Create a new copy of this module.""" """Create a new copy of this module."""
@ -163,14 +206,25 @@ class Module(models.ScoDocModel):
mod.app_critiques.append(app_critique) mod.app_critiques.append(app_critique)
return mod return mod
def to_dict(self, convert_objects=False, with_matiere=False, with_ue=False) -> dict: def to_dict(
self,
convert_objects=False,
with_matiere=False,
with_ue=False,
with_parcours_ids=False,
) -> dict:
"""If convert_objects, convert all attributes to native types """If convert_objects, convert all attributes to native types
(suitable jor json encoding). (suitable jor json encoding).
If convert_objects and with_parcours_ids, give parcours as a list of id (API)
""" """
d = dict(self.__dict__) d = dict(self.__dict__)
d.pop("_sa_instance_state", None) d.pop("_sa_instance_state", None)
d.pop("formation", None)
if convert_objects: if convert_objects:
d["parcours"] = [p.to_dict() for p in self.parcours] if with_parcours_ids:
d["parcours"] = [p.id for p in self.parcours]
else:
d["parcours"] = [p.to_dict() for p in self.parcours]
d["ue_coefs"] = [ d["ue_coefs"] = [
c.to_dict(convert_objects=convert_objects) for c in self.ue_coefs c.to_dict(convert_objects=convert_objects) for c in self.ue_coefs
] ]

View File

@ -1,7 +1,7 @@
"""ScoDoc 9 models : Unités d'Enseignement (UE) """ScoDoc 9 models : Unités d'Enseignement (UE)
""" """
from flask import g from flask import abort, g
import pandas as pd import pandas as pd
from app import db, log from app import db, log
@ -123,6 +123,16 @@ class UniteEns(models.ScoDocModel):
return args return args
def from_dict(self, args: dict, excluded: set[str] | None = None) -> bool:
"""Update object's fields given in dict. Add to session but don't commit.
True if modification.
- can't change formation nor niveau_competence
"""
return super().from_dict(
args,
excluded=(excluded or set()) | {"formation_id", "niveau_competence_id"},
)
def to_dict(self, convert_objects=False, with_module_ue_coefs=True): def to_dict(self, convert_objects=False, with_module_ue_coefs=True):
"""as a dict, with the same conversions as in ScoDoc7. """as a dict, with the same conversions as in ScoDoc7.
If convert_objects, convert all attributes to native types If convert_objects, convert all attributes to native types
@ -255,6 +265,30 @@ class UniteEns(models.ScoDocModel):
log(f"ue.set_ects( ue_id={self.id}, acronyme={self.acronyme}, ects={ects} )") log(f"ue.set_ects( ue_id={self.id}, acronyme={self.acronyme}, ects={ects} )")
db.session.add(self) db.session.add(self)
@classmethod
def get_ue(cls, ue_id: int, accept_none=False) -> "UniteEns":
"""UE ou 404 (ou None si accept_none),
cherche uniquement dans le département courant.
Si accept_none, return None si l'id est invalide ou inexistant.
"""
if not isinstance(ue_id, int):
try:
ue_id = int(ue_id)
except (TypeError, ValueError):
if accept_none:
return None
abort(404, "ue_id invalide")
query = cls.query.filter_by(id=ue_id)
if g.scodoc_dept:
from app.models import Formation
query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id)
if accept_none:
return query.first()
return query.first_or_404()
def get_ressources(self): def get_ressources(self):
"Liste des modules ressources rattachés à cette UE" "Liste des modules ressources rattachés à cette UE"
return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all() return self.modules.filter_by(module_type=scu.ModuleType.RESSOURCE).all()

View File

@ -97,6 +97,7 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
<link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" /> <link href="{scu.STATIC_DIR}/css/scodoc.css" rel="stylesheet" type="text/css" />
<link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" /> <link href="{scu.STATIC_DIR}/css/menu.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" type="text/css" href="{scu.STATIC_DIR}/DataTables/datatables.min.css" />
<script src="{scu.STATIC_DIR}/libjs/menu.js"></script> <script src="{scu.STATIC_DIR}/libjs/menu.js"></script>
<script src="{scu.STATIC_DIR}/libjs/bubble.js"></script> <script src="{scu.STATIC_DIR}/libjs/bubble.js"></script>
<script> <script>
@ -115,6 +116,7 @@ _HTML_BEGIN = f"""<!DOCTYPE html>
<script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script> <script src="{scu.STATIC_DIR}/libjs/jquery.field.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script> <script src="{scu.STATIC_DIR}/libjs/jquery-ui-1.10.4.custom/js/jquery-ui-1.10.4.custom.min.js"></script>
<script src="{scu.STATIC_DIR}/DataTables/datatables.min.js"></script>
<script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script> <script src="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.js"></script>
<link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" /> <link type="text/css" rel="stylesheet" href="{scu.STATIC_DIR}/libjs/qtip/jquery.qtip-3.0.3.min.css" />

View File

@ -30,8 +30,9 @@
import flask import flask
from flask import url_for, g, request from flask import url_for, g, request
from flask_login import current_user from flask_login import current_user
import sqlalchemy as sa
import app from app import db
from app.models import Departement, Identite from app.models import Departement, Identite
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
@ -101,9 +102,12 @@ def form_search_etud(
return "\n".join(H) return "\n".join(H)
def search_etuds_infos_from_exp(expnom: str = "") -> list[Identite]: def search_etuds_infos_from_exp(
expnom: str = "", dept_id: int | None = None
) -> list[Identite]:
"""Cherche étudiants, expnom peut être, dans cet ordre: """Cherche étudiants, expnom peut être, dans cet ordre:
un etudid (int), un code NIP, ou une partie d'un nom (case insensitive). un etudid (int), un code NIP, ou une partie d'un nom (case insensitive).
Si dept_id est None, cherche dans le dept courant, sinon cherche dans le dept indiqué.
""" """
if not isinstance(expnom, int) and len(expnom) <= 1: if not isinstance(expnom, int) and len(expnom) <= 1:
return [] # si expnom est trop court, n'affiche rien return [] # si expnom est trop court, n'affiche rien
@ -111,23 +115,31 @@ def search_etuds_infos_from_exp(expnom: str = "") -> list[Identite]:
etudid = int(expnom) etudid = int(expnom)
except ValueError: except ValueError:
etudid = None etudid = None
dept_id = g.scodoc_dept_id if dept_id is None else dept_id
if etudid is not None: if etudid is not None:
etud = Identite.query.filter_by(dept_id=g.scodoc_dept_id, id=etudid).first() etud = Identite.query.filter_by(dept_id=dept_id, id=etudid).first()
if etud: if etud:
return [etud] return [etud]
expnom_str = str(expnom) expnom_str = str(expnom)
if scu.is_valid_code_nip(expnom_str): if scu.is_valid_code_nip(expnom_str):
etuds = Identite.query.filter_by( etuds = sorted(
dept_id=g.scodoc_dept_id, code_nip=expnom_str Identite.query.filter_by(dept_id=dept_id, code_nip=expnom_str).all(),
).all() key=lambda e: e.sort_key,
)
if etuds: if etuds:
return etuds return etuds
try:
return ( return sorted(
Identite.query.filter_by(dept_id=g.scodoc_dept_id) Identite.query.filter_by(dept_id=dept_id)
.filter(Identite.nom.op("~*")(expnom_str)) .filter(
.all() Identite.nom.op("~*")(expnom_str)
) ) # ~* matches regular expression, case-insensitive
.all(),
key=lambda e: e.sort_key,
)
except sa.exc.DataError:
db.session.rollback()
return []
def search_etud_in_dept(expnom=""): def search_etud_in_dept(expnom=""):
@ -191,7 +203,7 @@ def search_etud_in_dept(expnom=""):
# Choix dans la liste des résultats: # Choix dans la liste des résultats:
rows = [] rows = []
e: Identite e: Identite
for e in etuds: for e in sorted(etuds, key=lambda e: e.sort_key):
url_args["etudid"] = e.id url_args["etudid"] = e.id
target = url_for(endpoint, **url_args) target = url_for(endpoint, **url_args)
cur_inscription = e.inscription_courante() cur_inscription = e.inscription_courante()
@ -219,6 +231,7 @@ def search_etud_in_dept(expnom=""):
"inscription_target": target, "inscription_target": target,
"groupes": groupes, "groupes": groupes,
"nomprenom": e.nomprenom, "nomprenom": e.nomprenom,
"_nomprenom_order": e.sort_key,
"_nomprenom_target": target, "_nomprenom_target": target,
"_nomprenom_td_attrs": f'id="{e.id}" class="etudinfo"', "_nomprenom_td_attrs": f'id="{e.id}" class="etudinfo"',
} }
@ -287,71 +300,36 @@ def search_etud_by_name(term: str) -> list:
{ "label" : "<nip> <nom> <prenom>", "value" : etudid } { "label" : "<nip> <nom> <prenom>", "value" : etudid }
""" """
may_be_nip = scu.is_valid_code_nip(term) may_be_nip = scu.is_valid_code_nip(term)
# term = term.upper() # conserve les accents etuds = search_etuds_infos_from_exp(term)
term = term.upper()
if (
not scu.ALPHANUM_EXP.match(term) # n'autorise pas les caractères spéciaux
and not may_be_nip
):
data = []
else:
if may_be_nip:
r = ndb.SimpleDictFetch(
"""SELECT nom, prenom, code_nip
FROM identite
WHERE
dept_id = %(dept_id)s
AND code_nip ILIKE %(beginning)s
ORDER BY nom
""",
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
)
data = [
{
"label": "%s %s %s"
% (x["code_nip"], x["nom"], scu.format_prenom(x["prenom"])),
"value": x["code_nip"],
}
for x in r
]
else:
r = ndb.SimpleDictFetch(
"""SELECT id AS etudid, nom, prenom
FROM identite
WHERE
dept_id = %(dept_id)s
AND nom ILIKE %(beginning)s
ORDER BY nom
""",
{"beginning": term + "%", "dept_id": g.scodoc_dept_id},
)
data = [ return [
{ {
"label": "%s %s" % (x["nom"], scu.format_prenom(x["prenom"])), "label": f"""{(etud.code_nip+' ') if {etud.code_nip and may_be_nip} else ""}{
"value": x["etudid"], etud.nom_prenom()}""",
} "value": etud.id,
for x in r }
] for etud in etuds
return data ]
# ---------- Recherche sur plusieurs département # ---------- Recherche sur plusieurs département
def search_etud_in_accessible_depts(expnom=None, code_nip=None): def search_etud_in_accessible_depts(
expnom=None,
) -> tuple[list[list[Identite]], list[str]]:
""" """
result is a list of (sorted) etuds, one list per dept. result: list of (sorted) etuds, one list per dept.
accessible_depts: list of dept acronyms
""" """
result = [] result = []
accessible_depts = [] accessible_depts = []
depts = Departement.query.filter_by(visible=True).all() depts = Departement.query.filter_by(visible=True).all()
for dept in depts: for dept in depts:
if current_user.has_permission(Permission.ScoView, dept=dept.acronym): if current_user.has_permission(Permission.ScoView, dept=dept.acronym):
if expnom or code_nip: if expnom:
accessible_depts.append(dept.acronym) accessible_depts.append(dept.acronym)
app.set_sco_dept(dept.acronym) etuds = search_etuds_infos_from_exp(expnom=expnom, dept_id=dept.id)
etuds = search_etuds_infos(expnom=expnom, code_nip=code_nip)
else: else:
etuds = [] etuds = []
result.append(etuds) result.append(etuds)
@ -371,21 +349,26 @@ def table_etud_in_accessible_depts(expnom=None):
] ]
for etuds in result: for etuds in result:
if etuds: if etuds:
dept_id = etuds[0]["dept"] dept = etuds[0].departement
# H.append('<h3>Département %s</h3>' % DeptId) rows = [
for e in etuds: {
e["_nomprenom_target"] = url_for( "nomprenom": etud.nom_prenom(),
"scolar.fiche_etud", scodoc_dept=dept_id, etudid=e["etudid"] "_nomprenom_target": url_for(
) "scolar.fiche_etud", scodoc_dept=dept.acronym, etudid=etud.id
e["_nomprenom_td_attrs"] = f"""id="{e['etudid']}" class="etudinfo" """ ),
"_nomprenom_td_attrs": f"""id="{etud.id}" class="etudinfo" """,
"_nomprenom_order": etud.sort_key,
}
for etud in etuds
]
tab = GenTable( tab = GenTable(
titles={"nomprenom": "Étudiants en " + dept_id}, titles={"nomprenom": "Étudiants en " + dept.acronym},
columns_ids=("nomprenom",), columns_ids=("nomprenom",),
rows=etuds, rows=rows,
html_sortable=True, html_sortable=True,
html_class="table_leftalign", html_class="table_leftalign",
table_id="etud_in_accessible_depts", # table_id="etud_in_accessible_depts",
) )
H.append('<div class="table_etud_in_dept">') H.append('<div class="table_etud_in_dept">')
@ -410,48 +393,3 @@ def table_etud_in_accessible_depts(expnom=None):
+ "\n".join(H) + "\n".join(H)
+ html_sco_header.standard_html_footer() + html_sco_header.standard_html_footer()
) )
def search_inscr_etud_by_nip(code_nip, fmt="json"):
"""Recherche multi-departement d'un étudiant par son code NIP
Seuls les départements accessibles par l'utilisateur sont cherchés.
Renvoie une liste des inscriptions de l'étudiants dans tout ScoDoc:
code_nip, nom, prenom, civilite_str, dept, formsemestre_id, date_debut_sem, date_fin_sem
"""
result, _ = search_etud_in_accessible_depts(code_nip=code_nip)
rows = []
for etuds in result:
if etuds:
dept_id = etuds[0]["dept"]
for e in etuds:
for sem in e["sems"]:
rows.append(
{
"dept": dept_id,
"etudid": e["etudid"],
"code_nip": e["code_nip"],
"civilite_str": e["civilite_str"],
"nom": e["nom"],
"prenom": e["prenom"],
"formsemestre_id": sem["formsemestre_id"],
"date_debut_iso": sem["date_debut_iso"],
"date_fin_iso": sem["date_fin_iso"],
}
)
columns_ids = (
"dept",
"etudid",
"code_nip",
"civilite_str",
"nom",
"prenom",
"formsemestre_id",
"date_debut_iso",
"date_fin_iso",
)
tab = GenTable(columns_ids=columns_ids, rows=rows, table_id="inscr_etud_by_nip")
return tab.make_page(fmt=fmt, with_html_headers=False, publish=True)

View File

@ -14,6 +14,9 @@ function get_etudid_from_elem(e) {
} }
$().ready(function () { $().ready(function () {
if (typeof SCO_URL == 'undefined') {
return;
}
var elems = $(".etudinfo:not(th)"); var elems = $(".etudinfo:not(th)");
var q_args = get_query_args(); var q_args = get_query_args();
@ -35,11 +38,7 @@ $().ready(function () {
$(elems[i]).qtip({ $(elems[i]).qtip({
content: { content: {
ajax: { ajax: {
url: url: `${SCO_URL}etud_info_html?etudid=` + get_etudid_from_elem(elems[i]) + qs,
SCO_URL +
"etud_info_html?etudid=" +
get_etudid_from_elem(elems[i]) +
qs,
type: "GET", type: "GET",
//success: function(data, status) { //success: function(data, status) {
// this.set('content.text', data); // this.set('content.text', data);

View File

@ -6,7 +6,7 @@ $(function () {
delay: 300, // wait 300ms before suggestions delay: 300, // wait 300ms before suggestions
minLength: 2, // min nb of chars before suggest minLength: 2, // min nb of chars before suggest
position: { collision: "flip" }, // automatic menu position up/down position: { collision: "flip" }, // automatic menu position up/down
source: SCO_URL + "search_etud_by_name", source: (typeof SCO_URL !== 'undefined' && SCO_URL) ? `${SCO_URL}search_etud_by_name` : "",
select: function (event, ui) { select: function (event, ui) {
$(".in-expnom").val(ui.item.value); $(".in-expnom").val(ui.item.value);
$("#form-chercheetud").submit(); $("#form-chercheetud").submit();

View File

@ -567,27 +567,6 @@ def get_etud_dept():
return Departement.query.get_or_404(last_etud.dept_id).acronym return Departement.query.get_or_404(last_etud.dept_id).acronym
# Bricolage pour le portail IUTV avec ScoDoc 7: (DEPRECATED: NE PAS UTILISER !)
@bp.route(
"/ScoDoc/search_inscr_etud_by_nip", methods=["GET"]
) # pour compat anciens clients PHP
@scodoc
@scodoc7func
def search_inscr_etud_by_nip(code_nip, fmt="json", __ac_name="", __ac_password=""):
auth_ok = False
user_name = __ac_name
user_password = __ac_password
if user_name and user_password:
u = User.query.filter_by(user_name=user_name).first()
if u and u.check_password(user_password):
auth_ok = True
flask_login.login_user(u)
if not auth_ok:
abort(403)
else:
return sco_find_etud.search_inscr_etud_by_nip(code_nip=code_nip, fmt=fmt)
@bp.route("/ScoDoc/about") @bp.route("/ScoDoc/about")
@bp.route("/ScoDoc/Scolarite/<scodoc_dept>/about") @bp.route("/ScoDoc/Scolarite/<scodoc_dept>/about")
@login_required @login_required

View File

@ -531,6 +531,9 @@ sco_publish(
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@as_json @as_json
def search_etud_by_name(): def search_etud_by_name():
"""Recherche étudiants par nom ou NIP
utilisé par autocomplete formulaire recherche
"""
term = request.args["term"] term = request.args["term"]
data = sco_find_etud.search_etud_by_name(term) data = sco_find_etud.search_etud_by_name(term)
return data return data

View File

@ -331,36 +331,116 @@ def test_referentiel_competences(api_headers):
def test_api_ue_apo(api_admin_headers): def test_api_ue_apo(api_admin_headers):
"""Routes """Routes
/ue/<int:ue_id> /formation/ue/<int:ue_id>
/ue/<int:ue_id>/set_code_apogee/<string:code_apogee> /formation/ue/<int:ue_id>/set_code_apogee/<string:code_apogee>
/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee> /formation/ue/<int:ue_id>/set_code_apogee_rcue/<string:code_apogee>
""" """
ue_id = 1 ue_id = 1
ue = GET(path=f"/ue/{ue_id}", headers=api_admin_headers) ue = GET(path=f"/formation/ue/{ue_id}", headers=api_admin_headers)
assert ue["id"] == ue_id assert ue["id"] == ue_id
r = POST_JSON(f"/ue/{ue_id}/set_code_apogee/APOUE", {}, api_admin_headers, raw=True) r = POST_JSON(
f"/formation/ue/{ue_id}/set_code_apogee/APOUE", {}, api_admin_headers, raw=True
)
assert r.text == "APOUE" assert r.text == "APOUE"
r = POST_JSON( r = POST_JSON(
f"/ue/{ue_id}/set_code_apogee_rcue/APORCUE", {}, api_admin_headers, raw=True f"/formation/ue/{ue_id}/set_code_apogee_rcue/APORCUE",
{},
api_admin_headers,
raw=True,
) )
assert r.text == "APORCUE" assert r.text == "APORCUE"
ue = GET(path=f"/ue/{ue_id}", headers=api_admin_headers) ue = GET(path=f"/formation/ue/{ue_id}", headers=api_admin_headers)
assert ue["code_apogee"] == "APOUE" assert ue["code_apogee"] == "APOUE"
assert ue["code_apogee_rcue"] == "APORCUE" assert ue["code_apogee_rcue"] == "APORCUE"
def test_api_module_apo(api_admin_headers): def test_api_module_apo(api_admin_headers):
"""Routes """Routes
/module/<int:module_id> /formation/module/<int:module_id>
/module/<int:module_id>/set_code_apogee/<string:code_apogee> /formation/module/<int:module_id>/set_code_apogee/<string:code_apogee>
""" """
module_id = 1 module_id = 1
module = GET(path=f"/module/{module_id}", headers=api_admin_headers) module = GET(path=f"/formation/module/{module_id}", headers=api_admin_headers)
assert module["id"] == module_id assert module["id"] == module_id
assert module["code_apogee"] == "" assert module["code_apogee"] == ""
r = POST_JSON( r = POST_JSON(
f"/module/{module_id}/set_code_apogee/APOMOD", {}, api_admin_headers, raw=True f"/formation/module/{module_id}/set_code_apogee/APOMOD",
{},
api_admin_headers,
raw=True,
) )
assert r.text == "APOMOD" assert r.text == "APOMOD"
module = GET(path=f"/module/{module_id}", headers=api_admin_headers) module = GET(path=f"/formation/module/{module_id}", headers=api_admin_headers)
assert module["code_apogee"] == "APOMOD" assert module["code_apogee"] == "APOMOD"
def test_api_module_edit(api_admin_headers):
"""Routes
/formation/module/<int:module_id>/edit
"""
module_id = 1
module = POST_JSON(
f"/formation/module/{module_id}/edit",
{"heures_cours": 16, "semestre_id": 1, "abbrev": "ALLO"},
api_admin_headers,
)
assert module["id"] == module_id
assert module["heures_cours"] == 16
assert module["semestre_id"] == 1
assert module["abbrev"] == "ALLO"
# tente de changer l'UE: ne devrait rien faire:
ue_id = module["ue_id"]
module = POST_JSON(
f"/formation/module/{module_id}/edit",
{"ue_id": 666},
api_admin_headers,
)
assert module["ue_id"] == ue_id
# tente de changer la formation: ne devrait rien faire:
formation_id = module["formation_id"]
module = POST_JSON(
f"/formation/module/{module_id}/edit",
{"formation_id": 666},
api_admin_headers,
)
assert module["formation_id"] == formation_id
# -- assignation de parcours (ce test suppose que les parcours 1, 2, 3 existent)
assert module["parcours"] == []
module = POST_JSON(
f"/formation/module/{module_id}/edit",
{"parcours": [1, 2, 3]},
api_admin_headers,
)
assert sorted(module["parcours"]) == [1, 2, 3]
def test_api_ue_edit(api_admin_headers):
"""Routes
/formation/ue/<int:ue_id>/edit
"""
ue_id = 1
ue = POST_JSON(
f"/formation/ue/{ue_id}/edit",
{"titre": "formation test modifiée", "numero": 22},
api_admin_headers,
)
assert ue["id"] == ue_id
assert ue["numero"] == 22
assert ue["titre"] == "formation test modifiée"
# tente de changer le niveau de compétence: ne devrait rien faire:
niveau_competence_id = ue["niveau_competence_id"]
ue = POST_JSON(
f"/formation/ue/{ue_id}/edit",
{"niveau_competence_id": 666},
api_admin_headers,
)
assert ue["niveau_competence_id"] == niveau_competence_id
# tente de changer la formation: ne devrait rien faire:
formation_id = ue["formation_id"]
ue = POST_JSON(
f"/formation/ue/{ue_id}/edit",
{"formation_id": 666},
api_admin_headers,
)
assert ue["formation_id"] == formation_id