# -*- coding: UTF-8 -* ############################################################################## # ScoDoc # Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved. # See LICENSE ############################################################################## """ScoDoc models: Groups & partitions """ from operator import attrgetter from sqlalchemy.exc import IntegrityError 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 from app.scodoc.sco_exceptions import ScoValueError class Partition(db.Model): """Partition: découpage d'une promotion en groupes""" __table_args__ = (db.UniqueConstraint("formsemestre_id", "partition_name"),) id = db.Column(db.Integer, primary_key=True) partition_id = db.synonym("id") formsemestre_id = db.Column( db.Integer, db.ForeignKey("notes_formsemestre.id"), index=True, ) # "TD", "TP", ... (NULL for 'all') partition_name = db.Column(db.String(SHORT_STR_LEN)) # Numero = ordre de presentation) numero = db.Column(db.Integer, nullable=False, default=0) # Calculer le rang ? bul_show_rank = db.Column( db.Boolean(), nullable=False, default=False, server_default="false" ) # Montrer quand on indique les groupes de l'étudiant ? show_in_lists = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) # Editable (créer/renommer groupes) ? (faux pour les groupes de parcours) groups_editable = db.Column( db.Boolean(), nullable=False, default=True, server_default="true" ) groups = db.relationship( "GroupDescr", backref=db.backref("partition", lazy=True), lazy="dynamic", cascade="all, delete-orphan", order_by="GroupDescr.numero", ) def __init__(self, **kwargs): super(Partition, self).__init__(**kwargs) if self.numero is None: # génère numero à la création last_partition = Partition.query.order_by(Partition.numero.desc()).first() if last_partition: self.numero = last_partition.numero + 1 else: self.numero = 1 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 (0 < len(partition_name.strip()) < SHORT_STR_LEN): 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 partition de parcours" return self.partition_name == scu.PARTITION_PARCOURS def to_dict(self, with_groups=False, str_keys: bool = False) -> dict: """as a dict, with or without groups. If str_keys, convert integer dict keys to strings (useful for JSON) """ d = dict(self.__dict__) d["partition_id"] = self.id d.pop("_sa_instance_state", None) d.pop("formsemestre", None) if with_groups: groups = sorted(self.groups, key=attrgetter("numero", "group_name")) # un dict et non plus une liste, pour JSON if str_keys: d["groups"] = { str(group.id): group.to_dict(with_partition=False) for group in groups } else: d["groups"] = { group.id: group.to_dict(with_partition=False) for group in groups } return d def get_etud_group(self, etudid: int) -> "GroupDescr": "Le groupe de l'étudiant dans cette partition, ou None si pas présent" return ( GroupDescr.query.filter_by(partition_id=self.id) .join(group_membership) .filter_by(etudid=etudid) .first() ) def set_etud_group(self, etudid: int, group: "GroupDescr"): """Affect etudid to group_id in given partition. Raises IntegrityError si conflit, or ValueError si ce group_id n'est pas dans cette partition ou que l'étudiant n'est pas inscrit au semestre. """ if not group.id in (g.id for g in self.groups): raise ScoValueError( f"""Le groupe {group.id} n'est pas dans la partition {self.partition_name or "tous"}""" ) if etudid not in (e.id for e in self.formsemestre.etuds): raise ScoValueError( f"etudiant {etudid} non inscrit au formsemestre du groupe {group.id}" ) try: existing_row = ( db.session.query(group_membership) .filter_by(etudid=etudid) .join(GroupDescr) .filter_by(partition_id=self.id) .first() ) if existing_row: existing_row.update({"group_id": group.id}) else: new_row = group_membership.insert().values( etudid=etudid, group_id=group.id ) db.session.execute(new_row) db.session.commit() except IntegrityError: db.session.rollback() raise class GroupDescr(db.Model): """Description d'un groupe d'une partition""" __tablename__ = "group_descr" __table_args__ = (db.UniqueConstraint("partition_id", "group_name"),) id = db.Column(db.Integer, primary_key=True) group_id = db.synonym("id") partition_id = db.Column(db.Integer, db.ForeignKey("partition.id")) # "A", "C2", ... (NULL for 'all'): group_name = db.Column(db.String(GROUPNAME_STR_LEN)) # Numero = ordre de presentation numero = db.Column(db.Integer, nullable=False, default=0) etuds = db.relationship( "Identite", secondary="group_membership", lazy="dynamic", ) def __repr__(self): return ( f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">""" ) def get_nom_with_part(self) -> str: "Nom avec partition: 'TD A'" return f"{self.partition.partition_name or ''} {self.group_name or '-'}" def to_dict(self, with_partition=True) -> dict: """as a dict, with or without partition""" d = dict(self.__dict__) d.pop("_sa_instance_state", None) if with_partition: d["partition"] = self.partition.to_dict(with_groups=False) return d @classmethod def check_name( cls, partition: "Partition", group_name: str, existing=False, default=False ) -> bool: """check if a group named 'group_name' can be created in the given partition. If existing is True, allow a group_name already existing in the partition. If default, name must be empty and default group must not yet exists. """ if not isinstance(group_name, str): return False if not default and not (0 < len(group_name.strip()) < GROUPNAME_STR_LEN): return False if (not existing) and (group_name in [g.group_name for g in partition.groups]): return False return True group_membership = db.Table( "group_membership", db.Column("etudid", db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE")), db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")), db.UniqueConstraint("etudid", "group_id"), ) # class GroupMembership(db.Model): # """Association groupe / étudiant""" # __tablename__ = "group_membership" # __table_args__ = (db.UniqueConstraint("etudid", "group_id"),) # id = db.Column(db.Integer, primary_key=True) # etudid = db.Column(db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE")) # group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id"))