############################################################################## # ScoDoc # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ ScoDoc 9 API : partitions """ from flask import g, jsonify, request from flask_login import login_required import app from app import db, log from app.api import api_bp as bp, api_web_bp from app.decorators import scodoc, permission_required from app.scodoc.sco_utils import json_error from app.models import FormSemestre, FormSemestreInscription, Identite from app.models import GroupDescr, Partition from app.models.groups import group_membership from app.scodoc import sco_cache from app.scodoc import sco_groups from app.scodoc.sco_permissions import Permission from app.scodoc import sco_utils as scu @bp.route("/partition/") @api_web_bp.route("/partition/") @login_required @scodoc @permission_required(Permission.ScoView) def partition_info(partition_id: int): """Info sur une partition. Exemple de résultat : ``` { 'bul_show_rank': False, 'formsemestre_id': 39, 'groups': [ {'id': 268, 'name': 'A', 'partition_id': 100}, {'id': 269, 'name': 'B', 'partition_id': 100} ], 'groups_editable': True, 'id': 100, 'numero': 100, 'partition_name': 'TD', 'show_in_lists': True } ``` """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition = query.first_or_404() return jsonify(partition.to_dict(with_groups=True)) @bp.route("/formsemestre//partitions") @api_web_bp.route("/formsemestre//partitions") @login_required @scodoc @permission_required(Permission.ScoView) def formsemestre_partitions(formsemestre_id: int): """Liste de toutes les partitions d'un formsemestre formsemestre_id : l'id d'un formsemestre { partition_id : { "bul_show_rank": False, "formsemestre_id": 1063, "groups" : group_id : { "id" : 12, "name" : "A", "partition_id" : partition_id, } }, ... } """ 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(formsemestre_id) partitions = sorted(formsemestre.partitions, key=lambda p: p.numero or 0) return jsonify( { partition.id: partition.to_dict(with_groups=True) for partition in partitions if partition.partition_name is not None } ) @bp.route("/group//etudiants") @api_web_bp.route("/group//etudiants") @login_required @scodoc @permission_required(Permission.ScoView) def etud_in_group(group_id: int): """ Retourne la liste des étudiants dans un groupe group_id : l'id d'un groupe Exemple de résultat : [ { 'civilite': 'M', 'id': 123456, 'ine': None, 'nip': '987654321', 'nom': 'MARTIN', 'nom_usuel': null, 'prenom': 'JEAN'} }, ... ] """ query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group = query.first_or_404() return jsonify([etud.to_dict_short() for etud in group.etuds]) @bp.route("/group//etudiants/query") @api_web_bp.route("/group//etudiants/query") @login_required @scodoc @permission_required(Permission.ScoView) def etud_in_group_query(group_id: int): """Étudiants du groupe, filtrés par état""" etat = request.args.get("etat") if etat not in {None, scu.INSCRIT, scu.DEMISSION, scu.DEF}: return json_error(404, "etat: valeur invalide") query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group = query.first_or_404() # just to ckeck that group exists in accessible dept query = Identite.query.join(FormSemestreInscription).filter_by( formsemestre_id=group.partition.formsemestre_id ) if etat is not None: query = query.filter_by(etat=etat) query = query.join(group_membership).filter_by(group_id=group_id) return jsonify([etud.to_dict_short() for etud in query]) @bp.route("/group//set_etudiant/", methods=["POST"]) @api_web_bp.route("/group//set_etudiant/", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def set_etud_group(etudid: int, group_id: int): """Affecte l'étudiant au groupe indiqué""" etud = Identite.query.get_or_404(etudid) query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group = query.first_or_404() if not group.partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: return json_error(404, "etud non inscrit au formsemestre du groupe") sco_groups.change_etud_group_in_partition( etudid, group_id, group.partition.to_dict() ) return jsonify({"group_id": group_id, "etudid": etudid}) @bp.route("/group//remove_etudiant/", methods=["POST"]) @api_web_bp.route( "/group//remove_etudiant/", methods=["POST"] ) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def group_remove_etud(group_id: int, etudid: int): """Retire l'étudiant de ce groupe. S'il n'y est pas, ne fait rien.""" etud = Identite.query.get_or_404(etudid) query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group = query.first_or_404() if not group.partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if etud in group.etuds: group.etuds.remove(etud) db.session.commit() sco_cache.invalidate_formsemestre(group.partition.formsemestre_id) return jsonify({"group_id": group_id, "etudid": etudid}) @bp.route( "/partition//remove_etudiant/", methods=["POST"] ) @api_web_bp.route( "/partition//remove_etudiant/", methods=["POST"] ) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def partition_remove_etud(partition_id: int, etudid: int): """Enlève l'étudiant de tous les groupes de cette partition (NB: en principe, un étudiant ne doit être que dans 0 ou 1 groupe d'une partition) """ etud = Identite.query.get_or_404(etudid) query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition = query.first_or_404() if not partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") groups = ( GroupDescr.query.filter_by(partition_id=partition_id) .join(group_membership) .filter_by(etudid=etudid) ) for group in groups: group.etuds.remove(etud) db.session.commit() app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) return jsonify({"partition_id": partition_id, "etudid": etudid}) @bp.route("/partition//group/create", methods=["POST"]) @api_web_bp.route("/partition//group/create", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def group_create(partition_id: int): """Création d'un groupe dans une partition The request content type should be "application/json": { "group_name" : nom_du_groupe, } """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() if not partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not partition.groups_editable: return json_error(403, "partition non editable") data = request.get_json(force=True) # may raise 400 Bad Request group_name = data.get("group_name") if group_name is None: return json_error(404, "missing group name or invalid data format") if not GroupDescr.check_name(partition, group_name): return json_error(404, "invalid group_name") group_name = group_name.strip() group = GroupDescr(group_name=group_name, partition_id=partition_id) db.session.add(group) db.session.commit() log(f"created group {group}") app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) return jsonify(group.to_dict(with_partition=True)) @bp.route("/group//delete", methods=["POST"]) @api_web_bp.route("/group//delete", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def group_delete(group_id: int): """Suppression d'un groupe""" query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group: GroupDescr = query.first_or_404() if not group.partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not group.partition.groups_editable: return json_error(403, "partition non editable") formsemestre_id = group.partition.formsemestre_id log(f"deleting {group}") db.session.delete(group) db.session.commit() app.set_sco_dept(group.partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(formsemestre_id) return jsonify({"OK": True}) @bp.route("/group//edit", methods=["POST"]) @api_web_bp.route("/group//edit", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def group_edit(group_id: int): """Edit a group""" query = GroupDescr.query.filter_by(id=group_id) if g.scodoc_dept: query = ( query.join(Partition).join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) ) group: GroupDescr = query.first_or_404() if not group.partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not group.partition.groups_editable: return json_error(403, "partition non editable") data = request.get_json(force=True) # may raise 400 Bad Request group_name = data.get("group_name") if group_name is not None: group_name = group_name.strip() if not GroupDescr.check_name(group.partition, group_name, existing=True): return json_error(404, "invalid group_name") group.group_name = group_name db.session.add(group) db.session.commit() log(f"modified {group}") app.set_sco_dept(group.partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(group.partition.formsemestre_id) return jsonify(group.to_dict(with_partition=True)) @bp.route("/formsemestre//partition/create", methods=["POST"]) @api_web_bp.route( "/formsemestre//partition/create", methods=["POST"] ) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def partition_create(formsemestre_id: int): """Création d'une partition dans un semestre The request content type should be "application/json": { "partition_name": str, "numero":int, "bul_show_rank":bool, "show_in_lists":bool, "groups_editable":bool } """ 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(formsemestre_id) if not formsemestre.etat: return json_error(403, "formsemestre verrouillé") data = request.get_json(force=True) # may raise 400 Bad Request partition_name = data.get("partition_name") if partition_name is None: return json_error(404, "missing partition_name or invalid data format") if partition_name == scu.PARTITION_PARCOURS: return json_error(404, f"invalid partition_name {scu.PARTITION_PARCOURS}") if not Partition.check_name(formsemestre, partition_name): return json_error(404, "invalid partition_name") numero = data.get("numero", 0) if not isinstance(numero, int): return json_error(404, "invalid type for numero") args = { "formsemestre_id": formsemestre_id, "partition_name": partition_name.strip(), "numero": numero, } for boolean_field in ("bul_show_rank", "show_in_lists", "groups_editable"): value = data.get( boolean_field, False if boolean_field != "groups_editable" else True ) if not isinstance(value, bool): return json_error(404, f"invalid type for {boolean_field}") args[boolean_field] = value partition = Partition(**args) db.session.add(partition) db.session.commit() log(f"created partition {partition}") app.set_sco_dept(formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(formsemestre_id) return jsonify(partition.to_dict(with_groups=True)) @bp.route("/formsemestre//partitions/order", methods=["POST"]) @api_web_bp.route( "/formsemestre//partitions/order", methods=["POST"] ) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def formsemestre_order_partitions(formsemestre_id: int): """Modifie l'ordre des partitions du formsemestre JSON args: [partition_id1, partition_id2, ...] """ 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(formsemestre_id) if not formsemestre.etat: return json_error(403, "formsemestre verrouillé") partition_ids = request.get_json(force=True) # may raise 400 Bad Request if not isinstance(partition_ids, int) and not all( isinstance(x, int) for x in partition_ids ): return json_error( 404, message="paramètre liste des partitions invalide", ) for p_id, numero in zip(partition_ids, range(len(partition_ids))): p = Partition.query.get_or_404(p_id) p.numero = numero db.session.add(p) db.session.commit() app.set_sco_dept(formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(formsemestre_id) return jsonify( [ partition.to_dict() for partition in formsemestre.partitions.order_by(Partition.numero) if partition.partition_name is not None ] ) @bp.route("/partition//groups/order", methods=["POST"]) @api_web_bp.route("/partition//groups/order", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def partition_order_groups(partition_id: int): """Modifie l'ordre des groupes de la partition JSON args: [group_id1, group_id2, ...] """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() if not partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") group_ids = request.get_json(force=True) # may raise 400 Bad Request if not isinstance(group_ids, int) and not all( isinstance(x, int) for x in group_ids ): return json_error( 404, message="paramètre liste de groupe invalide", ) for group_id, numero in zip(group_ids, range(len(group_ids))): group = GroupDescr.query.get_or_404(group_id) group.numero = numero db.session.add(group) db.session.commit() app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) log(f"partition_order_groups: {partition} : {group_ids}") return jsonify(partition.to_dict(with_groups=True)) @bp.route("/partition//edit", methods=["POST"]) @api_web_bp.route("/partition//edit", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def partition_edit(partition_id: int): """Modification d'une partition dans un semestre The request content type should be "application/json" All fields are optional: { "partition_name": str, "numero":int, "bul_show_rank":bool, "show_in_lists":bool, "groups_editable":bool } """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() if not partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") data = request.get_json(force=True) # may raise 400 Bad Request modified = False partition_name = data.get("partition_name") # if partition_name is not None and partition_name != partition.partition_name: if partition.is_parcours(): return json_error(404, f"can't rename {scu.PARTITION_PARCOURS}") if not Partition.check_name( partition.formsemestre, partition_name, existing=True ): return json_error(404, "invalid partition_name") partition.partition_name = partition_name.strip() modified = True numero = data.get("numero") if numero is not None and numero != partition.numero: if not isinstance(numero, int): return json_error(404, "invalid type for numero") partition.numero = numero modified = True for boolean_field in ("bul_show_rank", "show_in_lists", "groups_editable"): value = data.get(boolean_field) if value is not None and value != getattr(partition, boolean_field): if not isinstance(value, bool): return json_error(404, f"invalid type for {boolean_field}") if boolean_field == "groups_editable" and partition.is_parcours(): return json_error(404, f"can't change {scu.PARTITION_PARCOURS}") setattr(partition, boolean_field, value) modified = True if modified: db.session.add(partition) db.session.commit() log(f"modified partition {partition}") app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(partition.formsemestre_id) return jsonify(partition.to_dict(with_groups=True)) @bp.route("/partition//delete", methods=["POST"]) @api_web_bp.route("/partition//delete", methods=["POST"]) @login_required @scodoc @permission_required(Permission.ScoEtudChangeGroups) def partition_delete(partition_id: int): """Suppression d'une partition (et de tous ses groupes). Note 1: La partition par défaut (tous les étudiants du sem.) ne peut pas être supprimée. Note 2: Si la partition de parcours est supprimée, les étudiants sont désinscrits des parcours. """ query = Partition.query.filter_by(id=partition_id) if g.scodoc_dept: query = query.join(FormSemestre).filter_by(dept_id=g.scodoc_dept_id) partition: Partition = query.first_or_404() if not partition.formsemestre.etat: return json_error(403, "formsemestre verrouillé") if not partition.partition_name: return json_error(404, "ne peut pas supprimer la partition par défaut") is_parcours = partition.is_parcours() formsemestre: FormSemestre = partition.formsemestre log(f"deleting partition {partition}") db.session.delete(partition) db.session.commit() app.set_sco_dept(partition.formsemestre.departement.acronym) sco_cache.invalidate_formsemestre(formsemestre.id) if is_parcours: formsemestre.update_inscriptions_parcours_from_groups() return jsonify({"OK": True})