############################################################################## # ScoDoc # Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ ScoDoc 9 API : jury WIP à compléter avec enregistrement décisions """ import datetime from flask import g, request, url_for from flask_json import as_json from flask_login import current_user, login_required import app from app import db, log from app.api import api_bp as bp, api_web_bp, API_CLIENT_ERROR, tools from app.decorators import scodoc, permission_required from app.scodoc.sco_exceptions import ScoException from app.but import jury_but_results from app.models import ( ApcParcours, ApcValidationAnnee, ApcValidationRCUE, Formation, FormSemestre, Identite, ScolarAutorisationInscription, ScolarFormSemestreValidation, ScolarNews, Scolog, UniteEns, ) from app.scodoc import codes_cursus from app.scodoc import sco_cache from app.scodoc.sco_permissions import Permission from app.scodoc.sco_utils import json_error @bp.route("/formsemestre//decisions_jury") @api_web_bp.route("/formsemestre//decisions_jury") @login_required @scodoc @permission_required(Permission.ScoView) @as_json def decisions_jury(formsemestre_id: int): """Décisions du jury des étudiants du formsemestre.""" # APC, pair: formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id) if formsemestre is None: return json_error( 404, message="formsemestre inconnu", ) if formsemestre.formation.is_apc(): app.set_sco_dept(formsemestre.departement.acronym) rows = jury_but_results.get_jury_but_results(formsemestre) return rows else: raise ScoException("non implemente") def _news_delete_jury_etud(etud: Identite): "génère news sur effacement décision" # n'utilise pas g.scodoc_dept, pas toujours dispo en mode API url = url_for( "scolar.ficheEtud", scodoc_dept=etud.departement.acronym, etudid=etud.id ) ScolarNews.add( typ=ScolarNews.NEWS_JURY, obj=etud.id, text=f"""Suppression décision jury pour {etud.nomprenom}""", url=url, ) @bp.route( "/etudiant//jury/validation_ue//delete", methods=["POST"], ) @api_web_bp.route( "/etudiant//jury/validation_ue//delete", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.ScoView) @as_json def validation_ue_delete(etudid: int, validation_id: int): "Efface cette validation" return _validation_ue_delete(etudid, validation_id) @bp.route( "/etudiant//jury/validation_formsemestre//delete", methods=["POST"], ) @api_web_bp.route( "/etudiant//jury/validation_formsemestre//delete", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.ScoView) @as_json def validation_formsemestre_delete(etudid: int, validation_id: int): "Efface cette validation" # c'est la même chose (formations classiques) return _validation_ue_delete(etudid, validation_id) def _validation_ue_delete(etudid: int, validation_id: int): "Efface cette validation (semestres classiques ou UEs)" etud = tools.get_etud(etudid) if etud is None: return "étudiant inconnu", 404 validation = ScolarFormSemestreValidation.query.filter_by( id=validation_id, etudid=etudid ).first_or_404() # Vérification de la permission: # A le droit de supprimer cette validation: le chef de dept ou quelqu'un ayant # le droit de saisir des décisions de jury dans le formsemestre concerné s'il y en a un # (c'est le cas pour les validations de jury, mais pas pour les "antérieures" non # rattachées à un formsemestre) if not g.scodoc_dept: # accès API if not current_user.has_permission(Permission.EtudInscrit): return json_error(403, "opération non autorisée (117)") else: if validation.formsemestre: if ( validation.formsemestre.dept_id != g.scodoc_dept_id ) or not validation.formsemestre.can_edit_jury(): return json_error(403, "opération non autorisée (123)") elif not current_user.has_permission(Permission.EtudInscrit): # Validation non rattachée à un semestre: on doit être chef return json_error(403, "opération non autorisée (126)") log(f"validation_ue_delete: etuid={etudid} {validation}") db.session.delete(validation) sco_cache.invalidate_formsemestre_etud(etud) db.session.commit() _news_delete_jury_etud(etud) return "ok" @bp.route( "/etudiant//jury/autorisation_inscription//delete", methods=["POST"], ) @api_web_bp.route( "/etudiant//jury/autorisation_inscription//delete", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EtudInscrit) @as_json def autorisation_inscription_delete(etudid: int, validation_id: int): "Efface cette validation" etud = tools.get_etud(etudid) if etud is None: return "étudiant inconnu", 404 validation = ScolarAutorisationInscription.query.filter_by( id=validation_id, etudid=etudid ).first_or_404() log(f"autorisation_inscription_delete: etuid={etudid} {validation}") db.session.delete(validation) sco_cache.invalidate_formsemestre_etud(etud) db.session.commit() _news_delete_jury_etud(etud) return "ok" @bp.route( "/etudiant//jury/validation_rcue/record", methods=["POST"], ) @api_web_bp.route( "/etudiant//jury/validation_rcue/record", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EtudInscrit) @as_json def validation_rcue_record(etudid: int): """Enregistre une validation de RCUE. Si une validation existe déjà pour ce RCUE, la remplace. The request content type should be "application/json": { "code" : str, "ue1_id" : int, "ue2_id" : int, // Optionnel: "formsemestre_id" : int, "date" : date_iso, // si non spécifié, now() "parcours_id" :int, } """ etud = tools.get_etud(etudid) if etud is None: return json_error(404, "étudiant inconnu") data = request.get_json(force=True) # may raise 400 Bad Request code = data.get("code") if code is None: return json_error(API_CLIENT_ERROR, "missing argument: code") if code not in codes_cursus.CODES_JURY_RCUE: return json_error(API_CLIENT_ERROR, "invalid code value") ue1_id = data.get("ue1_id") if ue1_id is None: return json_error(API_CLIENT_ERROR, "missing argument: ue1_id") try: ue1_id = int(ue1_id) except ValueError: return json_error(API_CLIENT_ERROR, "invalid value for ue1_id") ue2_id = data.get("ue2_id") if ue2_id is None: return json_error(API_CLIENT_ERROR, "missing argument: ue2_id") try: ue2_id = int(ue2_id) except ValueError: return json_error(API_CLIENT_ERROR, "invalid value for ue2_id") formsemestre_id = data.get("formsemestre_id") date_validation_str = data.get("date", datetime.datetime.now().isoformat()) parcours_id = data.get("parcours_id") # query = UniteEns.query.filter_by(id=ue1_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) ue1: UniteEns = query.first_or_404() query = UniteEns.query.filter_by(id=ue2_id) if g.scodoc_dept: query = query.join(Formation).filter_by(dept_id=g.scodoc_dept_id) ue2: UniteEns = query.first_or_404() if ue1.niveau_competence_id != ue2.niveau_competence_id: return json_error( API_CLIENT_ERROR, "UEs non associees au meme niveau de competence" ) if formsemestre_id is not None: query = FormSemestre.query.filter_by(id=formsemestre_id) if g.scodoc_dept: query = query.filter_by(dept_id=g.scodoc_dept_id) formsemestre: FormSemestre = query.first_or_404() if (formsemestre.formation_id != ue1.formation_id) or ( formsemestre.formation_id != ue2.formation_id ): return json_error( API_CLIENT_ERROR, "ues et semestre ne sont pas de la meme formation" ) else: formsemestre = None try: date_validation = datetime.datetime.fromisoformat(date_validation_str) except ValueError: return json_error(API_CLIENT_ERROR, "invalid date string") if parcours_id is not None: parcours: ApcParcours = ApcParcours.query.get_or_404(parcours_id) if parcours.referentiel_id != ue1.niveau_competence.competence.referentiel_id: return json_error(API_CLIENT_ERROR, "niveau et parcours incompatibles") # Une validation pour ce niveau de compétence existe-elle ? validation = ( ApcValidationRCUE.query.filter_by(etudid=etudid) .join(UniteEns, UniteEns.id == ApcValidationRCUE.ue2_id) .filter_by(niveau_competence_id=ue2.niveau_competence_id) .first() ) if validation: validation.code = code validation.date = date_validation validation.formsemestre_id = formsemestre_id validation.parcours_id = parcours_id validation.ue1_id = ue1_id validation.ue2_id = ue2_id operation = "update" else: validation = ApcValidationRCUE( code=code, date=date_validation, etudid=etudid, formsemestre_id=formsemestre_id, parcours_id=parcours_id, ue1_id=ue1_id, ue2_id=ue2_id, ) operation = "record" db.session.add(validation) # invalider bulletins (les autres résultats ne dépendent pas des RCUEs): sco_cache.invalidate_formsemestre_etud(etud) db.session.commit() Scolog.logdb( method="validation_rcue_record", etudid=etudid, msg=f"Enregistrement {validation}", commit=True, ) log(f"{operation} {validation}") return validation.to_dict() @bp.route( "/etudiant//jury/validation_rcue//delete", methods=["POST"], ) @api_web_bp.route( "/etudiant//jury/validation_rcue//delete", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EtudInscrit) @as_json def validation_rcue_delete(etudid: int, validation_id: int): "Efface cette validation" etud = tools.get_etud(etudid) if etud is None: return "étudiant inconnu", 404 validation = ApcValidationRCUE.query.filter_by( id=validation_id, etudid=etudid ).first_or_404() log(f"validation_ue_delete: etuid={etudid} {validation}") db.session.delete(validation) sco_cache.invalidate_formsemestre_etud(etud) db.session.commit() _news_delete_jury_etud(etud) return "ok" @bp.route( "/etudiant//jury/validation_annee_but//delete", methods=["POST"], ) @api_web_bp.route( "/etudiant//jury/validation_annee_but//delete", methods=["POST"], ) @login_required @scodoc @permission_required(Permission.EtudInscrit) @as_json def validation_annee_but_delete(etudid: int, validation_id: int): "Efface cette validation" etud = tools.get_etud(etudid) if etud is None: return "étudiant inconnu", 404 validation = ApcValidationAnnee.query.filter_by( id=validation_id, etudid=etudid ).first_or_404() log(f"validation_annee_but: etuid={etudid} {validation}") db.session.delete(validation) sco_cache.invalidate_formsemestre_etud(etud) db.session.commit() _news_delete_jury_etud(etud) return "ok"