Billets absences: nouvelle API + modernisation code

This commit is contained in:
Emmanuel Viennet 2022-08-01 21:42:19 +02:00
parent fd06d8428a
commit 9a2c3b8174
11 changed files with 405 additions and 190 deletions

View File

@ -23,12 +23,15 @@ def requested_format(default_format="json", allowed_formats=None):
from app.api import tokens from app.api import tokens
from app.api import departements from app.api import (
from app.api import etudiants absences,
from app.api import formations billets_absences,
from app.api import formsemestres departements,
from app.api import partitions etudiants,
from app.api import evaluations formations,
from app.api import jury formsemestres,
from app.api import absences logos,
from app.api import logos partitions,
evaluations,
jury,
)

View File

@ -0,0 +1,84 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""
API : billets d'absences
"""
from flask import g, jsonify, request
from flask_login import login_required
import app
from app import db
from app.api import api_bp as bp, api_web_bp
from app.decorators import scodoc, permission_required
from app.api.errors import error_response
from app.models import BilletAbsence
from app.models.etudiants import Identite
from app.scodoc import sco_abs_billets
from app.scodoc.sco_permissions import Permission
@bp.route("/billets_absence/etudiant/<int:etudid>")
@api_web_bp.route("/billets_absence/etudiant/<int:etudid>")
@login_required
@scodoc
@permission_required(Permission.ScoView)
def billets_absence_etudiant(etudid: int):
"""Liste des billets d'absence pour cet étudiant"""
billets = sco_abs_billets.query_billets_etud(etudid)
return jsonify([billet.to_dict() for billet in billets])
@bp.route("/billets_absence/add", methods=["POST"])
@api_web_bp.route("/billets_absence/add", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoAbsAddBillet)
def billets_absence_add():
"""Ajout d'un billet d'absence"""
data = request.get_json(force=True) # may raise 400 Bad Request
etudid = data.get("etudid")
abs_begin = data.get("abs_begin")
abs_end = data.get("abs_end")
description = data.get("description", "")
justified = data.get("justified", False)
if None in (etudid, abs_begin, abs_end):
return error_response(
404, message="Paramètre manquant: etudid, abs_bein, abs_end requis"
)
query = Identite.query.filter_by(etudid=etudid)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
etud = query.first_or_404()
billet = BilletAbsence(
etudid=etud.id,
abs_begin=abs_begin,
abs_end=abs_end,
description=description,
etat=False,
justified=justified,
)
db.session.add(billet)
db.session.commit()
return jsonify(billet.to_dict())
@bp.route("/billets_absence/<int:billet_id>/delete", methods=["POST"])
@api_web_bp.route("/billets_absence/<int:billet_id>/delete", methods=["POST"])
@login_required
@scodoc
@permission_required(Permission.ScoAbsAddBillet)
def billets_absence_delete(billet_id: int):
"""Suppression d'un billet d'absence"""
query = BilletAbsence.query.filter_by(id=billet_id)
if g.scodoc_dept is not None:
# jointure avec departement de l'étudiant
query = query.join(BilletAbsence.etudiant).filter_by(dept_id=g.scodoc_dept_id)
billet = query.first_or_404()
db.session.delete(billet)
db.session.commit()
return jsonify({"OK": True})

View File

@ -88,7 +88,7 @@ class BilletAbsence(db.Model):
justified = db.Column(db.Boolean(), default=False, server_default="false") justified = db.Column(db.Boolean(), default=False, server_default="false")
def to_dict(self): def to_dict(self):
data = { return {
"id": self.id, "id": self.id,
"billet_id": self.id, "billet_id": self.id,
"etudid": self.etudid, "etudid": self.etudid,
@ -99,4 +99,3 @@ class BilletAbsence(db.Model):
"entry_date": self.entry_date, "entry_date": self.entry_date,
"justified": self.justified, "justified": self.justified,
} }
return data

View File

