2021-08-07 15:20:30 +02:00
|
|
|
# -*- coding: UTF-8 -*
|
2022-07-20 15:07:31 +02:00
|
|
|
##############################################################################
|
|
|
|
# ScoDoc
|
2023-01-02 13:16:27 +01:00
|
|
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
2022-07-20 15:07:31 +02:00
|
|
|
# See LICENSE
|
|
|
|
##############################################################################
|
2021-08-07 15:20:30 +02:00
|
|
|
|
2022-07-20 15:07:31 +02:00
|
|
|
"""ScoDoc models: Groups & partitions
|
2021-08-07 15:20:30 +02:00
|
|
|
"""
|
2023-04-03 17:46:31 +02:00
|
|
|
from operator import attrgetter
|
2023-07-05 19:15:33 +02:00
|
|
|
from sqlalchemy.exc import IntegrityError
|
2021-08-07 15:20:30 +02:00
|
|
|
|
|
|
|
from app import db
|
|
|
|
from app.models import SHORT_STR_LEN
|
2021-08-14 18:54:32 +02:00
|
|
|
from app.models import GROUPNAME_STR_LEN
|
2022-07-20 15:07:31 +02:00
|
|
|
from app.scodoc import sco_utils as scu
|
2023-07-05 19:15:33 +02:00
|
|
|
from app.scodoc.sco_exceptions import ScoValueError
|
2021-08-07 15:20:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
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"),
|
2021-08-08 17:38:46 +02:00
|
|
|
index=True,
|
2021-08-07 15:20:30 +02:00
|
|
|
)
|
|
|
|
# "TD", "TP", ... (NULL for 'all')
|
|
|
|
partition_name = db.Column(db.String(SHORT_STR_LEN))
|
2022-05-26 23:45:57 +02:00
|
|
|
# Numero = ordre de presentation)
|
2023-04-03 17:46:31 +02:00
|
|
|
numero = db.Column(db.Integer, nullable=False, default=0)
|
2022-05-18 20:43:01 +02:00
|
|
|
# Calculer le rang ?
|
2021-08-10 00:23:30 +02:00
|
|
|
bul_show_rank = db.Column(
|
|
|
|
db.Boolean(), nullable=False, default=False, server_default="false"
|
|
|
|
)
|
2022-05-18 20:43:01 +02:00
|
|
|
# Montrer quand on indique les groupes de l'étudiant ?
|
2021-08-10 00:23:30 +02:00
|
|
|
show_in_lists = db.Column(
|
|
|
|
db.Boolean(), nullable=False, default=True, server_default="true"
|
|
|
|
)
|
2022-08-03 21:42:53 +02:00
|
|
|
# Editable (créer/renommer groupes) ? (faux pour les groupes de parcours)
|
2022-05-26 23:45:57 +02:00
|
|
|
groups_editable = db.Column(
|
|
|
|
db.Boolean(), nullable=False, default=True, server_default="true"
|
|
|
|
)
|
2022-02-17 18:13:04 +01:00
|
|
|
groups = db.relationship(
|
|
|
|
"GroupDescr",
|
|
|
|
backref=db.backref("partition", lazy=True),
|
|
|
|
lazy="dynamic",
|
2022-07-20 15:07:31 +02:00
|
|
|
cascade="all, delete-orphan",
|
2022-08-31 19:14:21 +02:00
|
|
|
order_by="GroupDescr.numero",
|
2022-02-17 18:13:04 +01:00
|
|
|
)
|
2021-08-07 15:20:30 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2022-02-17 18:13:04 +01:00
|
|
|
def __repr__(self):
|
|
|
|
return f"""<{self.__class__.__name__} {self.id} "{self.partition_name or '(default)'}">"""
|
|
|
|
|
2022-07-20 15:07:31 +02:00
|
|
|
@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
|
2023-02-22 02:13:06 +01:00
|
|
|
if not (0 < len(partition_name.strip()) < SHORT_STR_LEN):
|
2022-07-20 15:07:31 +02:00
|
|
|
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:
|
2022-08-03 21:42:53 +02:00
|
|
|
"Vrai s'il s'agit de la partition de parcours"
|
2022-07-20 15:07:31 +02:00
|
|
|
return self.partition_name == scu.PARTITION_PARCOURS
|
|
|
|
|
2023-04-12 13:21:13 +02:00
|
|
|
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)
|
|
|
|
"""
|
2022-07-19 22:17:10 +02:00
|
|
|
d = dict(self.__dict__)
|
2023-01-22 22:15:56 +01:00
|
|
|
d["partition_id"] = self.id
|
2022-07-19 22:17:10 +02:00
|
|
|
d.pop("_sa_instance_state", None)
|
2022-07-20 22:33:41 +02:00
|
|
|
d.pop("formsemestre", None)
|
2022-07-19 22:17:10 +02:00
|
|
|
|
2022-05-18 20:43:01 +02:00
|
|
|
if with_groups:
|
2023-04-03 17:46:31 +02:00
|
|
|
groups = sorted(self.groups, key=attrgetter("numero", "group_name"))
|
2022-07-23 09:07:53 +02:00
|
|
|
# un dict et non plus une liste, pour JSON
|
2023-04-12 13:21:13 +02:00
|
|
|
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
|
|
|
|
}
|
2022-05-18 20:43:01 +02:00
|
|
|
return d
|
|
|
|
|
2023-03-27 18:30:36 +02:00
|
|
|
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()
|
|
|
|
)
|
|
|
|
|
2023-07-08 13:57:44 +02:00
|
|
|
def set_etud_group(self, etudid: int, group: "GroupDescr") -> bool:
|
2023-07-05 19:15:33 +02:00
|
|
|
"""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.
|
2023-07-08 13:57:44 +02:00
|
|
|
Return True si changement, False s'il était déjà dans ce groupe.
|
2023-07-05 19:15:33 +02:00
|
|
|
"""
|
|
|
|
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()
|
|
|
|
)
|
2023-07-08 13:57:44 +02:00
|
|
|
existing_group_id = existing_row[1]
|
2023-07-05 19:15:33 +02:00
|
|
|
if existing_row:
|
2023-07-08 13:57:44 +02:00
|
|
|
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)
|
2023-07-05 19:15:33 +02:00
|
|
|
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
|
2023-07-08 13:57:44 +02:00
|
|
|
return True
|
2023-07-05 19:15:33 +02:00
|
|
|
|
2021-08-07 15:20:30 +02:00
|
|
|
|
|
|
|
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'):
|
2021-08-14 18:54:32 +02:00
|
|
|
group_name = db.Column(db.String(GROUPNAME_STR_LEN))
|
2022-08-31 19:14:21 +02:00
|
|
|
# Numero = ordre de presentation
|
2023-04-03 17:46:31 +02:00
|
|
|
numero = db.Column(db.Integer, nullable=False, default=0)
|
2021-08-07 15:20:30 +02:00
|
|
|
|
2022-03-09 18:03:18 +01:00
|
|
|
etuds = db.relationship(
|
|
|
|
"Identite",
|
|
|
|
secondary="group_membership",
|
|
|
|
lazy="dynamic",
|
|
|
|
)
|
|
|
|
|
2022-02-17 18:13:04 +01:00
|
|
|
def __repr__(self):
|
|
|
|
return (
|
|
|
|
f"""<{self.__class__.__name__} {self.id} "{self.group_name or '(tous)'}">"""
|
|
|
|
)
|
|
|
|
|
2022-04-08 16:36:56 +02:00
|
|
|
def get_nom_with_part(self) -> str:
|
|
|
|
"Nom avec partition: 'TD A'"
|
|
|
|
return f"{self.partition.partition_name or ''} {self.group_name or '-'}"
|
|
|
|
|
2022-05-18 20:43:01 +02:00
|
|
|
def to_dict(self, with_partition=True) -> dict:
|
|
|
|
"""as a dict, with or without partition"""
|
2022-08-02 09:48:11 +02:00
|
|
|
d = dict(self.__dict__)
|
|
|
|
d.pop("_sa_instance_state", None)
|
2022-05-18 20:43:01 +02:00
|
|
|
if with_partition:
|
2022-07-25 06:53:35 +02:00
|
|
|
d["partition"] = self.partition.to_dict(with_groups=False)
|
2022-05-18 20:43:01 +02:00
|
|
|
return d
|
|
|
|
|
2022-07-20 09:50:02 +02:00
|
|
|
@classmethod
|
|
|
|
def check_name(
|
2022-09-03 10:07:34 +02:00
|
|
|
cls, partition: "Partition", group_name: str, existing=False, default=False
|
2022-07-20 09:50:02 +02:00
|
|
|
) -> 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.
|
2022-09-03 10:07:34 +02:00
|
|
|
If default, name must be empty and default group must not yet exists.
|
2022-07-20 09:50:02 +02:00
|
|
|
"""
|
|
|
|
if not isinstance(group_name, str):
|
|
|
|
return False
|
2023-02-22 02:13:06 +01:00
|
|
|
if not default and not (0 < len(group_name.strip()) < GROUPNAME_STR_LEN):
|
2022-07-20 09:50:02 +02:00
|
|
|
return False
|
|
|
|
if (not existing) and (group_name in [g.group_name for g in partition.groups]):
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2021-08-07 15:20:30 +02:00
|
|
|
|
|
|
|
group_membership = db.Table(
|
|
|
|
"group_membership",
|
2022-05-26 03:55:03 +02:00
|
|
|
db.Column("etudid", db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE")),
|
2021-08-07 15:20:30 +02:00
|
|
|
db.Column("group_id", db.Integer, db.ForeignKey("group_descr.id")),
|
|
|
|
db.UniqueConstraint("etudid", "group_id"),
|
|
|
|
)
|
2022-05-18 20:43:01 +02:00
|
|
|
# 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)
|
2022-05-26 03:55:03 +02:00
|
|
|
# etudid = db.Column(db.Integer, db.ForeignKey("identite.id", ondelete="CASCADE"))
|
2022-05-18 20:43:01 +02:00
|
|
|
# group_id = db.Column(db.Integer, db.ForeignKey("group_descr.id"))
|