forked from ScoDoc/ScoDoc
Calcul des etuds d'un modimpl avec notes en ATT. Affichage sur tableau bord. Fix tri liste etuds (#595).
This commit is contained in:
parent
58184664df
commit
1309e77bfa
@ -85,6 +85,8 @@ class ModuleImplResults:
|
|||||||
"{ evaluation.id : bool } indique si à prendre en compte ou non."
|
"{ evaluation.id : bool } indique si à prendre en compte ou non."
|
||||||
self.evaluations_etat = {}
|
self.evaluations_etat = {}
|
||||||
"{ evaluation_id: EvaluationEtat }"
|
"{ evaluation_id: EvaluationEtat }"
|
||||||
|
self.etudids_attente = set()
|
||||||
|
"etudids avec au moins une note ATT dans ce module"
|
||||||
self.en_attente = False
|
self.en_attente = False
|
||||||
"Vrai si au moins une évaluation a une note en attente"
|
"Vrai si au moins une évaluation a une note en attente"
|
||||||
#
|
#
|
||||||
@ -145,7 +147,6 @@ class ModuleImplResults:
|
|||||||
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
evals_notes = pd.DataFrame(index=self.etudids, dtype=float)
|
||||||
self.evaluations_completes = []
|
self.evaluations_completes = []
|
||||||
self.evaluations_completes_dict = {}
|
self.evaluations_completes_dict = {}
|
||||||
self.en_attente = False
|
|
||||||
for evaluation in moduleimpl.evaluations:
|
for evaluation in moduleimpl.evaluations:
|
||||||
eval_df = self._load_evaluation_notes(evaluation)
|
eval_df = self._load_evaluation_notes(evaluation)
|
||||||
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
# is_complete ssi tous les inscrits (non dem) au semestre ont une note
|
||||||
@ -172,15 +173,20 @@ class ModuleImplResults:
|
|||||||
eval_df, how="left", left_index=True, right_index=True
|
eval_df, how="left", left_index=True, right_index=True
|
||||||
)
|
)
|
||||||
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
# Notes en attente: (ne prend en compte que les inscrits, non démissionnaires)
|
||||||
nb_att = sum(
|
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
|
||||||
evals_notes[str(evaluation.id)][list(inscrits_module)]
|
eval_etudids_attente = set(
|
||||||
== scu.NOTES_ATTENTE
|
eval_notes_inscr.iloc[
|
||||||
|
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
|
||||||
|
].index
|
||||||
)
|
)
|
||||||
|
self.etudids_attente |= eval_etudids_attente
|
||||||
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
self.evaluations_etat[evaluation.id] = EvaluationEtat(
|
||||||
evaluation_id=evaluation.id, nb_attente=nb_att, is_complete=is_complete
|
evaluation_id=evaluation.id,
|
||||||
|
nb_attente=len(eval_etudids_attente),
|
||||||
|
is_complete=is_complete,
|
||||||
)
|
)
|
||||||
if nb_att > 0:
|
# au moins une note en ATT dans ce modimpl:
|
||||||
self.en_attente = True
|
self.en_attente = bool(self.etudids_attente)
|
||||||
|
|
||||||
# Force columns names to integers (evaluation ids)
|
# Force columns names to integers (evaluation ids)
|
||||||
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
|
evals_notes.columns = pd.Index([int(x) for x in evals_notes.columns], dtype=int)
|
||||||
|
@ -5,10 +5,12 @@ import pandas as pd
|
|||||||
import flask_sqlalchemy
|
import flask_sqlalchemy
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
|
from app.auth.models import User
|
||||||
from app.comp import df_cache
|
from app.comp import df_cache
|
||||||
from app.models.etudiants import Identite
|
from app.models.etudiants import Identite
|
||||||
from app.models.modules import Module
|
from app.models.modules import Module
|
||||||
|
from app.scodoc.sco_exceptions import AccessDenied, ScoLockedSemError
|
||||||
|
from app.scodoc.sco_permissions import Permission
|
||||||
from app.scodoc import sco_utils as scu
|
from app.scodoc import sco_utils as scu
|
||||||
|
|
||||||
|
|
||||||
@ -99,6 +101,27 @@ class ModuleImpl(db.Model):
|
|||||||
d.pop("module", None)
|
d.pop("module", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
|
||||||
|
"""Check if user can modify module resp.
|
||||||
|
If raise_exc, raises exception (AccessDenied or ScoLockedSemError) if not.
|
||||||
|
= Admin, et dir des etud. (si option l'y autorise)
|
||||||
|
"""
|
||||||
|
if not self.formsemestre.etat:
|
||||||
|
if raise_exc:
|
||||||
|
raise ScoLockedSemError("Modification impossible: semestre verrouille")
|
||||||
|
return False
|
||||||
|
# -- check access
|
||||||
|
# admin ou resp. semestre avec flag resp_can_change_resp
|
||||||
|
if user.has_permission(Permission.ScoImplement):
|
||||||
|
return True
|
||||||
|
if (
|
||||||
|
user.id in [resp.id for resp in self.formsemestre.responsables]
|
||||||
|
) and self.formsemestre.resp_can_change_ens:
|
||||||
|
return True
|
||||||
|
if raise_exc:
|
||||||
|
raise AccessDenied(f"Modification impossible pour {user}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
# Enseignants (chargés de TD ou TP) d'un moduleimpl
|
||||||
notes_modules_enseignants = db.Table(
|
notes_modules_enseignants = db.Table(
|
||||||
|
@ -122,6 +122,14 @@ class ScoLockedFormError(ScoValueError):
|
|||||||
super().__init__(msg=msg, dest_url=dest_url)
|
super().__init__(msg=msg, dest_url=dest_url)
|
||||||
|
|
||||||
|
|
||||||
|
class ScoLockedSemError(ScoValueError):
|
||||||
|
"Modification d'un formsemestre verrouillé"
|
||||||
|
|
||||||
|
def __init__(self, msg="", dest_url=None):
|
||||||
|
msg = "Ce semestre est verrouillé ! " + str(msg)
|
||||||
|
super().__init__(msg=msg, dest_url=dest_url)
|
||||||
|
|
||||||
|
|
||||||
class ScoNonEmptyFormationObject(ScoValueError):
|
class ScoNonEmptyFormationObject(ScoValueError):
|
||||||
"""On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent"""
|
"""On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent"""
|
||||||
|
|
||||||
|
@ -377,7 +377,7 @@ def can_change_module_resp(moduleimpl_id):
|
|||||||
if not current_user.has_permission(Permission.ScoImplement) and (
|
if not current_user.has_permission(Permission.ScoImplement) and (
|
||||||
(current_user.id not in sem["responsables"]) or (not sem["resp_can_change_ens"])
|
(current_user.id not in sem["responsables"]) or (not sem["resp_can_change_ens"])
|
||||||
):
|
):
|
||||||
raise AccessDenied("Modification impossible pour %s" % current_user)
|
raise AccessDenied(f"Modification impossible pour {current_user}")
|
||||||
return M, sem
|
return M, sem
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ from app.comp.res_compat import NotesTableCompat
|
|||||||
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
from app.models import FormSemestre, Identite, ScolarFormSemestreValidation, UniteEns
|
||||||
|
|
||||||
from app import log
|
from app import log
|
||||||
|
from app.tables import list_etuds
|
||||||
from app.scodoc.scolog import logdb
|
from app.scodoc.scolog import logdb
|
||||||
from app.scodoc import html_sco_header
|
from app.scodoc import html_sco_header
|
||||||
from app.scodoc import htmlutils
|
from app.scodoc import htmlutils
|
||||||
@ -520,14 +521,15 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
|||||||
H.append(f"""<th title="{ue.titre or ''}">{ue.acronyme}</th>""")
|
H.append(f"""<th title="{ue.titre or ''}">{ue.acronyme}</th>""")
|
||||||
H.append("""</tr>""")
|
H.append("""</tr>""")
|
||||||
|
|
||||||
for etudid, ues_etud in table_inscr.items():
|
etuds = list_etuds.etuds_sorted_from_ids(table_inscr.keys())
|
||||||
etud: Identite = Identite.query.get(etudid)
|
for etud in etuds:
|
||||||
|
ues_etud = table_inscr[etud.id]
|
||||||
H.append(
|
H.append(
|
||||||
f"""<tr><td><a class="discretelink etudinfo" id={etudid}
|
f"""<tr><td><a class="discretelink etudinfo" id={etud.id}
|
||||||
href="{url_for(
|
href="{url_for(
|
||||||
"scolar.ficheEtud",
|
"scolar.ficheEtud",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
etudid=etudid,
|
etudid=etud.id,
|
||||||
)}"
|
)}"
|
||||||
>{etud.nomprenom}</a></td>"""
|
>{etud.nomprenom}</a></td>"""
|
||||||
)
|
)
|
||||||
@ -539,7 +541,7 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
|||||||
else:
|
else:
|
||||||
# Validations d'UE déjà enregistrées dans d'autres semestres
|
# Validations d'UE déjà enregistrées dans d'autres semestres
|
||||||
validations_ue = (
|
validations_ue = (
|
||||||
ScolarFormSemestreValidation.query.filter_by(etudid=etudid)
|
ScolarFormSemestreValidation.query.filter_by(etudid=etud.id)
|
||||||
.filter(
|
.filter(
|
||||||
ScolarFormSemestreValidation.formsemestre_id
|
ScolarFormSemestreValidation.formsemestre_id
|
||||||
!= res.formsemestre.id,
|
!= res.formsemestre.id,
|
||||||
@ -556,7 +558,8 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
|||||||
)
|
)
|
||||||
validation = validations_ue[-1] if validations_ue else None
|
validation = validations_ue[-1] if validations_ue else None
|
||||||
expl_validation = (
|
expl_validation = (
|
||||||
f"""Validée ({validation.code}) le {validation.event_date.strftime("%d/%m/%Y")}"""
|
f"""Validée ({validation.code}) le {
|
||||||
|
validation.event_date.strftime("%d/%m/%Y")}"""
|
||||||
if validation
|
if validation
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
@ -567,13 +570,13 @@ def _list_but_ue_inscriptions(res: NotesTableCompat, read_only: bool = True) ->
|
|||||||
title="{etud.nomprenom} {'inscrit' if est_inscr else 'non inscrit'} à l'UE {ue.acronyme}. {expl_validation}",
|
title="{etud.nomprenom} {'inscrit' if est_inscr else 'non inscrit'} à l'UE {ue.acronyme}. {expl_validation}",
|
||||||
onchange="change_ue_inscr(this);"
|
onchange="change_ue_inscr(this);"
|
||||||
data-url_inscr={
|
data-url_inscr={
|
||||||
url_for("notes.etud_inscrit_ue",
|
url_for("notes.etud_inscrit_ue",
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
scodoc_dept=g.scodoc_dept, etudid=etud.id,
|
||||||
formsemestre_id=res.formsemestre.id, ue_id=ue.id)
|
formsemestre_id=res.formsemestre.id, ue_id=ue.id)
|
||||||
}
|
}
|
||||||
data-url_desinscr={
|
data-url_desinscr={
|
||||||
url_for("notes.etud_desinscrit_ue",
|
url_for("notes.etud_desinscrit_ue",
|
||||||
scodoc_dept=g.scodoc_dept, etudid=etudid,
|
scodoc_dept=g.scodoc_dept, etudid=etud.id,
|
||||||
formsemestre_id=res.formsemestre.id, ue_id=ue.id)
|
formsemestre_id=res.formsemestre.id, ue_id=ue.id)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -36,6 +36,7 @@ from flask_login import current_user
|
|||||||
from app import db
|
from app import db
|
||||||
from app.auth.models import User
|
from app.auth.models import User
|
||||||
from app.comp import res_sem
|
from app.comp import res_sem
|
||||||
|
from app.comp.res_common import ResultatsSemestre
|
||||||
from app.comp.res_compat import NotesTableCompat
|
from app.comp.res_compat import NotesTableCompat
|
||||||
from app.models import FormSemestre, ModuleImpl
|
from app.models import FormSemestre, ModuleImpl
|
||||||
from app.models.evaluations import Evaluation
|
from app.models.evaluations import Evaluation
|
||||||
@ -59,9 +60,7 @@ from app.scodoc import sco_formsemestre_status
|
|||||||
from app.scodoc import sco_groups
|
from app.scodoc import sco_groups
|
||||||
from app.scodoc import sco_moduleimpl
|
from app.scodoc import sco_moduleimpl
|
||||||
from app.scodoc import sco_permissions_check
|
from app.scodoc import sco_permissions_check
|
||||||
from app.scodoc import sco_users
|
from app.tables import list_etuds
|
||||||
|
|
||||||
# ported from old DTML code in oct 2009
|
|
||||||
|
|
||||||
# menu evaluation dans moduleimpl
|
# menu evaluation dans moduleimpl
|
||||||
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
|
def moduleimpl_evaluation_menu(evaluation_id, nbnotes=0) -> str:
|
||||||
@ -196,23 +195,20 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
if not isinstance(moduleimpl_id, int):
|
if not isinstance(moduleimpl_id, int):
|
||||||
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
|
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
|
||||||
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||||
M = modimpl.to_dict()
|
mi_dict = modimpl.to_dict()
|
||||||
formsemestre_id = modimpl.formsemestre_id
|
formsemestre_id = modimpl.formsemestre_id
|
||||||
formsemestre: FormSemestre = modimpl.formsemestre
|
formsemestre: FormSemestre = modimpl.formsemestre
|
||||||
Mod = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
|
mod_dict = sco_edit_module.module_list(args={"module_id": modimpl.module_id})[0]
|
||||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||||
F = sco_formations.formation_list(args={"formation_id": sem["formation_id"]})[0]
|
formation_dict = sco_formations.formation_list(
|
||||||
|
args={"formation_id": sem["formation_id"]}
|
||||||
|
)[0]
|
||||||
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
mod_inscrits = sco_moduleimpl.do_moduleimpl_inscription_list(
|
||||||
moduleimpl_id=moduleimpl_id
|
moduleimpl_id=moduleimpl_id
|
||||||
)
|
)
|
||||||
|
|
||||||
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
|
||||||
|
# Evaluations, la plus RECENTE en tête
|
||||||
# mod_evals = sco_evaluation_db.do_evaluation_list({"moduleimpl_id": moduleimpl_id})
|
|
||||||
# mod_evals.sort(
|
|
||||||
# key=lambda x: (x["numero"], x["jour"], x["heure_debut"]), reverse=True
|
|
||||||
# )
|
|
||||||
# la plus RECENTE en tête
|
|
||||||
evaluations = modimpl.evaluations.order_by(
|
evaluations = modimpl.evaluations.order_by(
|
||||||
Evaluation.numero.desc(),
|
Evaluation.numero.desc(),
|
||||||
Evaluation.jour.desc(),
|
Evaluation.jour.desc(),
|
||||||
@ -240,18 +236,23 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
)
|
)
|
||||||
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
|
||||||
#
|
#
|
||||||
module_resp = User.query.get(M["responsable_id"])
|
module_resp = User.query.get(modimpl.responsable_id)
|
||||||
mod_type_name = scu.MODULE_TYPE_NAMES[Mod["module_type"]]
|
mod_type_name = scu.MODULE_TYPE_NAMES[mod_dict["module_type"]]
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.sco_header(
|
html_sco_header.sco_header(
|
||||||
page_title=f"{mod_type_name} {Mod['code']} {Mod['titre']}"
|
page_title=f"{mod_type_name} {mod_dict['code']} {mod_dict['titre']}",
|
||||||
|
javascripts=["js/etud_info.js"],
|
||||||
|
init_qtip=True,
|
||||||
),
|
),
|
||||||
f"""<h2 class="formsemestre">{mod_type_name}
|
f"""<h2 class="formsemestre">{mod_type_name}
|
||||||
<tt>{Mod['code']}</tt> {Mod['titre']}
|
<tt>{mod_dict['code']}</tt> {mod_dict['titre']}
|
||||||
{"dans l'UE " + modimpl.module.ue.acronyme if modimpl.module.module_type == scu.ModuleType.MALUS else ""}
|
{"dans l'UE " + modimpl.module.ue.acronyme
|
||||||
|
if modimpl.module.module_type == scu.ModuleType.MALUS
|
||||||
|
else ""
|
||||||
|
}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="moduleimpl_tableaubord moduleimpl_type_{
|
<div class="moduleimpl_tableaubord moduleimpl_type_{
|
||||||
scu.ModuleType(Mod['module_type']).name.lower()}">
|
scu.ModuleType(mod_dict['module_type']).name.lower()}">
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="fichetitre2">Responsable: </td><td class="redboldtext">
|
<td class="fichetitre2">Responsable: </td><td class="redboldtext">
|
||||||
@ -259,18 +260,14 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
<span class="blacktt">({module_resp.user_name})</span>
|
<span class="blacktt">({module_resp.user_name})</span>
|
||||||
""",
|
""",
|
||||||
]
|
]
|
||||||
try:
|
if modimpl.can_change_ens_by(current_user):
|
||||||
sco_moduleimpl.can_change_module_resp(moduleimpl_id)
|
|
||||||
H.append(
|
H.append(
|
||||||
"""<a class="stdlink" href="edit_moduleimpl_resp?moduleimpl_id=%s">modifier</a>"""
|
f"""<a class="stdlink" href="{url_for("notes.edit_moduleimpl_resp",
|
||||||
% moduleimpl_id
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||||
|
}" >modifier</a>"""
|
||||||
)
|
)
|
||||||
except:
|
|
||||||
pass
|
|
||||||
H.append("""</td><td>""")
|
H.append("""</td><td>""")
|
||||||
H.append(
|
H.append(", ".join([u.get_nomprenom() for u in modimpl.enseignants]))
|
||||||
", ".join([sco_users.user_info(m["ens_id"])["nomprenom"] for m in M["ens"]])
|
|
||||||
)
|
|
||||||
H.append("""</td><td>""")
|
H.append("""</td><td>""")
|
||||||
try:
|
try:
|
||||||
sco_moduleimpl.can_change_ens(moduleimpl_id)
|
sco_moduleimpl.can_change_ens(moduleimpl_id)
|
||||||
@ -302,7 +299,8 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
H.append("""</td><td></td></tr>""")
|
H.append("""</td><td></td></tr>""")
|
||||||
# 3ieme ligne: Formation
|
# 3ieme ligne: Formation
|
||||||
H.append(
|
H.append(
|
||||||
"""<tr><td class="fichetitre2">Formation: </td><td>%(titre)s</td></tr>""" % F
|
"""<tr><td class="fichetitre2">Formation: </td><td>%(titre)s</td></tr>"""
|
||||||
|
% formation_dict
|
||||||
)
|
)
|
||||||
# Ligne: Inscrits
|
# Ligne: Inscrits
|
||||||
H.append(
|
H.append(
|
||||||
@ -312,15 +310,18 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
if current_user.has_permission(Permission.ScoEtudInscrit):
|
if current_user.has_permission(Permission.ScoEtudInscrit):
|
||||||
H.append(
|
H.append(
|
||||||
"""<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">modifier</a>"""
|
"""<a class="stdlink" style="margin-left:2em;" href="moduleimpl_inscriptions_edit?moduleimpl_id=%s">modifier</a>"""
|
||||||
% M["moduleimpl_id"]
|
% mi_dict["moduleimpl_id"]
|
||||||
)
|
)
|
||||||
H.append("</td></tr>")
|
H.append("</td></tr>")
|
||||||
# Ligne: règle de calcul
|
# Ligne: règle de calcul
|
||||||
has_expression = sco_compute_moy.moduleimpl_has_expression(M)
|
has_expression = sco_compute_moy.moduleimpl_has_expression(mi_dict)
|
||||||
if has_expression:
|
if has_expression:
|
||||||
H.append(
|
H.append(
|
||||||
'<tr><td class="fichetitre2" colspan="4">Règle de calcul: <span class="formula" title="mode de calcul de la moyenne du module">moyenne=<tt>%s</tt></span>'
|
f"""<tr>
|
||||||
% M["computation_expr"]
|
<td class="fichetitre2" colspan="4">Règle de calcul:
|
||||||
|
<span class="formula" title="mode de calcul de la moyenne du module"
|
||||||
|
>moyenne=<tt>{mi_dict["computation_expr"]}</tt>
|
||||||
|
</span>"""
|
||||||
)
|
)
|
||||||
H.append("""<span class="warning">inutilisée dans cette version de ScoDoc""")
|
H.append("""<span class="warning">inutilisée dans cette version de ScoDoc""")
|
||||||
if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
|
if sco_moduleimpl.can_change_ens(moduleimpl_id, raise_exc=False):
|
||||||
@ -380,20 +381,24 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
#
|
#
|
||||||
if formsemestre_has_decisions(formsemestre_id):
|
if formsemestre_has_decisions(formsemestre_id):
|
||||||
H.append(
|
H.append(
|
||||||
"""<ul class="tf-msg"><li class="tf-msg warning">Décisions de jury saisies: seul le responsable du semestre peut saisir des notes (il devra modifier les décisions de jury).</li></ul>"""
|
"""<ul class="tf-msg">
|
||||||
|
<li class="tf-msg warning">Décisions de jury saisies: seul le responsable du
|
||||||
|
semestre peut saisir des notes (il devra modifier les décisions de jury).
|
||||||
|
</li>
|
||||||
|
</ul>"""
|
||||||
)
|
)
|
||||||
#
|
#
|
||||||
H.append(
|
H.append(
|
||||||
"""<p><form name="f"><span style="font-size:120%%; font-weight: bold;">%d évaluations :</span>
|
f"""<p><form name="f">
|
||||||
|
<span style="font-size:120%%; font-weight: bold;">{nb_evaluations} évaluations :</span>
|
||||||
<span style="padding-left: 30px;">
|
<span style="padding-left: 30px;">
|
||||||
<input type="hidden" name="moduleimpl_id" value="%s"/>"""
|
<input type="hidden" name="moduleimpl_id" value="{moduleimpl_id}"/>"""
|
||||||
% (nb_evaluations, moduleimpl_id)
|
|
||||||
)
|
)
|
||||||
#
|
#
|
||||||
# Liste les noms de partitions
|
# Liste les noms de partitions
|
||||||
partitions = sco_groups.get_partitions_list(sem["formsemestre_id"])
|
partitions = sco_groups.get_partitions_list(sem["formsemestre_id"])
|
||||||
H.append(
|
H.append(
|
||||||
"""Afficher les groupes
|
"""Afficher les groupes
|
||||||
de <select name="partition_id" onchange="document.f.submit();">"""
|
de <select name="partition_id" onchange="document.f.submit();">"""
|
||||||
)
|
)
|
||||||
been_selected = False
|
been_selected = False
|
||||||
@ -409,8 +414,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
if name is None:
|
if name is None:
|
||||||
name = "Tous"
|
name = "Tous"
|
||||||
H.append(
|
H.append(
|
||||||
"""<option value="%s" %s>%s</option>"""
|
f"""<option value="{partition['partition_id']}" {selected}>{name}</option>"""
|
||||||
% (partition["partition_id"], selected, name)
|
|
||||||
)
|
)
|
||||||
H.append(
|
H.append(
|
||||||
"""</select>
|
"""</select>
|
||||||
@ -420,20 +424,21 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
</form>
|
</form>
|
||||||
</p>
|
</p>
|
||||||
"""
|
"""
|
||||||
% M
|
% mi_dict
|
||||||
)
|
)
|
||||||
|
|
||||||
# -------- Tableau des evaluations
|
# -------- Tableau des evaluations
|
||||||
top_table_links = ""
|
top_table_links = ""
|
||||||
if can_edit_evals:
|
if can_edit_evals:
|
||||||
top_table_links = f"""<a class="stdlink" href="{
|
top_table_links = f"""<a class="stdlink" href="{
|
||||||
url_for("notes.evaluation_create", scodoc_dept=g.scodoc_dept, moduleimpl_id=M['moduleimpl_id'])
|
url_for("notes.evaluation_create", scodoc_dept=g.scodoc_dept, moduleimpl_id=mi_dict['moduleimpl_id'])
|
||||||
}">Créer nouvelle évaluation</a>
|
}">Créer nouvelle évaluation</a>
|
||||||
"""
|
"""
|
||||||
if nb_evaluations > 0:
|
if nb_evaluations > 0:
|
||||||
top_table_links += f"""
|
top_table_links += f"""
|
||||||
<a class="stdlink" style="margin-left:2em;" href="{
|
<a class="stdlink" style="margin-left:2em;" href="{
|
||||||
url_for("notes.moduleimpl_evaluation_renumber", scodoc_dept=g.scodoc_dept, moduleimpl_id=M['moduleimpl_id'],
|
url_for("notes.moduleimpl_evaluation_renumber",
|
||||||
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=mi_dict['moduleimpl_id'],
|
||||||
redirect=1)
|
redirect=1)
|
||||||
}">Trier par date</a>
|
}">Trier par date</a>
|
||||||
"""
|
"""
|
||||||
@ -477,31 +482,35 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||||||
f"""</td></tr>
|
f"""</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<div class="list_etuds_attente">
|
||||||
|
{_html_modimpl_etuds_attente(nt, modimpl)}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- LEGENDE -->
|
<!-- LEGENDE -->
|
||||||
<hr>
|
<hr>
|
||||||
<h4>Légende</h4>
|
<h4>Légende</h4>
|
||||||
<ul>
|
<ul>
|
||||||
<li>{scu.icontag("edit_img")} : modifie description de l'évaluation
|
<li>{scu.icontag("edit_img")} : modifie description de l'évaluation
|
||||||
(date, heure, coefficient, ...)
|
(date, heure, coefficient, ...)
|
||||||
</li>
|
</li>
|
||||||
<li>{scu.icontag("notes_img")} : saisie des notes</li>
|
<li>{scu.icontag("notes_img")} : saisie des notes</li>
|
||||||
<li>{scu.icontag("delete_img")} : indique qu'il n'y a aucune note
|
<li>{scu.icontag("delete_img")} : indique qu'il n'y a aucune note
|
||||||
entrée (cliquer pour supprimer cette évaluation)
|
entrée (cliquer pour supprimer cette évaluation)
|
||||||
</li>
|
</li>
|
||||||
<li>{scu.icontag("status_orange_img")} : indique qu'il manque
|
<li>{scu.icontag("status_orange_img")} : indique qu'il manque
|
||||||
quelques notes dans cette évaluation
|
quelques notes dans cette évaluation
|
||||||
</li>
|
</li>
|
||||||
<li>{scu.icontag("status_green_img")} : toutes les notes sont
|
<li>{scu.icontag("status_green_img")} : toutes les notes sont
|
||||||
entrées (cliquer pour les afficher)
|
entrées (cliquer pour les afficher)
|
||||||
</li>
|
</li>
|
||||||
<li>{scu.icontag("status_visible_img")} : indique que cette évaluation
|
<li>{scu.icontag("status_visible_img")} : indique que cette évaluation
|
||||||
sera mentionnée dans les bulletins au format "intermédiaire"
|
sera mentionnée dans les bulletins au format "intermédiaire"
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Rappel : seules les notes des évaluations complètement saisies
|
<p>Rappel : seules les notes des évaluations complètement saisies
|
||||||
(affichées en vert) apparaissent dans les bulletins.
|
(affichées en vert) apparaissent dans les bulletins.
|
||||||
</p>
|
</p>
|
||||||
"""
|
"""
|
||||||
@ -844,3 +853,22 @@ def _evaluation_poids_html(evaluation: Evaluation, max_poids: float = 0.0) -> st
|
|||||||
+ "</div>"
|
+ "</div>"
|
||||||
)
|
)
|
||||||
return H
|
return H
|
||||||
|
|
||||||
|
|
||||||
|
def _html_modimpl_etuds_attente(res: ResultatsSemestre, modimpl: ModuleImpl) -> str:
|
||||||
|
"""Affiche la liste des étudiants ayant au moins une note en attente dans ce modimpl"""
|
||||||
|
m_res = res.modimpls_results.get(modimpl.id)
|
||||||
|
if m_res:
|
||||||
|
if not m_res.etudids_attente:
|
||||||
|
return "<div><em>Aucun étudiant n'a de notes en attente.</em></div>"
|
||||||
|
elif len(m_res.etudids_attente) < 10:
|
||||||
|
return f"""
|
||||||
|
<h4>Étudiants avec une note en attente :</h4>
|
||||||
|
{list_etuds.html_table_etuds(m_res.etudids_attente)}
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
return f"""<div class="warning"><em>{
|
||||||
|
len(m_res.etudids_attente)
|
||||||
|
} étudiants ont des notes en attente.</em></div>"""
|
||||||
|
|
||||||
|
return ""
|
||||||
|
@ -641,4 +641,9 @@ table.dataTable.order-column.stripe.hover tbody tr.even:hover td.sorting_1 {
|
|||||||
table.dataTable.gt_table {
|
table.dataTable.gt_table {
|
||||||
width: auto;
|
width: auto;
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables non centrées */
|
||||||
|
table.dataTable.gt_table.gt_left {
|
||||||
|
margin-left: 16px;
|
||||||
}
|
}
|
117
app/tables/list_etuds.py
Normal file
117
app/tables/list_etuds.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
##############################################################################
|
||||||
|
# ScoDoc
|
||||||
|
# Copyright (c) 1999 - 2023 Emmanuel Viennet. All rights reserved.
|
||||||
|
# See LICENSE
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
"""Liste simple d'étudiants
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import g, url_for
|
||||||
|
from app.models import Identite
|
||||||
|
from app.tables import table_builder as tb
|
||||||
|
|
||||||
|
|
||||||
|
class TableEtud(tb.Table):
|
||||||
|
"""Table listant des étudiants
|
||||||
|
Peut-être sous-classée pour ajouter des colonnes.
|
||||||
|
L'id de la ligne est etuid, et le row stocke etud.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
etuds: list[Identite] = None,
|
||||||
|
classes: list[str] = None,
|
||||||
|
row_class=None,
|
||||||
|
with_foot_titles=False,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.rows: list["RowEtud"] = [] # juste pour que VSCode nous aide sur .rows
|
||||||
|
classes = classes or ["gt_table", "gt_left"]
|
||||||
|
super().__init__(
|
||||||
|
row_class=row_class or RowEtud,
|
||||||
|
classes=classes,
|
||||||
|
with_foot_titles=with_foot_titles,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
self.add_etuds(etuds)
|
||||||
|
|
||||||
|
def add_etuds(self, etuds: list[Identite]):
|
||||||
|
"Ajoute des étudiants à la table"
|
||||||
|
for etud in etuds:
|
||||||
|
row = self.row_class(self, etud)
|
||||||
|
row.add_etud_cols()
|
||||||
|
self.add_row(row)
|
||||||
|
|
||||||
|
|
||||||
|
class RowEtud(tb.Row):
|
||||||
|
"Ligne de la table d'étudiants"
|
||||||
|
# pour le moment très simple, extensible (codes, liens bulletins, ...)
|
||||||
|
def __init__(self, table: TableEtud, etud: Identite, *args, **kwargs):
|
||||||
|
super().__init__(table, etud.id, *args, **kwargs)
|
||||||
|
self.etud = etud
|
||||||
|
|
||||||
|
def add_etud_cols(self):
|
||||||
|
"""Ajoute colonnes étudiant: codes, noms"""
|
||||||
|
etud = self.etud
|
||||||
|
self.table.group_titles.update(
|
||||||
|
{
|
||||||
|
"etud_codes": "Codes",
|
||||||
|
"identite_detail": "",
|
||||||
|
"identite_court": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
# --- Codes (seront cachés, mais exportés en excel)
|
||||||
|
# self.add_cell("etudid", "etudid", etud.id, "etud_codes")
|
||||||
|
# self.add_cell(
|
||||||
|
# "code_nip",
|
||||||
|
# "code_nip",
|
||||||
|
# etud.code_nip or "",
|
||||||
|
# "etud_codes",
|
||||||
|
# )
|
||||||
|
|
||||||
|
# --- Identité étudiant
|
||||||
|
# url_bulletin = url_for(
|
||||||
|
# "notes.formsemestre_bulletinetud",
|
||||||
|
# scodoc_dept=g.scodoc_dept,
|
||||||
|
# formsemestre_id=res.formsemestre.id,
|
||||||
|
# etudid=etud.id,
|
||||||
|
# )
|
||||||
|
url_bulletin = None # pour extension future
|
||||||
|
self.add_cell("civilite_str", "Civ.", etud.civilite_str, "identite_detail")
|
||||||
|
self.add_cell(
|
||||||
|
"nom_disp",
|
||||||
|
"Nom",
|
||||||
|
etud.nom_disp(),
|
||||||
|
"identite_detail",
|
||||||
|
data={"order": etud.sort_key},
|
||||||
|
target=url_bulletin,
|
||||||
|
target_attrs={"class": "etudinfo discretelink", "id": str(etud.id)},
|
||||||
|
)
|
||||||
|
self.add_cell("prenom", "Prénom", etud.prenom, "identite_detail")
|
||||||
|
# self.add_cell(
|
||||||
|
# "nom_short",
|
||||||
|
# "Nom",
|
||||||
|
# etud.nom_short,
|
||||||
|
# "identite_court",
|
||||||
|
# data={
|
||||||
|
# "order": etud.sort_key,
|
||||||
|
# "etudid": etud.id,
|
||||||
|
# "nomprenom": etud.nomprenom,
|
||||||
|
# },
|
||||||
|
# target=url_bulletin,
|
||||||
|
# target_attrs={"class": "etudinfo", "id": str(etud.id)},
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
def etuds_sorted_from_ids(etudids) -> list[Identite]:
|
||||||
|
"Liste triée d'etuds à partir d'une collections d'etudids"
|
||||||
|
etuds = [Identite.query.get_or_404(etudid) for etudid in etudids]
|
||||||
|
return sorted(etuds, key=lambda etud: etud.sort_key)
|
||||||
|
|
||||||
|
|
||||||
|
def html_table_etuds(etudids) -> str:
|
||||||
|
"""Table HTML simple des étudiants indiqués"""
|
||||||
|
etuds = etuds_sorted_from_ids(etudids)
|
||||||
|
table = TableEtud(etuds)
|
||||||
|
return table.html()
|
@ -38,8 +38,6 @@ class TableRecap(tb.Table):
|
|||||||
moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
|
moy_sae_<modimpl_id>_<ue_id>, ... les moyennes de SAE dans l'UE
|
||||||
|
|
||||||
On ajoute aussi des classes:
|
On ajoute aussi des classes:
|
||||||
- pour les lignes:
|
|
||||||
selected_row pour l'étudiant sélectionné
|
|
||||||
- les colonnes:
|
- les colonnes:
|
||||||
- la moyenne générale a la classe col_moy_gen
|
- la moyenne générale a la classe col_moy_gen
|
||||||
- les colonnes SAE ont la classe col_sae
|
- les colonnes SAE ont la classe col_sae
|
||||||
|
@ -68,6 +68,7 @@ class Table(Element):
|
|||||||
classes: list[str] = None,
|
classes: list[str] = None,
|
||||||
attrs: dict[str, str] = None,
|
attrs: dict[str, str] = None,
|
||||||
data: dict = None,
|
data: dict = None,
|
||||||
|
with_foot_titles=True,
|
||||||
row_class=None,
|
row_class=None,
|
||||||
xls_sheet_name="feuille",
|
xls_sheet_name="feuille",
|
||||||
xls_before_table=[], # liste de cellules a placer avant la table
|
xls_before_table=[], # liste de cellules a placer avant la table
|
||||||
@ -100,8 +101,10 @@ class Table(Element):
|
|||||||
self.head_title_row: "Row" = Row(
|
self.head_title_row: "Row" = Row(
|
||||||
self, "title_head", cell_elt="th", classes=["titles"]
|
self, "title_head", cell_elt="th", classes=["titles"]
|
||||||
)
|
)
|
||||||
self.foot_title_row: "Row" = Row(
|
self.foot_title_row: "Row" = (
|
||||||
self, "title_foot", cell_elt="th", classes=["titles"]
|
Row(self, "title_foot", cell_elt="th", classes=["titles"])
|
||||||
|
if with_foot_titles
|
||||||
|
else None
|
||||||
)
|
)
|
||||||
self.empty_cell = Cell.empty()
|
self.empty_cell = Cell.empty()
|
||||||
# Excel (xls) spécifique:
|
# Excel (xls) spécifique:
|
||||||
@ -119,8 +122,10 @@ class Table(Element):
|
|||||||
"""
|
"""
|
||||||
self.sort_columns()
|
self.sort_columns()
|
||||||
# Titres
|
# Titres
|
||||||
self.add_head_row(self.head_title_row)
|
if self.head_title_row:
|
||||||
self.add_foot_row(self.foot_title_row)
|
self.add_head_row(self.head_title_row)
|
||||||
|
if self.foot_title_row:
|
||||||
|
self.add_foot_row(self.foot_title_row)
|
||||||
|
|
||||||
def get_row_by_id(self, row_id) -> "Row":
|
def get_row_by_id(self, row_id) -> "Row":
|
||||||
"return the row, or None"
|
"return the row, or None"
|
||||||
@ -261,18 +266,23 @@ class Table(Element):
|
|||||||
title = title or ""
|
title = title or ""
|
||||||
if col_id not in self.titles:
|
if col_id not in self.titles:
|
||||||
self.titles[col_id] = title
|
self.titles[col_id] = title
|
||||||
self.head_title_row.cells[col_id] = self.head_title_row.add_cell(
|
if self.head_title_row:
|
||||||
col_id,
|
self.head_title_row.cells[col_id] = self.head_title_row.add_cell(
|
||||||
None,
|
col_id,
|
||||||
title,
|
None,
|
||||||
classes=classes,
|
title,
|
||||||
group=self.column_group.get(col_id),
|
classes=classes,
|
||||||
)
|
group=self.column_group.get(col_id),
|
||||||
self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
|
)
|
||||||
col_id, None, title, classes=classes
|
if self.foot_title_row:
|
||||||
)
|
self.foot_title_row.cells[col_id] = self.foot_title_row.add_cell(
|
||||||
|
col_id, None, title, classes=classes
|
||||||
return self.head_title_row.cells.get(col_id), self.foot_title_row.cells[col_id]
|
)
|
||||||
|
head_cell = (
|
||||||
|
self.head_title_row.cells.get(col_id) if self.head_title_row else None
|
||||||
|
)
|
||||||
|
foot_cell = self.foot_title_row.cells[col_id] if self.foot_title_row else None
|
||||||
|
return head_cell, foot_cell
|
||||||
|
|
||||||
def excel(self, wb: Workbook = None):
|
def excel(self, wb: Workbook = None):
|
||||||
"""Simple Excel representation of the table."""
|
"""Simple Excel representation of the table."""
|
||||||
|
@ -1043,15 +1043,18 @@ def edit_enseignants_form(moduleimpl_id):
|
|||||||
@scodoc
|
@scodoc
|
||||||
@permission_required(Permission.ScoView)
|
@permission_required(Permission.ScoView)
|
||||||
@scodoc7func
|
@scodoc7func
|
||||||
def edit_moduleimpl_resp(moduleimpl_id):
|
def edit_moduleimpl_resp(moduleimpl_id: int):
|
||||||
"""Changement d'un enseignant responsable de module
|
"""Changement d'un enseignant responsable de module
|
||||||
Accessible par Admin et dir des etud si flag resp_can_change_ens
|
Accessible par Admin et dir des etud si flag resp_can_change_ens
|
||||||
"""
|
"""
|
||||||
M, sem = sco_moduleimpl.can_change_module_resp(moduleimpl_id)
|
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||||
|
modimpl.can_change_ens_by(current_user, raise_exc=True) # access control
|
||||||
H = [
|
H = [
|
||||||
html_sco_header.html_sem_header(
|
html_sco_header.html_sem_header(
|
||||||
'Modification du responsable du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
|
f"""Modification du responsable du <a href="{
|
||||||
% (moduleimpl_id, M["module"]["titre"]),
|
url_for("notes.moduleimpl_status",
|
||||||
|
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
|
||||||
|
}">module {modimpl.module.titre or ""}</a>""",
|
||||||
javascripts=["libjs/AutoSuggest.js"],
|
javascripts=["libjs/AutoSuggest.js"],
|
||||||
cssstyles=["css/autosuggest_inquisitor.css"],
|
cssstyles=["css/autosuggest_inquisitor.css"],
|
||||||
bodyOnLoad="init_tf_form('')",
|
bodyOnLoad="init_tf_form('')",
|
||||||
@ -1065,9 +1068,9 @@ def edit_moduleimpl_resp(moduleimpl_id):
|
|||||||
uid2display[u["id"]] = u["nomplogin"]
|
uid2display[u["id"]] = u["nomplogin"]
|
||||||
allowed_user_names = list(uid2display.values())
|
allowed_user_names = list(uid2display.values())
|
||||||
|
|
||||||
initvalues = M
|
initvalues = modimpl.to_dict(with_module=False)
|
||||||
initvalues["responsable_id"] = uid2display.get(
|
initvalues["responsable_id"] = uid2display.get(
|
||||||
M["responsable_id"], M["responsable_id"]
|
modimpl.responsable_id, modimpl.responsable_id
|
||||||
)
|
)
|
||||||
form = [
|
form = [
|
||||||
("moduleimpl_id", {"input_type": "hidden"}),
|
("moduleimpl_id", {"input_type": "hidden"}),
|
||||||
@ -1112,9 +1115,8 @@ def edit_moduleimpl_resp(moduleimpl_id):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
responsable_id = User.get_user_id_from_nomplogin(tf[2]["responsable_id"])
|
responsable_id = User.get_user_id_from_nomplogin(tf[2]["responsable_id"])
|
||||||
if (
|
if not responsable_id:
|
||||||
not responsable_id
|
# presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps)
|
||||||
): # presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps)
|
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"notes.moduleimpl_status",
|
"notes.moduleimpl_status",
|
||||||
@ -1123,16 +1125,15 @@ def edit_moduleimpl_resp(moduleimpl_id):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
sco_moduleimpl.do_moduleimpl_edit(
|
modimpl.responsable_id = responsable_id
|
||||||
{"moduleimpl_id": moduleimpl_id, "responsable_id": responsable_id},
|
db.session.add(modimpl)
|
||||||
formsemestre_id=sem["formsemestre_id"],
|
db.session.commit()
|
||||||
)
|
flash("Responsable modifié")
|
||||||
return flask.redirect(
|
return flask.redirect(
|
||||||
url_for(
|
url_for(
|
||||||
"notes.moduleimpl_status",
|
"notes.moduleimpl_status",
|
||||||
scodoc_dept=g.scodoc_dept,
|
scodoc_dept=g.scodoc_dept,
|
||||||
moduleimpl_id=moduleimpl_id,
|
moduleimpl_id=moduleimpl_id,
|
||||||
head_message="responsable modifié",
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- mode: python -*-
|
# -*- mode: python -*-
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
SCOVERSION = "9.4.39"
|
SCOVERSION = "9.4.40"
|
||||||
|
|
||||||
SCONAME = "ScoDoc"
|
SCONAME = "ScoDoc"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user