forked from ScoDoc/ScoDoc
itemsuivi étudiants (devenir/débouchés): ré-écriture du backend, API, adaptation frontend js.
This commit is contained in:
parent
b79e40f030
commit
41a76267f6
@ -113,6 +113,7 @@ from app.api import (
|
|||||||
assiduites,
|
assiduites,
|
||||||
billets_absences,
|
billets_absences,
|
||||||
departements,
|
departements,
|
||||||
|
etud_suivi,
|
||||||
etudiants,
|
etudiants,
|
||||||
evaluations,
|
evaluations,
|
||||||
formations,
|
formations,
|
||||||
|
213
app/api/etud_suivi.py
Normal file
213
app/api/etud_suivi.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""
|
||||||
|
API : itemsuivi devenir/débouché des étudiants
|
||||||
|
|
||||||
|
CATEGORY
|
||||||
|
--------
|
||||||
|
Étudiants
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from flask import g, request, Response
|
||||||
|
from flask_json import as_json
|
||||||
|
from flask_login import login_required
|
||||||
|
|
||||||
|
from app.api import api_bp as bp, api_web_bp
|
||||||
|
from app.api import api_permission_required as permission_required
|
||||||
|
from app import db, log
|
||||||
|
from app.decorators import scodoc
|
||||||
|
from app.models import Identite, ItemSuivi, ItemSuiviTag, Scolog
|
||||||
|
from app.scodoc import sco_permissions_check
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
|
from app.scodoc.sco_exceptions import AccessDenied
|
||||||
|
import app.scodoc.sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/etudiant/<int:etudid>/itemsuivi/create")
|
||||||
|
@api_web_bp.post("/etudiant/<int:etudid>/itemsuivi/create")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.EtudChangeAdr)
|
||||||
|
@as_json
|
||||||
|
def itemsuivi_create(etudid):
|
||||||
|
"""Création d'un item de suivi associé à l'étudiant.
|
||||||
|
|
||||||
|
The form MAY contain:
|
||||||
|
- item_date: date of the item
|
||||||
|
- situation: text
|
||||||
|
"""
|
||||||
|
if not sco_permissions_check.can_edit_suivi():
|
||||||
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
|
etud = Identite.get_etud(etudid)
|
||||||
|
ok, item_date = _get_date_from_form()
|
||||||
|
item_date = item_date if ok else datetime.datetime.now()
|
||||||
|
situation = request.form.get("situation") or ""
|
||||||
|
item = ItemSuivi(etudid=etudid, item_date=item_date, situation=situation)
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
Scolog.logdb(method="itemsuivi_create", etudid=etud.id, commit=True)
|
||||||
|
log(f"itemsuivi_create {item} for {etud}")
|
||||||
|
db.session.refresh(item)
|
||||||
|
return item.to_dict(merge_tags=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/etudiant/itemsuivi/<int:itemid>/delete")
|
||||||
|
@api_web_bp.post("/etudiant/itemsuivi/<int:itemid>/delete")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.EtudChangeAdr)
|
||||||
|
@as_json
|
||||||
|
def itemsuivi_suppress(itemid):
|
||||||
|
"""Suppression d'un item"""
|
||||||
|
if not sco_permissions_check.can_edit_suivi():
|
||||||
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
|
item: ItemSuivi = ItemSuivi.get_instance(itemid, accept_none=True)
|
||||||
|
if item is None:
|
||||||
|
return {"status": "ok", "message": "item not found"}
|
||||||
|
etudid = item.etudid
|
||||||
|
db.session.delete(item)
|
||||||
|
db.session.commit()
|
||||||
|
Scolog.logdb(method="itemsuivi_suppress", etudid=etudid, commit=True)
|
||||||
|
log(f"itemsuivi_suppress: itemid={itemid}")
|
||||||
|
return {"status": "ok", "message": "item deleted"}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/etudiant/<int:etudid>/itemsuivis")
|
||||||
|
@api_web_bp.get("/etudiant/<int:etudid>/itemsuivis")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
@as_json
|
||||||
|
def itemsuivi_list_etud(etudid: int):
|
||||||
|
"""Liste des items pour cet étudiant, avec tags"""
|
||||||
|
etud = Identite.get_etud(etudid)
|
||||||
|
items = [it.to_dict(merge_tags=True) for it in etud.itemsuivis]
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/etudiant/itemsuivi/<int:itemid>/tag")
|
||||||
|
@api_web_bp.post("/etudiant/itemsuivi/<int:itemid>/tag")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.EtudChangeAdr)
|
||||||
|
@as_json
|
||||||
|
def itemsuivi_tag_set(itemid: int):
|
||||||
|
"""
|
||||||
|
taglist a string with tag names separated by commas e.g.: `un,deux`
|
||||||
|
"""
|
||||||
|
if not sco_permissions_check.can_edit_suivi():
|
||||||
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
|
item: ItemSuivi = ItemSuivi.get_instance(itemid)
|
||||||
|
taglist_str = request.form.get("taglist")[: scu.MAX_TEXT_LEN]
|
||||||
|
if taglist_str is None:
|
||||||
|
return scu.json_error(400, "missing taglist in form")
|
||||||
|
taglist = [t.strip() for t in taglist_str.split(",")]
|
||||||
|
item.set_tags(taglist)
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
log(f"itemsuivi_tag_set: itemsuivi_id={item.id} taglist={taglist}")
|
||||||
|
return item.to_dict(merge_tags=True)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/etudiant/itemsuivi/<int:itemid>/set_situation")
|
||||||
|
@api_web_bp.post("/etudiant/itemsuivi/<int:itemid>/set_situation")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.EtudChangeAdr)
|
||||||
|
def itemsuivi_set_situation(itemid: int):
|
||||||
|
"""Modifie le champ situation de l'item de suivi de l'étudiant.
|
||||||
|
|
||||||
|
Form data: "value" : "situation ..."
|
||||||
|
|
||||||
|
Appel utilisé par champ éditable sur fiche étudiant.
|
||||||
|
"""
|
||||||
|
if not sco_permissions_check.can_edit_suivi():
|
||||||
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
|
item: ItemSuivi = ItemSuivi.get_instance(itemid)
|
||||||
|
if "value" not in request.form:
|
||||||
|
return scu.json_error(400, "missing value in form")
|
||||||
|
situation = request.form["value"]
|
||||||
|
item.situation = situation.strip("-_ \t\r\n")[: scu.MAX_TEXT_LEN]
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
log(f"itemsuivi_set_situation: itemid={itemid} situation={item.situation[:32]}")
|
||||||
|
return item.situation or scu.IT_SITUATION_MISSING_STR
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/etudiant/itemsuivi/<int:itemid>/set_date")
|
||||||
|
@api_web_bp.post("/etudiant/itemsuivi/<int:itemid>/set_date")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.EtudChangeAdr)
|
||||||
|
@as_json
|
||||||
|
def itemsuivi_set_date(itemid: int):
|
||||||
|
"""set item date
|
||||||
|
|
||||||
|
Specify date as an ISO date string
|
||||||
|
or date_dmy, string dd/mm/yyyy
|
||||||
|
"""
|
||||||
|
if not sco_permissions_check.can_edit_suivi():
|
||||||
|
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||||
|
ok, date_or_resp = _get_date_from_form()
|
||||||
|
if not ok:
|
||||||
|
return date_or_resp
|
||||||
|
item: ItemSuivi = ItemSuivi.get_instance(itemid)
|
||||||
|
item.item_date = date_or_resp
|
||||||
|
db.session.add(item)
|
||||||
|
db.session.commit()
|
||||||
|
log(f"itemsuivi_set_date: itemid={itemid} date={item.item_date}")
|
||||||
|
return item.to_dict(merge_tags=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_date_from_form() -> tuple[bool, datetime.datetime | Response]:
|
||||||
|
"""get date from form
|
||||||
|
Specify date as an ISO date string
|
||||||
|
or date_dmy, string dd/mm/yyyy
|
||||||
|
"""
|
||||||
|
date_iso = request.form.get("date") # ISO format
|
||||||
|
if date_iso:
|
||||||
|
try:
|
||||||
|
return True, datetime.datetime.fromisoformat(date_iso)
|
||||||
|
except ValueError:
|
||||||
|
return False, scu.json_error(400, "invalide date format (iso)")
|
||||||
|
date_dmy = request.form.get("date_dmy")
|
||||||
|
if not date_dmy:
|
||||||
|
return False, scu.json_error(400, "missing date or date_dmy in form")
|
||||||
|
try:
|
||||||
|
return True, datetime.datetime.strptime(date_dmy, scu.DATE_FMT)
|
||||||
|
except ValueError:
|
||||||
|
return False, scu.json_error(400, "invalide date format (jj/mm/aaaa)")
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/etudiant/itemsuivi/tags/search")
|
||||||
|
@api_web_bp.get("/etudiant/itemsuivi/tags/search")
|
||||||
|
@login_required
|
||||||
|
@scodoc
|
||||||
|
@permission_required(Permission.ScoView)
|
||||||
|
@as_json
|
||||||
|
def itemsuivi_tag_search():
|
||||||
|
"""List all used tag names (for auto-completion)
|
||||||
|
|
||||||
|
Query: term
|
||||||
|
"""
|
||||||
|
if getattr(g, "scodoc_dept_id", None) is None:
|
||||||
|
return [] # accès départemental seulement
|
||||||
|
term = request.args.get("term", "").strip()[: scu.MAX_TEXT_LEN]
|
||||||
|
# restrict charset to avoid injections
|
||||||
|
if not scu.ALPHANUM_EXP.match(term):
|
||||||
|
data = []
|
||||||
|
else:
|
||||||
|
data = [
|
||||||
|
x.title
|
||||||
|
for x in ItemSuiviTag.query.filter(
|
||||||
|
ItemSuiviTag.title.like(f"{term}%"),
|
||||||
|
ItemSuiviTag.dept_id == g.scodoc_dept_id,
|
||||||
|
).all()
|
||||||
|
]
|
||||||
|
|
||||||
|
return data
|
@ -128,9 +128,13 @@ class ScoDocModel(db.Model):
|
|||||||
def get_instance(cls, oid: int, accept_none=False):
|
def get_instance(cls, oid: int, accept_none=False):
|
||||||
"""Instance du modèle ou ou 404 (ou None si accept_none),
|
"""Instance du modèle ou ou 404 (ou None si accept_none),
|
||||||
cherche uniquement dans le département courant.
|
cherche uniquement dans le département courant.
|
||||||
Ne fonctionne que si le modèle a un attribut dept_id.
|
|
||||||
|
Ne fonctionne que si le modèle a un attribut dept_id
|
||||||
|
ou que l'attribut de classe _sco_dept_relations indique les jointures
|
||||||
|
à effectuer pour trouver le département.
|
||||||
|
|
||||||
Si accept_none, return None si l'id est invalide ou ne correspond
|
Si accept_none, return None si l'id est invalide ou ne correspond
|
||||||
pas à une validation.
|
pas à une instance. Sinon lève 404 en cas d'erreur.
|
||||||
"""
|
"""
|
||||||
if not isinstance(oid, int):
|
if not isinstance(oid, int):
|
||||||
try:
|
try:
|
||||||
|
@ -115,7 +115,12 @@ class Identite(models.ScoDocModel):
|
|||||||
cascade="all, delete",
|
cascade="all, delete",
|
||||||
passive_deletes=True,
|
passive_deletes=True,
|
||||||
)
|
)
|
||||||
|
itemsuivis = db.relationship(
|
||||||
|
"ItemSuivi",
|
||||||
|
backref="etudiant",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
lazy="dynamic",
|
||||||
|
)
|
||||||
# Relations avec les assiduites et les justificatifs
|
# Relations avec les assiduites et les justificatifs
|
||||||
assiduites = db.relationship(
|
assiduites = db.relationship(
|
||||||
"Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
"Assiduite", back_populates="etudiant", lazy="dynamic", cascade="all, delete"
|
||||||
@ -1118,8 +1123,39 @@ class ItemSuivi(models.ScoDocModel):
|
|||||||
|
|
||||||
_sco_dept_relations = ("Identite",) # accès au dept_id
|
_sco_dept_relations = ("Identite",) # accès au dept_id
|
||||||
|
|
||||||
|
tags = db.relationship(
|
||||||
|
"ItemSuiviTag",
|
||||||
|
secondary="itemsuivi_tags_assoc",
|
||||||
|
lazy=True,
|
||||||
|
backref=db.backref("items", lazy=True),
|
||||||
|
)
|
||||||
|
|
||||||
class ItemSuiviTag(db.Model):
|
def to_dict(self, merge_tags=False):
|
||||||
|
"""Représentation dictionnaire.
|
||||||
|
Si merge_tags, regroupe les tags sur une seule chaine, valeurs séparées par des virgules
|
||||||
|
"""
|
||||||
|
d = super().to_dict()
|
||||||
|
# Convertit les tags en liste de strings:
|
||||||
|
if merge_tags:
|
||||||
|
d["tags"] = ", ".join([tag.title for tag in self.tags])
|
||||||
|
else:
|
||||||
|
d["tags"] = [tag.title for tag in self.tags]
|
||||||
|
# Ajoute date locale
|
||||||
|
d["item_date_dmy"] = self.item_date.strftime(scu.DATE_FMT)
|
||||||
|
return d
|
||||||
|
|
||||||
|
def set_tags(self, tags: list[str]):
|
||||||
|
"""Définit les tags de l'itemsuivi"""
|
||||||
|
self.tags = []
|
||||||
|
for tag in tags:
|
||||||
|
tag_obj = ItemSuiviTag.query.filter_by(title=tag).first()
|
||||||
|
if tag_obj is None:
|
||||||
|
tag_obj = ItemSuiviTag(title=tag)
|
||||||
|
self.tags.append(tag_obj)
|
||||||
|
|
||||||
|
|
||||||
|
class ItemSuiviTag(models.ScoDocModel):
|
||||||
|
"Tag sur un itemsuivi"
|
||||||
__tablename__ = "itemsuivi_tags"
|
__tablename__ = "itemsuivi_tags"
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
|
||||||
@ -1127,7 +1163,7 @@ class ItemSuiviTag(db.Model):
|
|||||||
title = db.Column(db.Text(), nullable=False, unique=True)
|
title = db.Column(db.Text(), nullable=False, unique=True)
|
||||||
|
|
||||||
|
|
||||||
# Association tag <-> module
|
# Association tag <-> itemsuivi
|
||||||
itemsuivi_tags_assoc = db.Table(
|
itemsuivi_tags_assoc = db.Table(
|
||||||
"itemsuivi_tags_assoc",
|
"itemsuivi_tags_assoc",
|
||||||
db.Column(
|
db.Column(
|
||||||
|
@ -28,22 +28,15 @@
|
|||||||
"""
|
"""
|
||||||
Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant
|
Rapport (table) avec dernier semestre fréquenté et débouché de chaque étudiant
|
||||||
"""
|
"""
|
||||||
import http
|
|
||||||
from flask import g, render_template, request, url_for
|
from flask import g, render_template, request, url_for
|
||||||
|
|
||||||
from app import log
|
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre, Scolog
|
from app.models import Identite
|
||||||
import app.scodoc.sco_utils as scu
|
import app.scodoc.sco_utils as scu
|
||||||
import app.scodoc.notesdb as ndb
|
import app.scodoc.notesdb as ndb
|
||||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
|
|
||||||
from app.scodoc.gen_tables import GenTable
|
from app.scodoc.gen_tables import GenTable
|
||||||
from app.scodoc import safehtml
|
|
||||||
from app.scodoc import sco_permissions_check
|
|
||||||
from app.scodoc import sco_preferences
|
from app.scodoc import sco_preferences
|
||||||
from app.scodoc import sco_tag_module
|
|
||||||
from app.scodoc import sco_etud
|
|
||||||
import sco_version
|
import sco_version
|
||||||
|
|
||||||
|
|
||||||
@ -75,6 +68,7 @@ def report_debouche_date(start_year=None, fmt="html"):
|
|||||||
tab.base_url = f"{request.base_url}?start_year={start_year}"
|
tab.base_url = f"{request.base_url}?start_year={start_year}"
|
||||||
return tab.make_page(
|
return tab.make_page(
|
||||||
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
|
title="""<h2 class="formsemestre">Débouchés étudiants </h2>""",
|
||||||
|
page_title="Débouchés étudiants",
|
||||||
fmt=fmt,
|
fmt=fmt,
|
||||||
with_html_headers=True,
|
with_html_headers=True,
|
||||||
template="sco_page_dept.j2",
|
template="sco_page_dept.j2",
|
||||||
@ -115,70 +109,62 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
|||||||
"""Rapport pour ces étudiants"""
|
"""Rapport pour ces étudiants"""
|
||||||
rows = []
|
rows = []
|
||||||
# Recherche les débouchés:
|
# Recherche les débouchés:
|
||||||
itemsuivi_etuds = {etudid: itemsuivi_list_etud(etudid) for etudid in etudids}
|
# itemsuivi_etuds = {etudid: itemsuivi_list_etud(etudid) for etudid in etudids}
|
||||||
all_tags = set()
|
all_tags = set()
|
||||||
for debouche in itemsuivi_etuds.values():
|
|
||||||
if debouche:
|
|
||||||
for it in debouche:
|
|
||||||
all_tags.update(tag.strip() for tag in it["tags"].split(","))
|
|
||||||
all_tags = tuple(sorted(all_tags))
|
|
||||||
|
|
||||||
for etudid in etudids:
|
for etudid in etudids:
|
||||||
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
|
etud = Identite.get_etud(etudid)
|
||||||
|
# collecte les tags
|
||||||
|
all_tags.update(tag.title for item in etud.itemsuivis for tag in item.tags)
|
||||||
# retrouve le "dernier" semestre (au sens de la date de fin)
|
# retrouve le "dernier" semestre (au sens de la date de fin)
|
||||||
sems = etud["sems"]
|
formsemestres = etud.get_formsemestres()
|
||||||
es = [(s["date_fin_iso"], i) for i, s in enumerate(sems)]
|
dates_fin = [s.date_fin for s in formsemestres]
|
||||||
imax = max(es)[1]
|
imax = dates_fin.index(max(dates_fin))
|
||||||
last_sem = sems[imax]
|
formsemestre = formsemestres[imax]
|
||||||
formsemestre = FormSemestre.query.get_or_404(last_sem["formsemestre_id"])
|
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
row = {
|
row = {
|
||||||
"etudid": etudid,
|
"etudid": etudid,
|
||||||
"civilite": etud["civilite"],
|
"civilite": etud.civilite,
|
||||||
"nom": etud["nom"],
|
"nom": etud.nom,
|
||||||
"prenom": etud["prenom"],
|
"prenom": etud.prenom,
|
||||||
"_nom_target": url_for(
|
"_nom_target": url_for(
|
||||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||||
),
|
),
|
||||||
"_prenom_target": url_for(
|
"_prenom_target": url_for(
|
||||||
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
|
||||||
),
|
),
|
||||||
"_nom_td_attrs": 'id="%s" class="etudinfo"' % (etud["etudid"]),
|
"_nom_td_attrs": f'id="{etudid}" class="etudinfo"',
|
||||||
# 'debouche' : etud['debouche'],
|
# 'debouche' : etud['debouche'],
|
||||||
"moy": scu.fmt_note(nt.get_etud_moy_gen(etudid), keep_numeric=keep_numeric),
|
"moy": scu.fmt_note(nt.get_etud_moy_gen(etudid), keep_numeric=keep_numeric),
|
||||||
"rang": nt.get_etud_rang(etudid),
|
"rang": nt.get_etud_rang(etudid),
|
||||||
"effectif": len(nt.T),
|
"effectif": len(nt.T),
|
||||||
"semestre_id": last_sem["semestre_id"],
|
"semestre_id": formsemestre.semestre_id,
|
||||||
"semestre": last_sem["titre"],
|
"semestre": formsemestre.titre,
|
||||||
"date_debut": last_sem["date_debut"],
|
"date_debut": formsemestre.date_debut,
|
||||||
"date_fin": last_sem["date_fin"],
|
"date_fin": formsemestre.date_fin,
|
||||||
"periode": "%s - %s" % (last_sem["mois_debut"], last_sem["mois_fin"]),
|
"periode": f"{formsemestre.mois_debut()} - {formsemestre.mois_fin()}",
|
||||||
"sem_ident": "%s %s"
|
"sem_ident": f"{formsemestre.date_debut.isoformat()} {formsemestre.titre}", # utile pour tris
|
||||||
% (last_sem["date_debut_iso"], last_sem["titre"]), # utile pour tris
|
|
||||||
}
|
}
|
||||||
# recherche des débouchés
|
# recherche des débouchés
|
||||||
debouche = itemsuivi_etuds[etudid] # liste de plusieurs items
|
itemsuivis = etud.itemsuivis # liste de plusieurs items
|
||||||
if debouche:
|
if itemsuivis:
|
||||||
if keep_numeric: # pour excel:
|
if keep_numeric: # pour excel:
|
||||||
row["debouche"] = "\n".join(
|
row["debouche"] = "\n".join(
|
||||||
f"""{it["item_date"]}: {it["situation"]}""" for it in debouche
|
f"""{it.item_date.strftime(scu.DATE_FMT)}: {it.situation or ""}"""
|
||||||
|
for it in itemsuivis
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
row["debouche"] = "<br>".join(
|
row["debouche"] = "<br>".join(
|
||||||
[
|
[
|
||||||
str(it["item_date"])
|
f"""{it.item_date.strftime(scu.DATE_FMT)} : {it.situation or ""}
|
||||||
+ " : "
|
<i>{', '.join( tag.title for tag in it.tags)}</i>
|
||||||
+ it["situation"]
|
"""
|
||||||
+ " <i>"
|
for it in itemsuivis
|
||||||
+ it["tags"]
|
|
||||||
+ "</i>"
|
|
||||||
for it in debouche
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
for it in debouche:
|
for it in itemsuivis:
|
||||||
for tag in it["tags"].split(","):
|
for tag in it.tags:
|
||||||
tag = tag.strip()
|
tag_name = tag.title.strip()
|
||||||
row[f"tag_{tag}"] = tag
|
row[f"tag_{tag_name}"] = tag_name
|
||||||
else:
|
else:
|
||||||
row["debouche"] = "non renseigné"
|
row["debouche"] = "non renseigné"
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
@ -236,177 +222,3 @@ def report_debouche_ask_date(msg: str) -> str:
|
|||||||
</form>
|
</form>
|
||||||
""",
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
#
|
|
||||||
# Nouveau suivi des etudiants (nov 2017)
|
|
||||||
#
|
|
||||||
# ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
_itemsuiviEditor = ndb.EditableTable(
|
|
||||||
"itemsuivi",
|
|
||||||
"itemsuivi_id",
|
|
||||||
("itemsuivi_id", "etudid", "item_date", "situation"),
|
|
||||||
sortkey="item_date desc",
|
|
||||||
convert_null_outputs_to_empty=True,
|
|
||||||
output_formators={
|
|
||||||
"situation": safehtml.html_to_safe_html,
|
|
||||||
"item_date": ndb.DateISOtoDMY,
|
|
||||||
},
|
|
||||||
input_formators={"item_date": ndb.DateDMYtoISO},
|
|
||||||
)
|
|
||||||
|
|
||||||
_itemsuivi_create = _itemsuiviEditor.create
|
|
||||||
_itemsuivi_delete = _itemsuiviEditor.delete
|
|
||||||
_itemsuivi_list = _itemsuiviEditor.list
|
|
||||||
_itemsuivi_edit = _itemsuiviEditor.edit
|
|
||||||
|
|
||||||
|
|
||||||
class ItemSuiviTag(sco_tag_module.ScoTag):
|
|
||||||
"""Les tags sur les items"""
|
|
||||||
|
|
||||||
tag_table = "itemsuivi_tags" # table (tag_id, title)
|
|
||||||
assoc_table = "itemsuivi_tags_assoc" # table (tag_id, object_id)
|
|
||||||
obj_colname = "itemsuivi_id" # column name for object_id in assoc_table
|
|
||||||
|
|
||||||
|
|
||||||
def itemsuivi_get(cnx, itemsuivi_id, ignore_errors=False):
|
|
||||||
"""get an item"""
|
|
||||||
items = _itemsuivi_list(cnx, {"itemsuivi_id": itemsuivi_id})
|
|
||||||
if items:
|
|
||||||
return items[0]
|
|
||||||
elif not ignore_errors:
|
|
||||||
raise ScoValueError(f"Débouché: item inexistant ({itemsuivi_id})")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def itemsuivi_suppress(itemsuivi_id):
|
|
||||||
"""Suppression d'un item"""
|
|
||||||
if not sco_permissions_check.can_edit_suivi():
|
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
item = itemsuivi_get(cnx, itemsuivi_id, ignore_errors=True)
|
|
||||||
if item:
|
|
||||||
_itemsuivi_delete(cnx, itemsuivi_id)
|
|
||||||
Scolog.logdb(method="itemsuivi_suppress", etudid=item["etudid"], commit=True)
|
|
||||||
log(f"suppressed itemsuivi {itemsuivi_id}")
|
|
||||||
return ("", 204)
|
|
||||||
|
|
||||||
|
|
||||||
def itemsuivi_create(etudid, item_date=None, situation="", fmt=None):
|
|
||||||
"""Creation d'un item"""
|
|
||||||
if not sco_permissions_check.can_edit_suivi():
|
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
itemsuivi_id = _itemsuivi_create(
|
|
||||||
cnx, args={"etudid": etudid, "item_date": item_date, "situation": situation}
|
|
||||||
)
|
|
||||||
Scolog.logdb(method="itemsuivi_create", etudid=etudid, commit=True)
|
|
||||||
log("created itemsuivi %s for %s" % (itemsuivi_id, etudid))
|
|
||||||
item = itemsuivi_get(cnx, itemsuivi_id)
|
|
||||||
if fmt == "json":
|
|
||||||
return scu.sendJSON(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
def itemsuivi_set_date(itemsuivi_id, item_date):
|
|
||||||
"""set item date
|
|
||||||
item_date is a string dd/mm/yyyy
|
|
||||||
"""
|
|
||||||
if not sco_permissions_check.can_edit_suivi():
|
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
||||||
# log('itemsuivi_set_date %s : %s' % (itemsuivi_id, item_date))
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
item = itemsuivi_get(cnx, itemsuivi_id)
|
|
||||||
item["item_date"] = item_date
|
|
||||||
_itemsuivi_edit(cnx, item)
|
|
||||||
return ("", 204)
|
|
||||||
|
|
||||||
|
|
||||||
def itemsuivi_set_situation(obj, value):
|
|
||||||
"""set situation"""
|
|
||||||
if not sco_permissions_check.can_edit_suivi():
|
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
||||||
itemsuivi_id = obj
|
|
||||||
situation = value.strip("-_ \t")
|
|
||||||
# log('itemsuivi_set_situation %s : %s' % (itemsuivi_id, situation))
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
item = itemsuivi_get(cnx, itemsuivi_id)
|
|
||||||
item["situation"] = situation
|
|
||||||
_itemsuivi_edit(cnx, item)
|
|
||||||
return situation or scu.IT_SITUATION_MISSING_STR
|
|
||||||
|
|
||||||
|
|
||||||
def itemsuivi_list_etud(etudid, fmt=None):
|
|
||||||
"""Liste des items pour cet étudiant, avec tags"""
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
items = _itemsuivi_list(cnx, {"etudid": etudid})
|
|
||||||
for it in items:
|
|
||||||
it["tags"] = ", ".join(itemsuivi_tag_list(it["itemsuivi_id"]))
|
|
||||||
if fmt == "json":
|
|
||||||
return scu.sendJSON(items)
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def itemsuivi_tag_list(itemsuivi_id):
|
|
||||||
"""les noms de tags associés à cet item"""
|
|
||||||
r = ndb.SimpleDictFetch(
|
|
||||||
"""SELECT t.title
|
|
||||||
FROM itemsuivi_tags_assoc a, itemsuivi_tags t
|
|
||||||
WHERE a.tag_id = t.id
|
|
||||||
AND a.itemsuivi_id = %(itemsuivi_id)s
|
|
||||||
""",
|
|
||||||
{"itemsuivi_id": itemsuivi_id},
|
|
||||||
)
|
|
||||||
return [x["title"] for x in r]
|
|
||||||
|
|
||||||
|
|
||||||
def itemsuivi_tag_search(term):
|
|
||||||
"""List all used tag names (for auto-completion)"""
|
|
||||||
# restrict charset to avoid injections
|
|
||||||
if not scu.ALPHANUM_EXP.match(term):
|
|
||||||
data = []
|
|
||||||
else:
|
|
||||||
r = ndb.SimpleDictFetch(
|
|
||||||
"SELECT title FROM itemsuivi_tags WHERE title LIKE %(term)s AND dept_id=%(dept_id)s",
|
|
||||||
{
|
|
||||||
"term": term + "%",
|
|
||||||
"dept_id": g.scodoc_dept_id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
data = [x["title"] for x in r]
|
|
||||||
|
|
||||||
return scu.sendJSON(data)
|
|
||||||
|
|
||||||
|
|
||||||
def itemsuivi_tag_set(itemsuivi_id="", taglist=None):
|
|
||||||
"""taglist may either be:
|
|
||||||
a string with tag names separated by commas ("un;deux")
|
|
||||||
or a list of strings (["un", "deux"])
|
|
||||||
"""
|
|
||||||
if not sco_permissions_check.can_edit_suivi():
|
|
||||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
|
||||||
if not taglist:
|
|
||||||
taglist = []
|
|
||||||
elif isinstance(taglist, str):
|
|
||||||
taglist = taglist.split(",")
|
|
||||||
taglist = [t.strip() for t in taglist]
|
|
||||||
# log('itemsuivi_tag_set: itemsuivi_id=%s taglist=%s' % (itemsuivi_id, taglist))
|
|
||||||
# Sanity check:
|
|
||||||
cnx = ndb.GetDBConnexion()
|
|
||||||
_ = itemsuivi_get(cnx, itemsuivi_id)
|
|
||||||
|
|
||||||
newtags = set(taglist)
|
|
||||||
oldtags = set(itemsuivi_tag_list(itemsuivi_id))
|
|
||||||
to_del = oldtags - newtags
|
|
||||||
to_add = newtags - oldtags
|
|
||||||
|
|
||||||
# should be atomic, but it's not.
|
|
||||||
for tagname in to_add:
|
|
||||||
t = ItemSuiviTag(tagname, object_id=itemsuivi_id)
|
|
||||||
for tagname in to_del:
|
|
||||||
t = ItemSuiviTag(tagname)
|
|
||||||
t.remove_tag_from_object(itemsuivi_id)
|
|
||||||
return "", http.HTTPStatus.NO_CONTENT
|
|
||||||
|
@ -262,6 +262,7 @@ def module_tag_set(module_id="", taglist=None):
|
|||||||
return scu.json_error(404, "invalid tag")
|
return scu.json_error(404, "invalid tag")
|
||||||
|
|
||||||
# TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
|
# TODO code à moderniser (+ revoir classe ScoTag, utiliser modèle)
|
||||||
|
# TODO Voir ItemSuiviTag et api etud_suivi
|
||||||
|
|
||||||
# Sanity check:
|
# Sanity check:
|
||||||
mod_dict = sco_edit_module.module_list(args={"module_id": module_id})
|
mod_dict = sco_edit_module.module_list(args={"module_id": module_id})
|
||||||
|
@ -16,9 +16,8 @@ function display_itemsuivis(active) {
|
|||||||
.off("click")
|
.off("click")
|
||||||
.click(function (e) {
|
.click(function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$.post(SCO_URL + "itemsuivi_create", {
|
$.post(`${SCO_URL}../api/etudiant/${etudid}/itemsuivi/create`, {
|
||||||
etudid: etudid,
|
etudid: etudid,
|
||||||
fmt: "json",
|
|
||||||
}).done(item_insert_new);
|
}).done(item_insert_new);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -26,13 +25,13 @@ function display_itemsuivis(active) {
|
|||||||
}
|
}
|
||||||
// add existing items
|
// add existing items
|
||||||
$.get(
|
$.get(
|
||||||
SCO_URL + "itemsuivi_list_etud",
|
`${SCO_URL}../api/etudiant/${etudid}/itemsuivis`,
|
||||||
{ etudid: etudid, fmt: "json" },
|
{ },
|
||||||
function (L) {
|
function (L) {
|
||||||
for (var i in L) {
|
for (var i in L) {
|
||||||
item_insert(
|
item_insert(
|
||||||
L[i]["itemsuivi_id"],
|
L[i]["id"],
|
||||||
L[i]["item_date"],
|
L[i]["item_date_dmy"],
|
||||||
L[i]["situation"],
|
L[i]["situation"],
|
||||||
L[i]["tags"],
|
L[i]["tags"],
|
||||||
readonly
|
readonly
|
||||||
@ -49,7 +48,7 @@ function display_itemsuivis(active) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function item_insert_new(it) {
|
function item_insert_new(it) {
|
||||||
item_insert(it.itemsuivi_id, it.item_date, it.situation, "", false);
|
item_insert(it.id, it.item_date, it.situation, "", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function item_insert(itemsuivi_id, item_date, situation, tags, readonly) {
|
function item_insert(itemsuivi_id, item_date, situation, tags, readonly) {
|
||||||
@ -78,35 +77,36 @@ function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
|
|||||||
|
|
||||||
var h = sel_mois;
|
var h = sel_mois;
|
||||||
// situation
|
// situation
|
||||||
h +=
|
h += `<div class="itemsituation editable"
|
||||||
'<div class="itemsituation editable" data-type="textarea" data-url="itemsuivi_set_situation" data-placeholder="<em>décrire situation...</em>" data-object="' +
|
data-type="textarea"
|
||||||
itemsuivi_id +
|
data-url="${SCO_URL}../api/etudiant/itemsuivi/${itemsuivi_id}/set_situation"
|
||||||
'">' +
|
data-placeholder="<em>décrire situation...</em>"
|
||||||
situation +
|
data-object="'${itemsuivi_id}'">${situation}</div>`;
|
||||||
"</div>";
|
|
||||||
// tags:
|
// tags:
|
||||||
h +=
|
h +=
|
||||||
'<div class="itemsuivi_tag_edit"><textarea class="itemsuivi_tag_editor">' +
|
`<div class="itemsuivi_tag_edit">
|
||||||
tags +
|
<textarea class="itemsuivi_tag_editor"
|
||||||
"</textarea></div>";
|
data-itemsuivi_id="${itemsuivi_id}"
|
||||||
|
>${tags}</textarea>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
var nodes = $($.parseHTML('<li class="itemsuivi">' + h + "</li>"));
|
var nodes = $($.parseHTML('<li class="itemsuivi">' + h + "</li>"));
|
||||||
var dp = nodes.find(".itemsuividatepicker");
|
var dp = nodes.find(".itemsuividatepicker");
|
||||||
dp.blur(function (e) {
|
dp.blur(function (e) {
|
||||||
var date = this.value;
|
var date = this.value;
|
||||||
// console.log('selected text: ' + date);
|
if (date) {
|
||||||
$.post(SCO_URL + "itemsuivi_set_date", {
|
$.post(`${SCO_URL}../api/etudiant/itemsuivi/${itemsuivi_id}/set_date`, {
|
||||||
item_date: date,
|
date_dmy: date,
|
||||||
itemsuivi_id: itemsuivi_id,
|
});
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
dp.datepicker({
|
dp.datepicker({
|
||||||
onSelect: function (date, instance) {
|
onSelect: function (date, instance) {
|
||||||
// console.log('selected: ' + date + 'for itemsuivi_id ' + itemsuivi_id);
|
if (date) {
|
||||||
$.post(SCO_URL + "itemsuivi_set_date", {
|
$.post(`${SCO_URL}../api/etudiant/itemsuivi/${itemsuivi_id}/set_date`, {
|
||||||
item_date: date,
|
date_dmy: date,
|
||||||
itemsuivi_id: itemsuivi_id,
|
});
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
showOn: "button",
|
showOn: "button",
|
||||||
buttonImage: "/ScoDoc/static/icons/calendar_img.png",
|
buttonImage: "/ScoDoc/static/icons/calendar_img.png",
|
||||||
@ -129,7 +129,8 @@ function item_nodes(itemsuivi_id, item_date, situation, tags, readonly) {
|
|||||||
initialTags: "",
|
initialTags: "",
|
||||||
placeholder: "Tags...",
|
placeholder: "Tags...",
|
||||||
onChange: function (field, editor, tags) {
|
onChange: function (field, editor, tags) {
|
||||||
$.post("itemsuivi_tag_set", {
|
let itemsuivi_id = field.data("itemsuivi_id");
|
||||||
|
$.post(`${SCO_URL}../api/etudiant/itemsuivi/${itemsuivi_id}/tag`, {
|
||||||
itemsuivi_id: itemsuivi_id,
|
itemsuivi_id: itemsuivi_id,
|
||||||
taglist: tags.join(),
|
taglist: tags.join(),
|
||||||
});
|
});
|
||||||
@ -160,9 +161,20 @@ function Date2DMY(date) {
|
|||||||
return day + "/" + month + "/" + year;
|
return day + "/" + month + "/" + year;
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemsuivi_suppress(itemsuivi_id) {
|
async function itemsuivi_suppress(itemsuivi_id) {
|
||||||
$.post(SCO_URL + "itemsuivi_suppress", { itemsuivi_id: itemsuivi_id });
|
const deleteUrl = `${SCO_URL}../api/etudiant/itemsuivi/${itemsuivi_id}/delete`;
|
||||||
// Clear items and rebuild:
|
|
||||||
$("ul.listdebouches li.itemsuivi").remove();
|
try {
|
||||||
display_itemsuivis(0);
|
const response = await fetch(deleteUrl, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Clear items and rebuild:
|
||||||
|
$("ul.listdebouches li.itemsuivi").remove();
|
||||||
|
display_itemsuivis(0);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting itemsuivi:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -719,46 +719,6 @@ sco_publish(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Debouche / devenir etudiant
|
|
||||||
sco_publish(
|
|
||||||
"/itemsuivi_suppress",
|
|
||||||
sco_debouche.itemsuivi_suppress,
|
|
||||||
Permission.EtudChangeAdr,
|
|
||||||
methods=["GET", "POST"],
|
|
||||||
)
|
|
||||||
sco_publish(
|
|
||||||
"/itemsuivi_create",
|
|
||||||
sco_debouche.itemsuivi_create,
|
|
||||||
Permission.EtudChangeAdr,
|
|
||||||
methods=["GET", "POST"],
|
|
||||||
)
|
|
||||||
sco_publish(
|
|
||||||
"/itemsuivi_set_date",
|
|
||||||
sco_debouche.itemsuivi_set_date,
|
|
||||||
Permission.EtudChangeAdr,
|
|
||||||
methods=["GET", "POST"],
|
|
||||||
)
|
|
||||||
sco_publish(
|
|
||||||
"/itemsuivi_set_situation",
|
|
||||||
sco_debouche.itemsuivi_set_situation,
|
|
||||||
Permission.EtudChangeAdr,
|
|
||||||
methods=["GET", "POST"],
|
|
||||||
)
|
|
||||||
sco_publish(
|
|
||||||
"/itemsuivi_list_etud", sco_debouche.itemsuivi_list_etud, Permission.ScoView
|
|
||||||
)
|
|
||||||
sco_publish("/itemsuivi_tag_list", sco_debouche.itemsuivi_tag_list, Permission.ScoView)
|
|
||||||
sco_publish(
|
|
||||||
"/itemsuivi_tag_search", sco_debouche.itemsuivi_tag_search, Permission.ScoView
|
|
||||||
)
|
|
||||||
sco_publish(
|
|
||||||
"/itemsuivi_tag_set",
|
|
||||||
sco_debouche.itemsuivi_tag_set,
|
|
||||||
Permission.EtudChangeAdr,
|
|
||||||
methods=["GET", "POST"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/doAddAnnotation", methods=["GET", "POST"])
|
@bp.route("/doAddAnnotation", methods=["GET", "POST"])
|
||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.EtudAddAnnotations)
|
@permission_required(Permission.EtudAddAnnotations)
|
||||||
|
Loading…
Reference in New Issue
Block a user