ScoDoc/app/models/groups.py

238 lines
8.7 KiB
Python

# -*- 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") -> bool:
"""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.
Return True si changement, False s'il était déjà dans ce groupe.
"""
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()
)
existing_group_id = existing_row[1]
if existing_row:
if group.id == existing_group_id:
return False
update_row = (
group_membership.update()
.where(
group_membership.c.etudid == etudid,
group_membership.c.group_id == existing_group_id,
)
.values(group_id=group.id)
)
db.session.execute(update_row)
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
return True
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"))