@ -133,7 +133,7 @@ def sidebar(etudid: int = None):
) )
if sco_preferences.get_preference("handle_billets_abs"): if sco_preferences.get_preference("handle_billets_abs"):
H.append( H.append(
f"""<li><a href="{ url_for('absences.listeBilletsEtud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Billets</a></li>""" f"""<li><a href="{ url_for('absences.billets_etud', scodoc_dept=g.scodoc_dept, etudid=etudid) }">Billets</a></li>"""
) )
H.append( H.append(
f""" f"""

View File

@ -770,33 +770,6 @@ def annule_justif(etudid, jour, matin):
invalidate_abs_etud_date(etudid, jour) invalidate_abs_etud_date(etudid, jour)
# ---- BILLETS
_billet_absenceEditor = ndb.EditableTable(
"billet_absence",
"billet_id",
(
"billet_id",
"etudid",
"abs_begin",
"abs_end",
"description",
"etat",
"entry_date",
"justified",
),
sortkey="entry_date desc",
input_formators={
"etat": bool,
"justified": bool,
},
)
billet_absence_create = _billet_absenceEditor.create
billet_absence_delete = _billet_absenceEditor.delete
billet_absence_list = _billet_absenceEditor.list
billet_absence_edit = _billet_absenceEditor.edit
# ------ HTML Calendar functions (see YearTable function) # ------ HTML Calendar functions (see YearTable function)
# MONTH/DAY NAMES: # MONTH/DAY NAMES:

View File

@ -0,0 +1,148 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
##############################################################################
#
# Gestion scolarite IUT
#
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Emmanuel Viennet emmanuel.viennet@viennet.net
#
##############################################################################
"""Fonctions sur les billets d'absences
"""
from flask import g, url_for
import flask_sqlalchemy
from app.models.absences import BilletAbsence
from app.models.etudiants import Identite
from app.scodoc.gen_tables import GenTable
from app.scodoc import sco_preferences
def query_billets_etud(
etudid: int = None, etat: bool = None
) -> flask_sqlalchemy.BaseQuery:
"""Billets d'absences pour un étudiant.
Si etat, filtre par état.
Si dans un département et que la gestion des billets n'a pas été activée
dans ses préférences, table toujours vide.
"""
if g.scodoc_dept is not None and not sco_preferences.get_preference(
"handle_billets_abs"
):
return []
billets = BilletAbsence.query.filter_by(etudid=etudid)
if etat is not None:
billets = billets.query.filter_by(etat=False)
if g.scodoc_dept is not None:
# jointure avec departement de l'étudiant
billets = billets.join(BilletAbsence.etudiant).filter_by(
dept_id=g.scodoc_dept_id
)
return billets
def table_billets_etud(
etudid: int = None, etat: bool = None, with_links=True
) -> GenTable:
"""Page avec table billets."""
etud = Identite.query.get_or_404(etudid) if etudid is not None else None
billets = query_billets_etud(etud.id, etat)
return table_billets(billets, etud=etud, with_links=with_links)
def table_billets(
billets: list[BilletAbsence], etud: Identite = None, title="", with_links=True
) -> GenTable:
"""Construit une table de billets d'absences"""
rows = []
for billet in billets:
billet_dict = billet.to_dict()
rows.append(billet_dict)
if billet_dict["abs_begin"].hour < 12:
m = " matin"
else:
m = " après-midi"
billet_dict["abs_begin_str"] = billet.abs_begin.strftime("%d/%m/%Y") + m
if billet.abs_end.hour < 12:
m = " matin"
else:
m = " après-midi"
billet_dict["abs_end_str"] = billet.abs_end.strftime("%d/%m/%Y") + m
if billet.etat == 0:
if billet.justified:
billet_dict["etat_str"] = "à traiter"
else:
billet_dict["etat_str"] = "à justifier"
if with_links:
if etud:
billet_dict["_etat_str_target"] = url_for(
"absences.process_billet_absence_form",
billet_id=billet_dict["billet_id"],
scodoc_dept=billet.etudiant.departement.acronym,
etudid=etud.id,
)
else:
billet_dict["_etat_str_target"] = url_for(
"absences.process_billet_absence_form",
billet_id=billet_dict["billet_id"],
scodoc_dept=g.scodoc_dept,
)
billet_dict["_billet_id_target"] = billet_dict["_etat_str_target"]
else:
billet_dict["etat_str"] = "ok"
if not etud:
# ajoute info etudiant
etud = billet.etudiant
if not etud:
billet_dict["nomprenom"] = "???" # should not occur
else:
billet_dict["nomprenom"] = etud.nomprenom
if with_links:
billet_dict["_nomprenom_target"] = url_for(
"scolar.ficheEtud",
scodoc_dept=g.scodoc_dept,
etudid=billet_dict["etudid"],
)
if etud and not title:
title = f"Billets d'absence déclarés par {etud.nomprenom}"
columns_ids = ["billet_id"]
if not etud:
columns_ids += ["nomprenom"]
columns_ids += ["abs_begin_str", "abs_end_str", "description", "etat_str"]
tab = GenTable(
titles={
"billet_id": "Numéro",
"abs_begin_str": "Début",
"abs_end_str": "Fin",
"description": "Raison de l'absence",
"etat_str": "Etat",
},
columns_ids=columns_ids,
page_title=title,
html_title=f"<h2>{title}</h2>",
preferences=sco_preferences.SemPreferences(),
rows=rows,
html_sortable=True,
)
return tab

View File

@ -60,7 +60,8 @@ from flask import g, request
from flask import url_for from flask import url_for
from flask_login import current_user from flask_login import current_user
from app import log from app import db, log
from app import api
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.decorators import ( from app.decorators import (
@ -83,6 +84,7 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_abs from app.scodoc import sco_abs
from app.scodoc import sco_abs_billets
from app.scodoc import sco_abs_views from app.scodoc import sco_abs_views
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_find_etud from app.scodoc import sco_find_etud
@ -142,9 +144,11 @@ def index_html():
Permission.ScoAbsChange Permission.ScoAbsChange
) and sco_preferences.get_preference("handle_billets_abs"): ) and sco_preferences.get_preference("handle_billets_abs"):
H.append( H.append(
""" f"""
<h2 style="margin-top: 30px;">Billets d'absence</h2> <h2 style="margin-top: 30px;">Billets d'absence</h2>
<ul><li><a href="listeBillets">Traitement des billets d'absence en attente</a></li></ul> <ul><li><a href="{url_for("absences.list_billets", scodoc_dept=g.scodoc_dept)
}">Traitement des billets d'absence en attente</a>
</li></ul>
""" """
) )
H.append(html_sco_header.sco_footer()) H.append(html_sco_header.sco_footer())
@ -1105,18 +1109,12 @@ def AddBilletAbsence(
"""Mémorise un "billet" """Mémorise un "billet"
begin et end sont au format ISO (eg "1999-01-08 04:05:06") begin et end sont au format ISO (eg "1999-01-08 04:05:06")
""" """
t0 = time.time() log("Warning: calling deprecated AddBilletAbsence")
begin = str(begin) begin = str(begin)
end = str(end) end = str(end)
code_nip = str(code_nip) if code_nip else None code_nip = str(code_nip) if code_nip else None
# check etudid etud = api.tools.get_etud(etudid=None, nip=None, ine=None)
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
if not etuds:
sco_etud.log_unknown_etud()
raise ScoValueError("étudiant inconnu")
etud = etuds[0]
# check dates # check dates
begin_date = dateutil.parser.isoparse(begin) # may raises ValueError begin_date = dateutil.parser.isoparse(begin) # may raises ValueError
end_date = dateutil.parser.isoparse(end) end_date = dateutil.parser.isoparse(end)
@ -1125,36 +1123,33 @@ def AddBilletAbsence(
# #
justified = bool(justified) justified = bool(justified)
xml_reply = bool(xml_reply) xml_reply = bool(xml_reply)
#
cnx = ndb.GetDBConnexion()
billet_id = sco_abs.billet_absence_create(
cnx,
{
"etudid": etud["etudid"],
"abs_begin": begin,
"abs_end": end,
"description": description,
"etat": False,
"justified": justified,
},
)
if xml_reply: # backward compat if xml_reply: # backward compat
format = "xml" format = "xml"
#
billet = BilletAbsence(
etudid=etud.id,
abs_begin=begin,
abs_end=end,
description=description,
etat=False,
justified=justified,
)
db.session.add(billet)
db.session.commit()
# Renvoie le nouveau billet au format demandé # Renvoie le nouveau billet au format demandé
billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id}) table = sco_abs_billets.table_billets([billet], etud=etud)
tab = _tableBillets(billets, etud=etud) log(f"AddBilletAbsence: new billet_id={billet.id}")
log("AddBilletAbsence: new billet_id=%s (%gs)" % (billet_id, time.time() - t0)) return table.make_page(format=format)
return tab.make_page(format=format)
@bp.route("/AddBilletAbsenceForm", methods=["GET", "POST"]) @bp.route("/add_billets_absence_form", methods=["GET", "POST"])
@scodoc @scodoc
@permission_required(Permission.ScoAbsAddBillet) @permission_required(Permission.ScoAbsAddBillet)
@scodoc7func @scodoc7func
def AddBilletAbsenceForm(etudid): def add_billets_absence_form(etudid):
"""Formulaire ajout billet (pour tests seulement, le vrai formulaire accessible aux etudiants """Formulaire ajout billet (pour tests seulement, le vrai
étant sur le portail étudiant). formulaire accessible aux etudiants étant sur le portail étudiant).
""" """
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0] etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
H = [ H = [
@ -1195,125 +1190,50 @@ def AddBilletAbsenceForm(etudid):
justified=tf[2]["justified"], justified=tf[2]["justified"],
) )
) )
return flask.redirect("listeBilletsEtud?etudid=" + str(etudid)) return flask.redirect("billets_etud?etudid=" + str(etudid))
def _tableBillets(billets, etud=None, title=""): @bp.route("/billets_etud/<int:etudid>")
for b in billets:
if b["abs_begin"].hour < 12:
m = " matin"
else:
m = " après-midi"
b["abs_begin_str"] = b["abs_begin"].strftime("%d/%m/%Y") + m
if b["abs_end"].hour < 12:
m = " matin"
else:
m = " après-midi"
b["abs_end_str"] = b["abs_end"].strftime("%d/%m/%Y") + m
if b["etat"] == 0:
if b["justified"]:
b["etat_str"] = "à traiter"
else:
b["etat_str"] = "à justifier"
b["_etat_str_target"] = (
"ProcessBilletAbsenceForm?billet_id=%s" % b["billet_id"]
)
if etud:
b["_etat_str_target"] += "&etudid=%s" % etud["etudid"]
b["_billet_id_target"] = b["_etat_str_target"]
else:
b["etat_str"] = "ok"
if not etud:
# ajoute info etudiant
e = sco_etud.get_etud_info(etudid=b["etudid"], filled=1)
if not e:
b["nomprenom"] = "???" # should not occur
else:
b["nomprenom"] = e[0]["nomprenom"]
b["_nomprenom_target"] = url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=b["etudid"]
)
if etud and not title:
title = "Billets d'absence déclarés par %(nomprenom)s" % etud
else:
title = title
columns_ids = ["billet_id"]
if not etud:
columns_ids += ["nomprenom"]
columns_ids += ["abs_begin_str", "abs_end_str", "description", "etat_str"]
tab = GenTable(
titles={
"billet_id": "Numéro",
"abs_begin_str": "Début",
"abs_end_str": "Fin",
"description": "Raison de l'absence",
"etat_str": "Etat",
},
columns_ids=columns_ids,
page_title=title,
html_title="<h2>%s</h2>" % title,
preferences=sco_preferences.SemPreferences(),
rows=billets,
html_sortable=True,
)
return tab
@bp.route("/listeBilletsEtud")
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func def billets_etud(etudid=False):
def listeBilletsEtud(etudid=False, format="html"):
"""Liste billets pour un etudiant""" """Liste billets pour un etudiant"""
etuds = sco_etud.get_etud_info(filled=True, etudid=etudid) fmt = request.args.get("format", "html")
if not etuds: if not fmt in {"html", "json", "xml", "xls", "xlsx"}:
sco_etud.log_unknown_etud() return ScoValueError("Format invalide")
raise ScoValueError("étudiant inconnu") table = sco_abs_billets.table_billets_etud(etudid)
if table:
etud = etuds[0] return table.make_page(format=fmt)
cnx = ndb.GetDBConnexion() return ""
billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]})
tab = _tableBillets(billets, etud=etud)
return tab.make_page(format=format)
@bp.route( # DEEPRECATED: pour compat anciens clients PHP
"/XMLgetBilletsEtud", methods=["GET", "POST"] @bp.route("/XMLgetBilletsEtud", methods=["GET", "POST"])
) # pour compat anciens clients PHP
@scodoc @scodoc
@permission_required_compat_scodoc7(Permission.ScoView) @permission_required_compat_scodoc7(Permission.ScoView)
@scodoc7func @scodoc7func
def XMLgetBilletsEtud(etudid=False): def XMLgetBilletsEtud(etudid=False):
"""Liste billets pour un etudiant""" """Liste billets pour un etudiant"""
if not sco_preferences.get_preference("handle_billets_abs"): log("Warning: called deprecated XMLgetBilletsEtud")
table = sco_abs_billets.table_billets_etud(etudid)
if table:
return table.make_page(format="xml")
return "" return ""
t0 = time.time()
r = listeBilletsEtud(etudid, format="xml")
log("XMLgetBilletsEtud (%gs)" % (time.time() - t0))
return r
@bp.route("/listeBillets", methods=["GET"]) @bp.route("/list_billets", methods=["GET"])
@scodoc @scodoc
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def listeBillets(): def list_billets():
"""Page liste des billets non traités et formulaire recherche d'un billet""" """Page liste des billets non traités pour tous les étudiants du département
# utilise Flask, jointure avec departement de l'étudiant et formulaire recherche d'un billet.
billets = ( """
BilletAbsence.query.filter_by(etat=False) table = sco_abs_billets.table_billets_etud(etat=False)
.join(BilletAbsence.etudiant, aliased=True) T = table.html()
.filter_by(dept_id=g.scodoc_dept_id)
)
# reconverti en dict pour les fonctions scodoc7
billets = [b.to_dict() for b in billets]
#
tab = _tableBillets(billets)
T = tab.html()
H = [ H = [
html_sco_header.sco_header(page_title="Billet d'absence non traités"), html_sco_header.sco_header(page_title="Billet d'absence non traités"),
"<h2>Billets d'absence en attente de traitement (%d)</h2>" % len(billets), f"<h2>Billets d'absence en attente de traitement ({table.get_nb_rows()})</h2>",
] ]
tf = TrivialFormulator( tf = TrivialFormulator(
@ -1326,34 +1246,38 @@ def listeBillets():
return "\n".join(H) + tf[1] + T + html_sco_header.sco_footer() return "\n".join(H) + tf[1] + T + html_sco_header.sco_footer()
else: else:
return flask.redirect( return flask.redirect(
"ProcessBilletAbsenceForm?billet_id=" + tf[2]["billet_id"] url_for(
"absences.process_billet_absence_form",
billet_id=tf[2]["billet_id"],
scodoc_dept=g.scodoc_dept,
)
) )
@bp.route("/deleteBilletAbsence", methods=["POST", "GET"]) @bp.route("/delete_billets_absence", methods=["POST", "GET"])
@scodoc @scodoc
@permission_required(Permission.ScoAbsChange) @permission_required(Permission.ScoAbsChange)
@scodoc7func @scodoc7func
def deleteBilletAbsence(billet_id, dialog_confirmed=False): def delete_billets_absence(billet_id, dialog_confirmed=False):
"""Supprime un billet.""" """Supprime un billet."""
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id}) billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id})
if not billets: if not billets:
return flask.redirect( return flask.redirect(
"listeBillets?head_message=Billet%%20%s%%20inexistant !" % billet_id "list_billets?head_message=Billet%%20%s%%20inexistant !" % billet_id
) )
if not dialog_confirmed: if not dialog_confirmed:
tab = _tableBillets(billets) tab = sco_abs_billets.table_billets(billets)
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Supprimer ce billet ?</h2>""" + tab.html(), """<h2>Supprimer ce billet ?</h2>""" + tab.html(),
dest_url="", dest_url="",
cancel_url="listeBillets", cancel_url="list_billets",
parameters={"billet_id": billet_id}, parameters={"billet_id": billet_id},
) )
sco_abs.billet_absence_delete(cnx, billet_id) sco_abs.billet_absence_delete(cnx, billet_id)
return flask.redirect("listeBillets?head_message=Billet%20supprimé") return flask.redirect("list_billets?head_message=Billet%20supprimé")
def _ProcessBilletAbsence(billet, estjust, description): def _ProcessBilletAbsence(billet, estjust, description):
@ -1417,17 +1341,17 @@ def _ProcessBilletAbsence(billet, estjust, description):
return n return n
@bp.route("/ProcessBilletAbsenceForm", methods=["POST", "GET"]) @bp.route("/process_billet_absence_form", methods=["POST", "GET"])
@scodoc @scodoc
@permission_required(Permission.ScoAbsChange) @permission_required(Permission.ScoAbsChange)
@scodoc7func @scodoc7func
def ProcessBilletAbsenceForm(billet_id): def process_billet_absence_form(billet_id):
"""Formulaire traitement d'un billet""" """Formulaire traitement d'un billet"""
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id}) billets = sco_abs.billet_absence_list(cnx, {"billet_id": billet_id})
if not billets: if not billets:
return flask.redirect( return flask.redirect(
"listeBillets?head_message=Billet%%20%s%%20inexistant !" % billet_id "list_billets?head_message=Billet%%20%s%%20inexistant !" % billet_id
) )
billet = billets[0] billet = billets[0]
etudid = billet["etudid"] etudid = billet["etudid"]
@ -1468,17 +1392,17 @@ def ProcessBilletAbsenceForm(billet_id):
submitlabel="Enregistrer ces absences", submitlabel="Enregistrer ces absences",
) )
if tf[0] == 0: if tf[0] == 0:
tab = _tableBillets([billet], etud=etud) tab = sco_abs_billets.table_billets([billet], etud=etud)
H.append(tab.html()) H.append(tab.html())
if billet["justified"]: if billet["justified"]:
H.append( H.append(
"""<p>L'étudiant pense pouvoir justifier cette absence.<br/><em>Vérifiez le justificatif avant d'enregistrer.</em></p>""" """<p>L'étudiant pense pouvoir justifier cette absence.<br/><em>Vérifiez le justificatif avant d'enregistrer.</em></p>"""
) )
F = ( F = (
"""<p><a class="stdlink" href="deleteBilletAbsence?billet_id=%s">Supprimer ce billet</a> (utiliser en cas d'erreur, par ex. billet en double)</p>""" """<p><a class="stdlink" href="delete_billets_absence?billet_id=%s">Supprimer ce billet</a> (utiliser en cas d'erreur, par ex. billet en double)</p>"""
% billet_id % billet_id
) )
F += '<p><a class="stdlink" href="listeBillets">Liste de tous les billets en attente</a></p>' F += '<p><a class="stdlink" href="list_billets">Liste de tous les billets en attente</a></p>'
return "\n".join(H) + "<br/>" + tf[1] + F + html_sco_header.sco_footer() return "\n".join(H) + "<br/>" + tf[1] + F + html_sco_header.sco_footer()
elif tf[0] == -1: elif tf[0] == -1:
@ -1497,11 +1421,11 @@ def ProcessBilletAbsenceForm(billet_id):
elif n < 0: elif n < 0:
H.append("Ce billet avait déjà été traité !") H.append("Ce billet avait déjà été traité !")
H.append( H.append(
'</div><p><a class="stdlink" href="listeBillets">Autre billets en attente</a></p><h4>Billets déclarés par %s</h4>' '</div><p><a class="stdlink" href="list_billets">Autre billets en attente</a></p><h4>Billets déclarés par %s</h4>'
% (etud["nomprenom"]) % (etud["nomprenom"])
) )
billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]}) billets = sco_abs.billet_absence_list(cnx, {"etudid": etud["etudid"]})
tab = _tableBillets(billets, etud=etud) tab = sco_abs_billets.table_billets(billets, etud=etud)
H.append(tab.html()) H.append(tab.html())
return "\n".join(H) + html_sco_header.sco_footer() return "\n".join(H) + html_sco_header.sco_footer()

View File

@ -38,3 +38,32 @@ def api_headers() -> dict:
r0 = requests.post(API_URL + "/tokens", auth=(API_USER, API_PASSWORD)) r0 = requests.post(API_URL + "/tokens", auth=(API_USER, API_PASSWORD))
token = r0.json()["token"] token = r0.json()["token"]
return {"Authorization": f"Bearer {token}"} return {"Authorization": f"Bearer {token}"}
class APIError(Exception):
pass
def GET(path: str, headers={}, errmsg=None, dept=None):
"""Get and returns as JSON"""
if dept:
url = SCODOC_URL + f"/ScoDoc/{dept}/api" + path
else:
url = API_URL + path
r = requests.get(url, headers=headers, verify=CHECK_CERTIFICATE)
if r.status_code != 200:
raise APIError(errmsg or f"""erreur status={r.status_code} !\n{r.text}""")
return r.json() # decode la reponse JSON
def POST_JSON(path: str, data: dict = {}, headers={}, errmsg=None):
"""Post"""
r = requests.post(
API_URL + path,
json=data,
headers=headers,
verify=CHECK_CERTIFICATE,
)
if r.status_code != 200:
raise APIError(errmsg or f"erreur status={r.status_code} !\n{r.text}")
return r.json() # decode la reponse JSON

View File

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
"""Test API Billets Absences
Utilisation :
pytest tests/api/test_api_billets.py
"""
import datetime
import requests
from tests.api.setup_test_api import GET, POST_JSON, api_headers
ETUDID = 1
def test_billets(api_headers):
"""
Ajout, Liste, Suppression billets absences
Routes :
- /billets_absence/add
- /billets_absence/etudiant/<int:etudid>[?format=xml|json]
- /billets_absence/delete
"""
billet_d = dict(
etudid=ETUDID,
abs_begin="2022-07-31",
abs_end="2022-08-01",
description="test 1",
)
billet_r = POST_JSON("/billets_absence/add", billet_d, headers=api_headers)
assert billet_r["etudid"] == billet_d["etudid"]
assert datetime.datetime.fromisoformat(billet_r["abs_begin"]).replace(
tzinfo=None
) == datetime.datetime.fromisoformat(billet_d["abs_begin"])
billets = GET("/billets_absence/etudiant/1", headers=api_headers)
assert isinstance(billets, list)
assert len(billets) == 1
assert billets[0] == billet_r
billet_d2 = dict(
etudid=ETUDID,
abs_begin="2022-08-01",
abs_end="2022-08-03",
description="test 2",
)
billet_r = POST_JSON("/billets_absence/add", billet_d2, headers=api_headers)
billets = GET("/billets_absence/etudiant/1", headers=api_headers)
assert len(billets) == 2
# Suppression
for billet in billets:
reply = POST_JSON(
f"/billets_absence/{billet['id']}/delete", headers=api_headers
)
assert reply["OK"] == True

View File

@ -125,7 +125,7 @@ def test_abs_basic(test_client):
- set_group - set_group
- EtatAbsenceGr - EtatAbsenceGr
- AddBilletAbsence - AddBilletAbsence
- listeBilletsEtud - billets_etud
""" """
G = sco_fake_gen.ScoFake(verbose=False) G = sco_fake_gen.ScoFake(verbose=False)
@ -333,9 +333,7 @@ def test_abs_basic(test_client):
code_ine=etuds[0]["code_ine"], code_ine=etuds[0]["code_ine"],
) )
li_bi = absences.listeBilletsEtud(etudid=etudid, format="json").get_data( li_bi = absences.billets_etud(etudid=etudid, format="json").get_data(as_text=True)
as_text=True
)
assert isinstance(li_bi, str) assert isinstance(li_bi, str)
load_li_bi = json.loads(li_bi) load_li_bi = json.loads(li_bi)

View File

@ -88,6 +88,9 @@ def create_users(dept: Departement) -> tuple:
sys.exit(1) sys.exit(1)
perm_sco_view = Permission.get_by_name("ScoView") perm_sco_view = Permission.get_by_name("ScoView")
role.add_permission(perm_sco_view) role.add_permission(perm_sco_view)
# Edition billets
perm_billets = Permission.get_by_name("ScoAbsAddBillet")
role.add_permission(perm_billets)
db.session.add(role) db.session.add(role)
user.add_role(role, None) user.add_role(role, None)