############################################################################## # ScoDoc # Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ ScoDoc 9 API : partitions """ from flask import jsonify, request import app from app import db, log from app.api import bp from app.api.auth import permission_required_api from app.api.errors import error_response 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.sco_permissions import Permission from app.scodoc import sco_utils as scu @bp.route("/partition/", methods=["GET"]) @permission_required_api(Permission.ScoView, Permission.APIView) 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 } ``` """ partition = Partition.query.get_or_404(partition_id) return jsonify(partition.to_dict(with_groups=True)) @bp.route("/formsemestre//partitions", methods=["GET"]) @permission_required_api(Permission.ScoView, Permission.APIView) 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, } }, ... } """ formsemestre: FormSemestre = FormSemestre.query.get_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", methods=["GET"]) @permission_required_api(Permission.ScoView, Permission.APIView) 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'} }, ... ] """ group = GroupDescr.query.get_or_404(group_id) return jsonify([etud.to_dict_short() for etud in group.etuds]) @bp.route("/group//etudiants/query", methods=["GET"]) @permission_required_api(Permission.ScoView, Permission.APIView) def etud_in_group_query(group_id: int): """Etudiants du groupe, filtrés par état""" etat = request.args.get("etat") if etat not in {scu.INSCRIT, scu.DEMISSION, scu.DEF}: return error_response(404, "etat: valeur invalide") group = GroupDescr.query.get_or_404(group_id) query = ( Identite.query.join(FormSemestreInscription) .filter_by(formsemestre_id=group.partition.formsemestre_id, etat=etat) .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"]) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) def set_etud_group(etudid: int, group_id: int): """Affecte l'étudiant au groupe indiqué""" etud = Identite.query.get_or_404(etudid) group = GroupDescr.query.get_or_404(group_id) if etud.id not in {e.id for e in group.partition.formsemestre.etuds}: return error_response(404, "etud non inscrit au formsemestre du groupe") groups = ( GroupDescr.query.filter_by(partition_id=group.partition.id) .join(group_membership) .filter_by(etudid=etudid) ) ok = False for g in groups: if g.id == group_id: ok = True else: g.etuds.remove(etud) if not ok: group.etuds.append(etud) log(f"set_etud_group({etud}, {group})") db.session.commit() return jsonify({"group_id": group_id, "etudid": etudid}) @bp.route( "/partition//remove_etudiant/", methods=["POST"] ) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) def partition_remove_etud(partition_id: int, etudid: int): """ """ etud = Identite.query.get_or_404(etudid) groups = ( GroupDescr.query.filter_by(partition_id=partition_id) .join(group_membership) .filter_by(etudid=etudid) ) for g in groups: g.etuds.remove(etud) db.session.commit() return jsonify({"partition_id": partition_id, "etudid": etudid}) @bp.route("/partition//group/create", methods=["POST"]) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) 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, } """ partition: Partition = Partition.query.get_or_404(partition_id) if not partition.groups_editable: return error_response(404, "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 error_response(404, "missing group name or invalid data format") if not GroupDescr.check_name(partition, group_name): return error_response(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"]) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) def group_delete(group_id: int): """Suppression d'un groupe""" group = GroupDescr.query.get_or_404(group_id) if not group.partition.groups_editable: return error_response(404, "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": 1}) @bp.route("/group//edit", methods=["POST"]) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) def group_edit(group_id: int): """Edit a group""" group: GroupDescr = GroupDescr.query.get_or_404(group_id) if not group.partition.groups_editable: return error_response(404, "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: if not GroupDescr.check_name(group.partition, group_name, existing=True): return error_response(404, "invalid group_name") group.group_name = group_name.strip() 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"]) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) 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 } """ formsemestre: FormSemestre = FormSemestre.query.get_or_404(formsemestre_id) data = request.get_json(force=True) # may raise 400 Bad Request partition_name = data.get("partition_name") if partition_name is None: return error_response(404, "missing partition_name or invalid data format") if not Partition.check_name(formsemestre, partition_name): return error_response(404, "invalid partition_name") numero = data.get("numero", 0) if not isinstance(numero, int): return error_response(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 error_response(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"]) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) def formsemestre_order_partitions(formsemestre_id: int): """Modifie l'ordre des partitions du formsemestre JSON args: [partition_id1, partition_id2, ...] """ formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id) 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 error_response( 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() return jsonify(formsemestre.to_dict()) @bp.route("/partition//groups/order", methods=["POST"]) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) def partition_order_groups(partition_id: int): """Modifie l'ordre des groupes de la partition JSON args: [group_id1, group_id2, ...] """ partition = Partition.query.get_or_404(partition_id) 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 error_response( 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() return jsonify(partition.to_dict(with_groups=True)) @bp.route("/partition//edit", methods=["POST"]) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) 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 } """ partition = Partition.query.get_or_404(partition_id) 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 not Partition.check_name( partition.formsemestre, partition_name, existing=True ): return error_response(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 error_response(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 error_response(404, f"invalid type for {boolean_field}") 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"]) @permission_required_api(Permission.ScoEtudChangeGroups, Permission.APIEditGroups) 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. """ partition = Partition.query.get_or_404(partition_id) if not partition.partition_name: return error_response(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": 1})