forked from ScoDoc/ScoDoc
PE: reformattage code, small bug fix.
This commit is contained in:
parent
86c12dee08
commit
f4c1d00046
@ -1,3 +1,11 @@
|
||||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""Affichages, debug
|
||||
"""
|
||||
|
||||
from app import log
|
||||
|
||||
PE_DEBUG = 0
|
||||
@ -15,5 +23,3 @@ else:
|
||||
SANS_NOTE = "-"
|
||||
NOM_STAT_GROUPE = "statistiques du groupe"
|
||||
NOM_STAT_PROMO = "statistiques de la promo"
|
||||
|
||||
|
||||
|
@ -52,7 +52,6 @@ from app.scodoc import sco_formsemestre
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
||||
|
||||
|
||||
# Generated LaTeX files are encoded as:
|
||||
PE_LATEX_ENCODING = "utf-8"
|
||||
|
||||
@ -98,7 +97,8 @@ def calcul_age(born: datetime.date) -> int:
|
||||
return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
|
||||
|
||||
|
||||
def remove_accents(input_unicode_str):
|
||||
# Nota: scu.suppress_accents fait la même chose mais renvoie un str et non un bytes
|
||||
def remove_accents(input_unicode_str: str) -> bytes:
|
||||
"""Supprime les accents d'une chaine unicode"""
|
||||
nfkd_form = unicodedata.normalize("NFKD", input_unicode_str)
|
||||
only_ascii = nfkd_form.encode("ASCII", "ignore")
|
||||
@ -133,15 +133,15 @@ def escape_for_latex(s):
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
def list_directory_filenames(path):
|
||||
"""List of regular filenames in a directory (recursive)
|
||||
def list_directory_filenames(path: str) -> list[str]:
|
||||
"""List of regular filenames (paths) in a directory (recursive)
|
||||
Excludes files and directories begining with .
|
||||
"""
|
||||
R = []
|
||||
paths = []
|
||||
for root, dirs, files in os.walk(path, topdown=True):
|
||||
dirs[:] = [d for d in dirs if d[0] != "."]
|
||||
R += [os.path.join(root, fn) for fn in files if fn[0] != "."]
|
||||
return R
|
||||
paths += [os.path.join(root, fn) for fn in files if fn[0] != "."]
|
||||
return paths
|
||||
|
||||
|
||||
def add_local_file_to_zip(zipfile, ziproot, pathname, path_in_zip):
|
||||
@ -195,13 +195,15 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
|
||||
def get_annee_diplome_semestre(
|
||||
sem_base: FormSemestre | dict, nbre_sem_formation: int = 6
|
||||
) -> int:
|
||||
"""Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT à 6 semestres)
|
||||
et connaissant le numéro du semestre, ses dates de début et de fin du semestre, prédit l'année à laquelle
|
||||
sera remis le diplôme BUT des étudiants qui y sont scolarisés
|
||||
(en supposant qu'il n'y ait pas de redoublement à venir).
|
||||
"""Pour un semestre ``sem_base`` donné (supposé être un semestre d'une formation BUT
|
||||
à 6 semestres) et connaissant le numéro du semestre, ses dates de début et de fin du
|
||||
semestre, prédit l'année à laquelle sera remis le diplôme BUT des étudiants qui y
|
||||
sont scolarisés (en supposant qu'il n'y ait pas de redoublement à venir).
|
||||
|
||||
**Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4, S6 pour des semestres décalés)
|
||||
s'étalent sur deux années civiles ; contrairement au semestre de seconde partie d'année universitaire.
|
||||
**Remarque sur le calcul** : Les semestres de 1ère partie d'année (S1, S3, S5 ou S4,
|
||||
S6 pour des semestres décalés)
|
||||
s'étalent sur deux années civiles ; contrairement au semestre de seconde partie
|
||||
d'année universitaire.
|
||||
|
||||
Par exemple :
|
||||
|
||||
@ -235,21 +237,22 @@ def get_annee_diplome_semestre(
|
||||
if (
|
||||
1 <= sem_id <= nbre_sem_formation
|
||||
): # Si le semestre est un semestre BUT => problème si formation BUT en 1 an ??
|
||||
nbreSemRestant = (
|
||||
nb_sem_restants = (
|
||||
nbre_sem_formation - sem_id
|
||||
) # nombre de semestres restant avant diplome
|
||||
nbreAnRestant = nbreSemRestant // 2 # nombre d'annees restant avant diplome
|
||||
# Flag permettant d'activer ou désactiver un increment à prendre en compte en cas de semestre décalé
|
||||
nb_annees_restantes = (
|
||||
nb_sem_restants // 2
|
||||
) # nombre d'annees restant avant diplome
|
||||
# Flag permettant d'activer ou désactiver un increment
|
||||
# à prendre en compte en cas de semestre décalé
|
||||
# avec 1 - delta = 0 si semestre de 1ere partie d'année / 1 sinon
|
||||
delta = annee_fin - annee_debut
|
||||
decalage = nbreSemRestant % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1
|
||||
decalage = nb_sem_restants % 2 # 0 si S4, 1 si S3, 0 si S2, 1 si S1
|
||||
increment = decalage * (1 - delta)
|
||||
return annee_fin + nbreAnRestant + increment
|
||||
return annee_fin + nb_annees_restantes + increment
|
||||
|
||||
|
||||
def get_cosemestres_diplomants(
|
||||
annee_diplome: int
|
||||
) -> dict[int, FormSemestre]:
|
||||
def get_cosemestres_diplomants(annee_diplome: int) -> dict[int, FormSemestre]:
|
||||
"""Ensemble des cosemestres donnant lieu à diplomation à l'``annee_diplome``.
|
||||
|
||||
**Définition** : Un co-semestre est un semestre :
|
||||
@ -264,15 +267,15 @@ def get_cosemestres_diplomants(
|
||||
Returns:
|
||||
Un dictionnaire {fid: FormSemestre(fid)} contenant les cosemestres
|
||||
"""
|
||||
tousLesSems = (
|
||||
tous_les_sems = (
|
||||
sco_formsemestre.do_formsemestre_list()
|
||||
) # tous les semestres memorisés dans scodoc
|
||||
|
||||
cosemestres_fids = {
|
||||
sem["id"]
|
||||
for sem in tousLesSems
|
||||
if get_annee_diplome_semestre(sem) == annee_diplome
|
||||
}
|
||||
sem["id"]
|
||||
for sem in tous_les_sems
|
||||
if get_annee_diplome_semestre(sem) == annee_diplome
|
||||
}
|
||||
|
||||
cosemestres = {}
|
||||
for fid in cosemestres_fids:
|
||||
@ -281,5 +284,3 @@ def get_cosemestres_diplomants(
|
||||
cosemestres[fid] = cosem
|
||||
|
||||
return cosemestres
|
||||
|
||||
|
||||
|
@ -37,13 +37,13 @@ Created on 17/01/2024
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
import app.pe.pe_rcs
|
||||
from app.models import FormSemestre, Identite, Formation
|
||||
from app.pe import pe_comp, pe_affichage
|
||||
from app.scodoc import codes_cursus
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.comp.res_sem import load_formsemestre_results
|
||||
|
||||
|
||||
class EtudiantsJuryPE:
|
||||
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
|
||||
|
||||
@ -123,20 +123,15 @@ class EtudiantsJuryPE:
|
||||
|
||||
# Les étudiants à prendre dans le diplôme, étudiants ayant abandonnés non compris
|
||||
self.etudiants_diplomes = self.get_etudiants_diplomes()
|
||||
"""Les identités des étudiants diplômés"""
|
||||
|
||||
self.diplomes_ids = set(self.etudiants_diplomes.keys())
|
||||
"""Les identifiants des étudiants diplômés"""
|
||||
|
||||
self.etudiants_ids = set(self.identites.keys())
|
||||
"""Les identifiants des étudiants (diplômés, redoublants ou ayant abandonnés) à traiter"""
|
||||
|
||||
# Les abandons (pour debug)
|
||||
self.abandons = self.get_etudiants_redoublants_ou_reorientes()
|
||||
"""Les identités des étudiants ayant redoublés ou ayant abandonnés"""
|
||||
# Les identités des étudiants ayant redoublés ou ayant abandonnés
|
||||
|
||||
self.abandons_ids = set(self.abandons)
|
||||
"""Les identifiants des étudiants ayant redoublés ou ayant abandonnés"""
|
||||
# Les identifiants des étudiants ayant redoublés ou ayant abandonnés
|
||||
|
||||
# Synthèse
|
||||
pe_affichage.pe_print(
|
||||
@ -279,7 +274,7 @@ class EtudiantsJuryPE:
|
||||
semestres_significatifs = self.get_semestres_significatifs(etudid)
|
||||
|
||||
# Tri des semestres par numéro de semestre
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT+1):
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
|
||||
# les semestres de n°i de l'étudiant:
|
||||
semestres_i = {
|
||||
fid: sem_sig
|
||||
@ -288,18 +283,20 @@ class EtudiantsJuryPE:
|
||||
}
|
||||
self.cursus[etudid][f"S{i}"] = semestres_i
|
||||
|
||||
|
||||
|
||||
def get_formsemestres_terminaux_aggregat(self, aggregat: str):
|
||||
def get_formsemestres_terminaux_aggregat(
|
||||
self, aggregat: str
|
||||
) -> dict[int, FormSemestre]:
|
||||
"""Pour un aggrégat donné, ensemble des formsemestres terminaux possibles pour l'aggrégat
|
||||
(pour l'aggrégat '3S' incluant S1+S2+S3, a pour semestre terminal S3).
|
||||
Ces formsemestres traduisent :
|
||||
|
||||
* les différents parcours des étudiants liés par exemple au choix de modalité (par ex: S1 FI + S2 FI + S3 FI
|
||||
ou S1 FI + S2 FI + S3 UFA), en renvoyant les formsemestre_id du S3 FI et du S3 UFA.
|
||||
* les éventuelles situations de redoublement (par ex pour 1 étudiant ayant redoublé sa 2ème année :
|
||||
S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en renvoyant les formsemestre_id du
|
||||
S3 (1ère session) et du S3 (2ème session)
|
||||
* les différents parcours des étudiants liés par exemple au choix de modalité
|
||||
(par ex: S1 FI + S2 FI + S3 FI ou S1 FI + S2 FI + S3 UFA), en renvoyant les
|
||||
formsemestre_id du S3 FI et du S3 UFA.
|
||||
* les éventuelles situations de redoublement (par ex pour 1 étudiant ayant
|
||||
redoublé sa 2ème année :
|
||||
S1 + S2 + S3 (1ère session) et S1 + S2 + S3 + S4 + S3 (2ème session), en
|
||||
renvoyant les formsemestre_id du S3 (1ère session) et du S3 (2ème session)
|
||||
|
||||
Args:
|
||||
aggregat: L'aggrégat
|
||||
@ -316,7 +313,6 @@ class EtudiantsJuryPE:
|
||||
formsemestres_terminaux[fid] = trajectoire.formsemestre_final
|
||||
return formsemestres_terminaux
|
||||
|
||||
|
||||
def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int:
|
||||
"""Partant d'un ensemble d'étudiants,
|
||||
nombre de semestres (étapes) maximum suivis par les étudiants du jury.
|
||||
@ -407,7 +403,7 @@ def get_etudiants_dans_semestres(semestres: dict[int, FormSemestre]) -> set:
|
||||
return etudiants_ids
|
||||
|
||||
|
||||
def get_annee_diplome(etud: Identite) -> int:
|
||||
def get_annee_diplome(etud: Identite) -> int | None:
|
||||
"""L'année de diplôme prévue d'un étudiant en fonction de ses semestres
|
||||
d'inscription (pour un BUT).
|
||||
|
||||
@ -415,13 +411,14 @@ def get_annee_diplome(etud: Identite) -> int:
|
||||
identite: L'identité d'un étudiant
|
||||
|
||||
Returns:
|
||||
L'année prévue de sa diplômation
|
||||
L'année prévue de sa diplômation, ou None si aucun semestre
|
||||
"""
|
||||
formsemestres_apc = get_semestres_apc(etud)
|
||||
|
||||
if formsemestres_apc:
|
||||
dates_possibles_diplome = []
|
||||
"""Années de diplômation prédites en fonction des semestres (d'une formation APC) d'un étudiant"""
|
||||
# Années de diplômation prédites en fonction des semestres
|
||||
# (d'une formation APC) d'un étudiant
|
||||
for sem_base in formsemestres_apc:
|
||||
annee = pe_comp.get_annee_diplome_semestre(sem_base)
|
||||
if annee:
|
||||
@ -452,23 +449,26 @@ def get_semestres_apc(identite: Identite) -> list:
|
||||
def arret_de_formation(etud: Identite, cosemestres: list[FormSemestre]) -> bool:
|
||||
"""Détermine si un étudiant a arrêté sa formation. Il peut s'agir :
|
||||
|
||||
* d'une réorientation à l'initiative du jury de semestre ou d'une démission (on pourrait
|
||||
utiliser les code NAR pour réorienté & DEM pour démissionnaire des résultats du jury renseigné dans la BDD,
|
||||
mais pas nécessaire ici)
|
||||
* d'une réorientation à l'initiative du jury de semestre ou d'une démission
|
||||
(on pourrait utiliser les code NAR pour réorienté & DEM pour démissionnaire
|
||||
des résultats du jury renseigné dans la BDD, mais pas nécessaire ici)
|
||||
|
||||
* d'un arrêt volontaire : l'étudiant disparait des listes d'inscrits (sans pour autant avoir été indiqué NAR ou DEM).
|
||||
* d'un arrêt volontaire : l'étudiant disparait des listes d'inscrits (sans pour
|
||||
autant avoir été indiqué NAR ou DEM).
|
||||
|
||||
Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas dans l'un des "derniers" cosemestres
|
||||
(semestres conduisant à la même année de diplômation) connu dans Scodoc.
|
||||
Dans les cas, on considérera que l'étudiant a arrêté sa formation s'il n'est pas
|
||||
dans l'un des "derniers" cosemestres (semestres conduisant à la même année de diplômation)
|
||||
connu dans Scodoc.
|
||||
|
||||
Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc), l'étudiant doit appartenir à une
|
||||
instance des S5 qui conduisent à la diplomation dans l'année visée. S'il n'est que dans un S4, il a sans doute
|
||||
arrêté. A moins qu'il ne soit parti à l'étranger et là, pas de notes.
|
||||
Par ex: au moment du jury PE en fin de S5 (pas de S6 renseigné dans Scodoc),
|
||||
l'étudiant doit appartenir à une instance des S5 qui conduisent à la diplomation dans
|
||||
l'année visée. S'il n'est que dans un S4, il a sans doute arrêté. A moins qu'il ne soit
|
||||
parti à l'étranger et là, pas de notes.
|
||||
TODO:: Cas de l'étranger, à coder/tester
|
||||
|
||||
**Attention** : Cela suppose que toutes les instances d'un semestre donné (par ex: toutes les instances de S6
|
||||
accueillant un étudiant soient créées ; sinon les étudiants non inscrits dans un S6 seront considérés comme
|
||||
ayant abandonnés)
|
||||
**Attention** : Cela suppose que toutes les instances d'un semestre donné
|
||||
(par ex: toutes les instances de S6 accueillant un étudiant soient créées ; sinon les
|
||||
étudiants non inscrits dans un S6 seront considérés comme ayant abandonnés)
|
||||
TODO:: Peut-être à mettre en regard avec les propositions d'inscriptions d'étudiants dans un nouveau semestre
|
||||
|
||||
Pour chaque étudiant, recherche son dernier semestre en date (validé ou non) et
|
||||
|
@ -1,16 +1,59 @@
|
||||
from app.comp import moy_sem
|
||||
##############################################################################
|
||||
#
|
||||
# Gestion scolarite IUT
|
||||
#
|
||||
# Copyright (c) 1999 - 2024 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on Thu Sep 8 09:36:33 2016
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
||||
from app.pe.pe_etudiant import EtudiantsJuryPE
|
||||
from app.pe.pe_rcs import RCS, RCSsJuryPE
|
||||
from app.pe.pe_rcstag import RCSTag
|
||||
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
|
||||
class RCSInterclasseTag(TableTag):
|
||||
# -------------------------------------------------------------------------------------------------------------------
|
||||
"""
|
||||
Interclasse l'ensemble des étudiants diplômés à une année
|
||||
donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S')
|
||||
en reportant :
|
||||
|
||||
* les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre
|
||||
le numéro de semestre de fin de l'aggrégat (indépendamment de son
|
||||
formsemestre)
|
||||
* calculant le classement sur les étudiants diplômes
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nom_rcs: str,
|
||||
@ -18,15 +61,6 @@ class RCSInterclasseTag(TableTag):
|
||||
rcss_jury_pe: RCSsJuryPE,
|
||||
rcss_tags: dict[tuple, RCSTag],
|
||||
):
|
||||
"""
|
||||
Interclasse l'ensemble des étudiants diplômés à une année
|
||||
donnée (celle du jury), pour un RCS donné (par ex: 'S2', '3S')
|
||||
en reportant :
|
||||
|
||||
* les moyennes obtenues sur la trajectoire qu'il ont suivi pour atteindre le numéro de semestre de fin de l'aggrégat (indépendamment de son
|
||||
formsemestres)
|
||||
* calculant le classement sur les étudiants diplômes
|
||||
"""
|
||||
TableTag.__init__(self)
|
||||
|
||||
self.nom_rcs = nom_rcs
|
||||
@ -43,7 +77,8 @@ class RCSInterclasseTag(TableTag):
|
||||
for etudid in self.diplomes_ids
|
||||
}
|
||||
|
||||
# Les trajectoires (et leur version tagguées), en ne gardant que celles associées à l'aggrégat
|
||||
# Les trajectoires (et leur version tagguées), en ne gardant que
|
||||
# celles associées à l'aggrégat
|
||||
self.rcss: dict[int, RCS] = {}
|
||||
"""Ensemble des trajectoires associées à l'aggrégat"""
|
||||
for trajectoire_id in rcss_jury_pe.rcss:
|
||||
@ -54,9 +89,7 @@ class RCSInterclasseTag(TableTag):
|
||||
self.trajectoires_taggues: dict[int, RCS] = {}
|
||||
"""Ensemble des trajectoires tagguées associées à l'aggrégat"""
|
||||
for trajectoire_id in self.rcss:
|
||||
self.trajectoires_taggues[trajectoire_id] = rcss_tags[
|
||||
trajectoire_id
|
||||
]
|
||||
self.trajectoires_taggues[trajectoire_id] = rcss_tags[trajectoire_id]
|
||||
|
||||
# Les trajectoires suivies par les étudiants du jury, en ne gardant que
|
||||
# celles associées aux diplomés
|
||||
|
@ -47,31 +47,29 @@ import os
|
||||
from zipfile import ZipFile
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app.pe.pe_affichage import NOM_STAT_PROMO, SANS_NOTE, NOM_STAT_GROUPE
|
||||
|
||||
from app.pe.pe_etudiant import *
|
||||
from app.pe.pe_rcs import *
|
||||
import app.pe.pe_comp as pe_comp
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
from app.pe.pe_etudiant import * # TODO A éviter -> pe_etudiant.
|
||||
from app.pe.pe_rcs import * # TODO A éviter
|
||||
from app.pe.pe_rcstag import RCSTag
|
||||
from app.pe.pe_semtag import SemestreTag
|
||||
from app.pe.pe_interclasstag import RCSInterclasseTag
|
||||
from app.pe.pe_rcstag import RCSTag
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class JuryPE(object):
|
||||
def __init__(self, diplome):
|
||||
"""
|
||||
Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base
|
||||
d'une année de diplôme. De ce semestre est déduit :
|
||||
1. l'année d'obtention du DUT,
|
||||
2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.
|
||||
"""
|
||||
Classe mémorisant toutes les informations nécessaires pour établir un jury de PE, sur la base
|
||||
d'une année de diplôme. De ce semestre est déduit :
|
||||
1. l'année d'obtention du DUT,
|
||||
2. tous les étudiants susceptibles à ce stade (au regard de leur parcours) d'être diplomés.
|
||||
|
||||
Args:
|
||||
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
|
||||
"""
|
||||
Args:
|
||||
diplome : l'année d'obtention du diplome BUT et du jury de PE (généralement février XXXX)
|
||||
"""
|
||||
|
||||
def __init__(self, diplome):
|
||||
self.diplome = diplome
|
||||
"L'année du diplome"
|
||||
|
||||
@ -161,9 +159,7 @@ class JuryPE(object):
|
||||
self.rcss.cree_rcss(self.etudiants)
|
||||
|
||||
# Génère les moyennes par tags des trajectoires
|
||||
pe_affichage.pe_print(
|
||||
"*** Calcule les moyennes par tag des RCS possibles"
|
||||
)
|
||||
pe_affichage.pe_print("*** Calcule les moyennes par tag des RCS possibles")
|
||||
self.rcss_tags = compute_trajectoires_tag(
|
||||
self.rcss, self.etudiants, self.sems_tags
|
||||
)
|
||||
@ -381,14 +377,13 @@ class JuryPE(object):
|
||||
champ = (descr, "", "note")
|
||||
notes_traj = moy_traj.get_notes()
|
||||
donnees.loc[etudids_communs, champ] = notes_traj.loc[
|
||||
etudids_communs]
|
||||
etudids_communs
|
||||
]
|
||||
|
||||
# Les rangs
|
||||
champ = (descr, NOM_STAT_GROUPE, "class.")
|
||||
rangs = moy_traj.get_rangs_inscrits()
|
||||
donnees.loc[etudids_communs, champ] = rangs.loc[
|
||||
etudids_communs
|
||||
]
|
||||
donnees.loc[etudids_communs, champ] = rangs.loc[etudids_communs]
|
||||
|
||||
# Les mins
|
||||
champ = (descr, NOM_STAT_GROUPE, "min")
|
||||
@ -487,9 +482,7 @@ class JuryPE(object):
|
||||
# La trajectoire de l'étudiant sur l'aggrégat
|
||||
trajectoire = self.rcss.suivi[etudid][aggregat]
|
||||
if trajectoire:
|
||||
trajectoire_tagguee = self.rcss_tags[
|
||||
trajectoire.rcs_id
|
||||
]
|
||||
trajectoire_tagguee = self.rcss_tags[trajectoire.rcs_id]
|
||||
if tag in trajectoire_tagguee.moyennes_tags:
|
||||
# L'interclassement
|
||||
interclass = self.interclassements_taggues[aggregat]
|
||||
@ -512,6 +505,7 @@ class JuryPE(object):
|
||||
df.sort_values(by=[("", "", "tag")], inplace=True)
|
||||
return df
|
||||
|
||||
|
||||
def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict:
|
||||
"""Ayant connaissance des étudiants dont il faut calculer les moyennes pour
|
||||
le jury PE (attribut `self.etudiant_ids) et de leur cursus (semestres
|
||||
@ -534,6 +528,7 @@ def get_formsemestres_etudiants(etudiants: EtudiantsJuryPE) -> dict:
|
||||
semestres = semestres | etudiants.cursus[etudid][cle]
|
||||
return semestres
|
||||
|
||||
|
||||
def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
|
||||
"""Créé les semestres taggués, de type 'S1', 'S2', ..., pour un groupe d'étudiants donnés.
|
||||
Chaque semestre taggué est rattaché à l'un des FormSemestre faisant partie du cursus scolaire
|
||||
@ -548,7 +543,7 @@ def compute_semestres_tag(etudiants: EtudiantsJuryPE) -> dict:
|
||||
Un dictionnaire {fid: SemestreTag(fid)}
|
||||
"""
|
||||
|
||||
"""Création des semestres taggués, de type 'S1', 'S2', ..."""
|
||||
# Création des semestres taggués, de type 'S1', 'S2', ...
|
||||
pe_affichage.pe_print("*** Création des semestres taggués")
|
||||
|
||||
formsemestres = get_formsemestres_etudiants(etudiants)
|
||||
|
118
app/pe/pe_rcs.py
118
app/pe/pe_rcs.py
@ -1,5 +1,15 @@
|
||||
##############################################################################
|
||||
# Module "Avis de poursuite d'étude"
|
||||
# conçu et développé par Cléo Baras (IUT de Grenoble)
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Created on 01-2024
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
|
||||
import app.pe.pe_comp as pe_comp
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
|
||||
from app.models import FormSemestre
|
||||
from app.pe.pe_etudiant import EtudiantsJuryPE, get_dernier_semestre_en_date
|
||||
@ -59,39 +69,41 @@ TYPES_RCS = {
|
||||
"descr": "Moyenne globale (S1+S2+S3+S4+S5+S6)",
|
||||
},
|
||||
}
|
||||
"""Dictionnaire détaillant les différents regroupements cohérents
|
||||
de semestres (RCS), en leur attribuant un nom et en détaillant
|
||||
le nom des semestres qu'ils regroupement et l'affichage qui en sera fait
|
||||
dans les tableurs de synthèse"""
|
||||
"""Dictionnaire détaillant les différents regroupements cohérents
|
||||
de semestres (RCS), en leur attribuant un nom et en détaillant
|
||||
le nom des semestres qu'ils regroupent et l'affichage qui en sera fait
|
||||
dans les tableurs de synthèse.
|
||||
"""
|
||||
|
||||
TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS.keys() if not cle.startswith("S")]
|
||||
TOUS_LES_RCS_AVEC_PLUSIEURS_SEM = [cle for cle in TYPES_RCS if not cle.startswith("S")]
|
||||
TOUS_LES_RCS = list(TYPES_RCS.keys())
|
||||
TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS.keys() if cle.startswith("S")]
|
||||
TOUS_LES_SEMESTRES = [cle for cle in TYPES_RCS if cle.startswith("S")]
|
||||
|
||||
|
||||
class RCS:
|
||||
"""Modélise un ensemble de semestres d'étudiants
|
||||
associé à un type de regroupement cohérent de semestres
|
||||
donné (par ex: 'S2', '3S', '2A').
|
||||
|
||||
Si le RCS est un semestre de type Si, stocke le (ou les)
|
||||
formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si
|
||||
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
|
||||
|
||||
Pour le RCS de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
|
||||
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
|
||||
terminal de la trajectoire (par ex: ici un S3).
|
||||
|
||||
Ces semestres peuvent être :
|
||||
|
||||
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
|
||||
* des S1+S2+(année de césure)+S3 si césure, ...
|
||||
|
||||
Args:
|
||||
nom_rcs: Un nom du RCS (par ex: '5S')
|
||||
semestre_final: Le semestre final du RCS
|
||||
"""
|
||||
|
||||
def __init__(self, nom_rcs: str, semestre_final: FormSemestre):
|
||||
"""Modélise un ensemble de semestres d'étudiants
|
||||
associé à un type de regroupement cohérent de semestres
|
||||
donné (par ex: 'S2', '3S', '2A').
|
||||
|
||||
Si le RCS est un semestre de type Si, stocke le (ou les)
|
||||
formsemestres de numéro i qu'ont suivi l'étudiant pour atteindre le Si
|
||||
(en général 1 si personnes n'a redoublé, mais 2 s'il y a des redoublants)
|
||||
|
||||
Pour le RCS de type iS ou iA (par ex, 3A=S1+S2+S3), elle identifie
|
||||
les semestres que les étudiants ont suivis pour les amener jusqu'au semestre
|
||||
terminal de la trajectoire (par ex: ici un S3).
|
||||
|
||||
Ces semestres peuvent être :
|
||||
|
||||
* des S1+S2+S1+S2+S3 si redoublement de la 1ère année
|
||||
* des S1+S2+(année de césure)+S3 si césure, ...
|
||||
|
||||
Args:
|
||||
nom_rcs: Un nom du RCS (par ex: '5S')
|
||||
semestre_final: Le semestre final du RCS
|
||||
"""
|
||||
self.nom = nom_rcs
|
||||
"""Nom du RCS"""
|
||||
|
||||
@ -121,21 +133,22 @@ class RCS:
|
||||
semestre = self.semestres_aggreges[fid]
|
||||
noms.append(f"S{semestre.semestre_id}({fid})")
|
||||
noms = sorted(noms)
|
||||
repr = f"{self.nom} ({self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"
|
||||
title = f"""{self.nom} ({
|
||||
self.formsemestre_final.formsemestre_id}) {self.formsemestre_final.date_fin.year}"""
|
||||
if verbose and noms:
|
||||
repr += " - " + "+".join(noms)
|
||||
return repr
|
||||
title += " - " + "+".join(noms)
|
||||
return title
|
||||
|
||||
|
||||
class RCSsJuryPE:
|
||||
"""Classe centralisant toutes les regroupements cohérents de
|
||||
semestres (RCS) des étudiants à prendre en compte dans un jury PE
|
||||
|
||||
Args:
|
||||
annee_diplome: L'année de diplomation
|
||||
"""
|
||||
|
||||
def __init__(self, annee_diplome: int):
|
||||
"""Classe centralisant toutes les regroupements cohérents de
|
||||
semestres (RCS) des étudiants à prendre en compte dans un jury PE
|
||||
|
||||
Args:
|
||||
annee_diplome: L'année de diplomation
|
||||
"""
|
||||
|
||||
self.annee_diplome = annee_diplome
|
||||
"""Année de diplômation"""
|
||||
|
||||
@ -155,7 +168,8 @@ class RCSsJuryPE:
|
||||
"""
|
||||
|
||||
for nom_rcs in pe_comp.TOUS_LES_SEMESTRES + TOUS_LES_RCS_AVEC_PLUSIEURS_SEM:
|
||||
"""L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre terminal (par ex: S3) et son numéro (par ex: 3)"""
|
||||
# L'aggrégat considéré (par ex: 3S=S1+S2+S3), son nom de son semestre
|
||||
# terminal (par ex: S3) et son numéro (par ex: 3)
|
||||
noms_semestre_de_aggregat = TYPES_RCS[nom_rcs]["aggregat"]
|
||||
nom_semestre_terminal = noms_semestre_de_aggregat[-1]
|
||||
|
||||
@ -164,17 +178,17 @@ class RCSsJuryPE:
|
||||
self.suivi[etudid] = {
|
||||
aggregat: None
|
||||
for aggregat in pe_comp.TOUS_LES_SEMESTRES
|
||||
+ TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
|
||||
+ TOUS_LES_RCS_AVEC_PLUSIEURS_SEM
|
||||
}
|
||||
|
||||
"""Le formsemestre terminal (dernier en date) associé au
|
||||
semestre marquant la fin de l'aggrégat
|
||||
(par ex: son dernier S3 en date)"""
|
||||
# Le formsemestre terminal (dernier en date) associé au
|
||||
# semestre marquant la fin de l'aggrégat
|
||||
# (par ex: son dernier S3 en date)
|
||||
semestres = etudiants.cursus[etudid][nom_semestre_terminal]
|
||||
if semestres:
|
||||
formsemestre_final = get_dernier_semestre_en_date(semestres)
|
||||
|
||||
"""Ajout ou récupération de la trajectoire"""
|
||||
# Ajout ou récupération de la trajectoire
|
||||
trajectoire_id = (nom_rcs, formsemestre_final.formsemestre_id)
|
||||
if trajectoire_id not in self.rcss:
|
||||
trajectoire = RCS(nom_rcs, formsemestre_final)
|
||||
@ -182,21 +196,22 @@ class RCSsJuryPE:
|
||||
else:
|
||||
trajectoire = self.rcss[trajectoire_id]
|
||||
|
||||
"""La liste des semestres de l'étudiant à prendre en compte
|
||||
pour cette trajectoire"""
|
||||
# La liste des semestres de l'étudiant à prendre en compte
|
||||
# pour cette trajectoire
|
||||
semestres_a_aggreger = get_rcs_etudiant(
|
||||
etudiants.cursus[etudid], formsemestre_final, nom_rcs
|
||||
)
|
||||
|
||||
"""Ajout des semestres à la trajectoire"""
|
||||
# Ajout des semestres à la trajectoire
|
||||
trajectoire.add_semestres_a_aggreger(semestres_a_aggreger)
|
||||
|
||||
"""Mémoire la trajectoire suivie par l'étudiant"""
|
||||
# Mémoire la trajectoire suivie par l'étudiant
|
||||
self.suivi[etudid][nom_rcs] = trajectoire
|
||||
|
||||
|
||||
def get_rcs_etudiant(semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str
|
||||
):
|
||||
def get_rcs_etudiant(
|
||||
semestres: dict[int:FormSemestre], formsemestre_final: FormSemestre, nom_rcs: str
|
||||
) -> dict[int, FormSemestre]:
|
||||
"""Ensemble des semestres parcourus par un étudiant, connaissant
|
||||
les semestres de son cursus,
|
||||
dans le cadre du RCS visé et ayant pour semestre terminal `formsemestre_final`.
|
||||
@ -224,7 +239,7 @@ def get_rcs_etudiant(semestres: dict[int:FormSemestre], formsemestre_final: Form
|
||||
numero_semestre_terminal = formsemestre_final.semestre_id
|
||||
# semestres_significatifs = self.get_semestres_significatifs(etudid)
|
||||
semestres_significatifs = {}
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT+1):
|
||||
for i in range(1, pe_comp.NBRE_SEMESTRES_DIPLOMANT + 1):
|
||||
semestres_significatifs = semestres_significatifs | semestres[f"S{i}"]
|
||||
|
||||
if nom_rcs.startswith("S"): # les semestres
|
||||
@ -247,6 +262,7 @@ def get_rcs_etudiant(semestres: dict[int:FormSemestre], formsemestre_final: Form
|
||||
semestres_aggreges[fid] = semestre
|
||||
return semestres_aggreges
|
||||
|
||||
|
||||
def get_descr_rcs(nom_rcs: str) -> str:
|
||||
"""Renvoie la description pour les tableurs de synthèse
|
||||
Excel d'un nom de RCS"""
|
||||
|
@ -35,33 +35,30 @@ Created on Fri Sep 9 09:15:05 2016
|
||||
|
||||
@author: barasc
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import app.pe.pe_etudiant
|
||||
from app import db, log, ScoValueError
|
||||
from app.comp import res_sem, moy_ue, moy_sem
|
||||
from app.comp.moy_sem import comp_ranks_series
|
||||
from app.comp.res_compat import NotesTableCompat
|
||||
from app import db, ScoValueError
|
||||
from app import comp
|
||||
from app.comp.res_sem import load_formsemestre_results
|
||||
from app.models import FormSemestre
|
||||
from app.models.moduleimpls import ModuleImpl
|
||||
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
from app.pe.pe_tabletags import TableTag, MoyenneTag
|
||||
from app.scodoc import sco_tag_module
|
||||
from app.scodoc.codes_cursus import UE_SPORT
|
||||
import app.pe.pe_affichage as pe_affichage
|
||||
from app.pe.pe_tabletags import TableTag, TAGS_RESERVES, MoyenneTag
|
||||
import pandas as pd
|
||||
|
||||
|
||||
class SemestreTag(TableTag):
|
||||
"""
|
||||
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
|
||||
accès aux moyennes par tag.
|
||||
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
|
||||
"""
|
||||
|
||||
def __init__(self, formsemestre_id: int):
|
||||
"""
|
||||
Un SemestreTag représente les résultats des étudiants à un semestre, en donnant
|
||||
accès aux moyennes par tag.
|
||||
Il s'appuie principalement sur FormSemestre et sur ResultatsSemestreBUT.
|
||||
|
||||
Args:
|
||||
nom: Nom à donner au SemestreTag
|
||||
formsemestre_id: Identifiant du ``FormSemestre`` sur lequel il se base
|
||||
"""
|
||||
TableTag.__init__(self)
|
||||
@ -103,27 +100,27 @@ class SemestreTag(TableTag):
|
||||
dict_ues_competences = get_noms_competences_from_ues(self.nt.formsemestre)
|
||||
noms_tags_comp = list(set(dict_ues_competences.values()))
|
||||
noms_tags_auto = ["but"] + noms_tags_comp
|
||||
self.tags = (
|
||||
noms_tags_perso + noms_tags_auto
|
||||
)
|
||||
self.tags = noms_tags_perso + noms_tags_auto
|
||||
"""Tags du semestre taggué"""
|
||||
|
||||
## Vérifie l'unicité des tags
|
||||
if len(set(self.tags)) != len(self.tags):
|
||||
intersection = list(set(noms_tags_perso) & set(noms_tags_auto))
|
||||
liste_intersection = "\n".join([f"<li><code>{tag}</code></li>" for tag in intersection])
|
||||
message = f"""Erreur dans le module PE : Un des tags saisis dans votre programme de formation
|
||||
fait parti des tags réservés. En particulier,
|
||||
votre semestre <em>{self.formsemestre.titre_annee()}</em>
|
||||
contient le(s) tag(s) réservé(s) suivant :
|
||||
liste_intersection = "\n".join(
|
||||
[f"<li><code>{tag}</code></li>" for tag in intersection]
|
||||
)
|
||||
s = "s" if len(intersection) > 0 else ""
|
||||
message = f"""Erreur dans le module PE : Un des tags saisis dans votre
|
||||
programme de formation fait parti des tags réservés. En particulier,
|
||||
votre semestre <em>{self.formsemestre.titre_annee()}</em>
|
||||
contient le{s} tag{s} réservé{s} suivant :
|
||||
<ul>
|
||||
{liste_intersection}
|
||||
</ul>
|
||||
Modifiez votre programme de formation pour le(s) supprimer. Il(s) sera(ont) automatiquement à vos documents de poursuites d'études.
|
||||
Modifiez votre programme de formation pour le{s} supprimer.
|
||||
Il{s} ser{'ont' if s else 'a'} automatiquement à vos documents de poursuites d'études.
|
||||
"""
|
||||
raise ScoValueError(
|
||||
message
|
||||
)
|
||||
raise ScoValueError(message)
|
||||
|
||||
# Calcul des moyennes & les classements de chaque étudiant à chaque tag
|
||||
self.moyennes_tags = {}
|
||||
@ -174,25 +171,25 @@ class SemestreTag(TableTag):
|
||||
La série des moyennes
|
||||
"""
|
||||
|
||||
"""Adaptation du mask de calcul des moyennes au tag visé"""
|
||||
# Adaptation du mask de calcul des moyennes au tag visé
|
||||
modimpls_mask = [
|
||||
modimpl.module.ue.type != UE_SPORT
|
||||
for modimpl in self.formsemestre.modimpls_sorted
|
||||
]
|
||||
|
||||
"""Désactive tous les modules qui ne sont pas pris en compte pour ce tag"""
|
||||
# Désactive tous les modules qui ne sont pas pris en compte pour ce tag
|
||||
for i, modimpl in enumerate(self.formsemestre.modimpls_sorted):
|
||||
if modimpl.moduleimpl_id not in tags_infos[tag]:
|
||||
modimpls_mask[i] = False
|
||||
|
||||
"""Applique la pondération des coefficients"""
|
||||
# Applique la pondération des coefficients
|
||||
modimpl_coefs_ponderes_df = self.modimpl_coefs_df.copy()
|
||||
for modimpl_id in tags_infos[tag]:
|
||||
ponderation = tags_infos[tag][modimpl_id]["ponderation"]
|
||||
modimpl_coefs_ponderes_df[modimpl_id] *= ponderation
|
||||
|
||||
"""Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)"""
|
||||
moyennes_ues_tag = moy_ue.compute_ue_moys_apc(
|
||||
# Calcule les moyennes pour le tag visé dans chaque UE (dataframe etudid x ues)#
|
||||
moyennes_ues_tag = comp.moy_ue.compute_ue_moys_apc(
|
||||
self.sem_cube,
|
||||
self.etuds,
|
||||
self.formsemestre.modimpls_sorted,
|
||||
@ -203,13 +200,13 @@ class SemestreTag(TableTag):
|
||||
block=self.formsemestre.block_moyennes,
|
||||
)
|
||||
|
||||
"""Les ects"""
|
||||
# Les ects
|
||||
ects = self.ues_inscr_parcours_df.fillna(0.0) * [
|
||||
ue.ects for ue in self.ues if ue.type != UE_SPORT
|
||||
]
|
||||
|
||||
"""Calcule la moyenne générale dans le semestre (pondérée par le ECTS)"""
|
||||
moy_gen_tag = moy_sem.compute_sem_moys_apc_using_ects(
|
||||
# Calcule la moyenne générale dans le semestre (pondérée par le ECTS)
|
||||
moy_gen_tag = comp.moy_sem.compute_sem_moys_apc_using_ects(
|
||||
moyennes_ues_tag,
|
||||
ects,
|
||||
formation_id=self.formsemestre.formation_id,
|
||||
@ -224,11 +221,6 @@ def get_moduleimpl(modimpl_id) -> dict:
|
||||
modimpl = db.session.get(ModuleImpl, modimpl_id)
|
||||
if modimpl:
|
||||
return modimpl
|
||||
if SemestreTag.DEBUG:
|
||||
log(
|
||||
"SemestreTag.get_moduleimpl( %s ) : le modimpl recherche n'existe pas"
|
||||
% (modimpl_id)
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@ -260,27 +252,26 @@ def get_synthese_tags_personnalises_semestre(formsemestre: FormSemestre):
|
||||
"""
|
||||
synthese_tags = {}
|
||||
|
||||
"""Instance des modules du semestre"""
|
||||
# Instance des modules du semestre
|
||||
modimpls = formsemestre.modimpls_sorted
|
||||
|
||||
for modimpl in modimpls:
|
||||
modimpl_id = modimpl.id
|
||||
|
||||
"""Liste des tags pour le module concerné"""
|
||||
# Liste des tags pour le module concerné
|
||||
tags = sco_tag_module.module_tag_list(modimpl.module.id)
|
||||
|
||||
"""Traitement des tags recensés, chacun pouvant étant de la forme
|
||||
"mathématiques", "théorie", "pe:0", "maths:2"
|
||||
"""
|
||||
# Traitement des tags recensés, chacun pouvant étant de la forme
|
||||
# "mathématiques", "théorie", "pe:0", "maths:2"
|
||||
for tag in tags:
|
||||
"""Extraction du nom du tag et du coeff de pondération"""
|
||||
# Extraction du nom du tag et du coeff de pondération
|
||||
(tagname, ponderation) = sco_tag_module.split_tagname_coeff(tag)
|
||||
|
||||
"""Ajout d'une clé pour le tag"""
|
||||
# Ajout d'une clé pour le tag
|
||||
if tagname not in synthese_tags:
|
||||
synthese_tags[tagname] = {}
|
||||
|
||||
"""Ajout du module (modimpl) au tagname considéré"""
|
||||
# Ajout du module (modimpl) au tagname considéré
|
||||
synthese_tags[tagname][modimpl_id] = {
|
||||
"modimpl": modimpl, # les données sur le module
|
||||
# "coeff": modimpl.module.coefficient, # le coeff du module dans le semestre
|
||||
@ -298,6 +289,8 @@ def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
|
||||
"""Partant d'un formsemestre, extrait le nom des compétences associés
|
||||
à (ou aux) parcours des étudiants du formsemestre.
|
||||
|
||||
Ignore les UEs non associées à un niveau de compétence.
|
||||
|
||||
Args:
|
||||
formsemestre: Un FormSemestre
|
||||
|
||||
@ -310,8 +303,8 @@ def get_noms_competences_from_ues(formsemestre: FormSemestre) -> dict[int, str]:
|
||||
|
||||
noms_competences = {}
|
||||
for ue in nt.ues:
|
||||
if ue.type != UE_SPORT:
|
||||
ordre = ue.niveau_competence.ordre
|
||||
if ue.niveau_competence and ue.type != UE_SPORT:
|
||||
# ?? inutilisé ordre = ue.niveau_competence.ordre
|
||||
nom = ue.niveau_competence.competence.titre
|
||||
noms_competences[ue.ue_id] = f"comp. {nom}"
|
||||
return noms_competences
|
||||
|
@ -870,7 +870,7 @@ def stripquotes(s):
|
||||
return s
|
||||
|
||||
|
||||
def suppress_accents(s):
|
||||
def suppress_accents(s: str) -> str:
|
||||
"remove accents and suppress non ascii characters from string s"
|
||||
if isinstance(s, str):
|
||||
return (
|
||||
|
Loading…
Reference in New Issue
Block a user