diff --git a/app/api/partitions.py b/app/api/partitions.py index c3aa736db..ccd6124ae 100644 --- a/app/api/partitions.py +++ b/app/api/partitions.py @@ -15,6 +15,7 @@ from app.api.auth import token_auth, token_permission_required 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 @@ -145,6 +146,8 @@ def group_create(partition_id: int): } """ partition: Partition = Partition.query.get_or_404(partition_id) + if not partition.groups_editable: + abort(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: @@ -156,6 +159,7 @@ def group_create(partition_id: int): group = GroupDescr(group_name=group_name, partition_id=partition_id) db.session.add(group) db.session.commit() + sco_cache.invalidate_formsemestre(partition.formsemestre_id) return jsonify(group.to_dict(with_partition=True)) @@ -163,19 +167,25 @@ def group_create(partition_id: int): @token_auth.login_required @token_permission_required(Permission.APIEditGroups) def group_delete(group_id: int): - """Delete group""" + """Suppression d'un groupe""" group = GroupDescr.query.get_or_404(group_id) + if not group.partition.groups_editable: + abort(404, "partition non editable") + formsemestre_id = group.partition.formsemestre_id db.session.delete(group) db.session.commit() + sco_cache.invalidate_formsemestre(formsemestre_id) return jsonify({"OK": 1}) -@bp.route("/group//edit", methods=["POST"]) +@bp.route("/group//edit", methods=["POST"]) @token_auth.login_required @token_permission_required(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: + abort(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: @@ -184,4 +194,124 @@ def group_edit(group_id: int): group.group_name = group_name.strip() db.session.add(group) db.session.commit() + sco_cache.invalidate_formsemestre(group.partition.formsemestre_id) return jsonify(group.to_dict(with_partition=True)) + + +@bp.route("/formsemestre//partition/create", methods=["POST"]) +@token_auth.login_required +@token_permission_required(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: + abort(404, "missing partition_name or invalid data format") + if not Partition.check_name(formsemestre, partition_name): + abort(404, "invalid partition_name") + numero = data.get("numero", 0) + if not isinstance(numero, int): + abort(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(boolean_field, bool): + abort(404, f"invalid type for {boolean_field}") + args[boolean_field] = value + + partition = Partition(**args) + db.session.add(partition) + db.session.commit() + sco_cache.invalidate_formsemestre(formsemestre_id) + return jsonify(partition.to_dict(with_groups=True)) + + +@bp.route("/partition//edit", methods=["POST"]) +@token_auth.login_required +@token_permission_required(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 + ): + abort(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): + abort(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(boolean_field, bool): + abort(404, f"invalid type for {boolean_field}") + setattr(partition, boolean_field, value) + modified = True + + if modified: + db.session.add(partition) + db.session.commit() + sco_cache.invalidate_formsemestre(partition.formsemestre_id) + + return jsonify(partition.to_dict(with_groups=True)) + + +@bp.route("/partition//delete", methods=["POST"]) +@token_auth.login_required +@token_permission_required(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: + abort(404, "ne peut pas supprimer la partition par défaut") + is_parcours = partition.is_parcours() + formsemestre: FormSemestre = partition.formsemestre + db.session.delete(partition) + sco_cache.invalidate_formsemestre(formsemestre.id) + if is_parcours: + formsemestre.update_inscriptions_parcours_from_groups() + return jsonify({"OK": 1}) diff --git a/app/models/formsemestre.py b/app/models/formsemestre.py index b3ad6672a..4016986f9 100644 --- a/app/models/formsemestre.py +++ b/app/models/formsemestre.py @@ -1,4 +1,9 @@ # -*- coding: UTF-8 -* +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## """ScoDoc models: formsemestre """ diff --git a/app/models/groups.py b/app/models/groups.py index dc3ac5101..cd71f2691 100644 --- a/app/models/groups.py +++ b/app/models/groups.py @@ -1,12 +1,17 @@ # -*- coding: UTF-8 -* +############################################################################## +# ScoDoc +# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved. +# See LICENSE +############################################################################## -"""Groups & partitions +"""ScoDoc models: Groups & partitions """ -from typing import Any from app import db from app.models import SHORT_STR_LEN from app.models import GROUPNAME_STR_LEN +from app.scodoc import sco_utils as scu class Partition(db.Model): @@ -41,6 +46,7 @@ class Partition(db.Model): "GroupDescr", backref=db.backref("partition", lazy=True), lazy="dynamic", + cascade="all, delete-orphan", ) def __init__(self, **kwargs): @@ -56,6 +62,27 @@ class Partition(db.Model): def __repr__(self): return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">""" + @classmethod + def check_name( + cls, formsemestre: "FormSemestre", partition_name: str, existing=False + ) -> bool: + """check if a partition named 'partition_name' can be created in the given formsemestre. + If existing is True, allow a partition_name already existing in the formsemestre. + """ + if not isinstance(partition_name, str): + return False + if not len(partition_name.strip()) > 0: + return False + if (not existing) and ( + partition_name in [p.partition_name for p in formsemestre.partitions] + ): + return False + return True + + def is_parcours(self) -> bool: + "Vrai s'il s'agit de la partitoon de parcours" + return self.partition_name == scu.PARTITION_PARCOURS + def to_dict(self, with_groups=False) -> dict: """as a dict, with or without groups""" d = dict(self.__dict__)