Update opolka/ScoDoc from ScoDoc/ScoDoc #2

Merged
opolka merged 1272 commits from ScoDoc/ScoDoc:master into master 2024-05-27 09:11:04 +02:00
31 changed files with 440 additions and 1637 deletions
Showing only changes of commit be39245e25 - Show all commits

View File

@ -374,115 +374,114 @@ def formsemestre_etudiants(
return sorted(etuds, key=itemgetter("sort_key"))
# retrait (temporaire ? à discuter)
# @bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
# @api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
# @login_required
# @scodoc
# @permission_required(Permission.ScoView)
# @as_json
# def etat_evals(formsemestre_id: int):
# """
# Informations sur l'état des évaluations d'un formsemestre.
@bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@api_web_bp.route("/formsemestre/<int:formsemestre_id>/etat_evals")
@login_required
@scodoc
@permission_required(Permission.ScoView)
@as_json
def etat_evals(formsemestre_id: int):
"""
Informations sur l'état des évaluations d'un formsemestre.
# formsemestre_id : l'id d'un semestre
formsemestre_id : l'id d'un semestre
# Exemple de résultat :
# [
# {
# "id": 1, // moduleimpl_id
# "titre": "Initiation aux réseaux informatiques",
# "evaluations": [
# {
# "id": 1,
# "description": null,
# "datetime_epreuve": null,
# "heure_fin": "09:00:00",
# "coefficient": "02.00"
# "is_complete": true,
# "nb_inscrits": 16,
# "nb_manquantes": 0,
# "ABS": 0,
# "ATT": 0,
# "EXC": 0,
# "saisie_notes": {
# "datetime_debut": "2021-09-11T00:00:00+02:00",
# "datetime_fin": "2022-08-25T00:00:00+02:00",
# "datetime_mediane": "2022-03-19T00:00:00+01:00"
# }
# },
# ...
# ]
# },
# ]
# """
# query = FormSemestre.query.filter_by(id=formsemestre_id)
# if g.scodoc_dept:
# query = query.filter_by(dept_id=g.scodoc_dept_id)
# formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
# app.set_sco_dept(formsemestre.departement.acronym)
# nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
Exemple de résultat :
[
{
"id": 1, // moduleimpl_id
"titre": "Initiation aux réseaux informatiques",
"evaluations": [
{
"id": 1,
"description": null,
"datetime_epreuve": null,
"heure_fin": "09:00:00",
"coefficient": "02.00"
"is_complete": true,
"nb_inscrits": 16,
"nb_manquantes": 0,
"ABS": 0,
"ATT": 0,
"EXC": 0,
"saisie_notes": {
"datetime_debut": "2021-09-11T00:00:00+02:00",
"datetime_fin": "2022-08-25T00:00:00+02:00",
"datetime_mediane": "2022-03-19T00:00:00+01:00"
}
},
...
]
},
]
"""
query = FormSemestre.query.filter_by(id=formsemestre_id)
if g.scodoc_dept:
query = query.filter_by(dept_id=g.scodoc_dept_id)
formsemestre: FormSemestre = query.first_or_404(formsemestre_id)
app.set_sco_dept(formsemestre.departement.acronym)
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
# result = []
# for modimpl_id in nt.modimpls_results:
# modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
# modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
# modimpl_dict = modimpl.to_dict(convert_objects=True)
result = []
for modimpl_id in nt.modimpls_results:
modimpl_results: ModuleImplResults = nt.modimpls_results[modimpl_id]
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(modimpl_id)
modimpl_dict = modimpl.to_dict(convert_objects=True, with_module=False)
# list_eval = []
# for evaluation_id in modimpl_results.evaluations_etat:
# eval_etat = modimpl_results.evaluations_etat[evaluation_id]
# evaluation = Evaluation.query.get_or_404(evaluation_id)
# eval_dict = evaluation.to_dict_api()
# eval_dict["etat"] = eval_etat.to_dict()
list_eval = []
for evaluation_id in modimpl_results.evaluations_etat:
eval_etat = modimpl_results.evaluations_etat[evaluation_id]
evaluation = Evaluation.query.get_or_404(evaluation_id)
eval_dict = evaluation.to_dict_api()
eval_dict["etat"] = eval_etat.to_dict()
# eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
# eval_dict["nb_notes_manquantes"] = len(
# modimpl_results.evals_etudids_sans_note[evaluation.id]
# )
# eval_dict["nb_notes_abs"] = sum(
# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE
# )
# eval_dict["nb_notes_att"] = eval_etat.nb_attente
# eval_dict["nb_notes_exc"] = sum(
# modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE
# )
eval_dict["nb_inscrits"] = modimpl_results.nb_inscrits_module
eval_dict["nb_notes_manquantes"] = len(
modimpl_results.evals_etudids_sans_note[evaluation.id]
)
eval_dict["nb_notes_abs"] = sum(
modimpl_results.evals_notes[evaluation.id] == scu.NOTES_ABSENCE
)
eval_dict["nb_notes_att"] = eval_etat.nb_attente
eval_dict["nb_notes_exc"] = sum(
modimpl_results.evals_notes[evaluation.id] == scu.NOTES_NEUTRALISE
)
# # Récupération de toutes les notes de l'évaluation
# # eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id)
# Récupération de toutes les notes de l'évaluation
# eval["notes"] = modimpl_results.get_eval_notes_dict(evaluation_id)
# notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all()
notes = NotesNotes.query.filter_by(evaluation_id=evaluation.id).all()
# date_debut = None
# date_fin = None
# date_mediane = None
date_debut = None
date_fin = None
date_mediane = None
# # Si il y a plus d'une note saisie pour l'évaluation
# if len(notes) >= 1:
# # Tri des notes en fonction de leurs dates
# notes_sorted = sorted(notes, key=attrgetter("date"))
# Si il y a plus d'une note saisie pour l'évaluation
if len(notes) >= 1:
# Tri des notes en fonction de leurs dates
notes_sorted = sorted(notes, key=attrgetter("date"))
# date_debut = notes_sorted[0].date
# date_fin = notes_sorted[-1].date
date_debut = notes_sorted[0].date
date_fin = notes_sorted[-1].date
# # Note médiane
# date_mediane = notes_sorted[len(notes_sorted) // 2].date
# Note médiane
date_mediane = notes_sorted[len(notes_sorted) // 2].date
# eval_dict["saisie_notes"] = {
# "datetime_debut": date_debut.isoformat()
# if date_debut is not None
# else None,
# "datetime_fin": date_fin.isoformat() if date_fin is not None else None,
# "datetime_mediane": date_mediane.isoformat()
# if date_mediane is not None
# else None,
# }
eval_dict["saisie_notes"] = {
"datetime_debut": date_debut.isoformat()
if date_debut is not None
else None,
"datetime_fin": date_fin.isoformat() if date_fin is not None else None,
"datetime_mediane": date_mediane.isoformat()
if date_mediane is not None
else None,
}
# list_eval.append(eval_dict)
list_eval.append(eval_dict)
# modimpl_dict["evaluations"] = list_eval
# result.append(modimpl_dict)
# return result
modimpl_dict["evaluations"] = list_eval
result.append(modimpl_dict)
return result
@bp.route("/formsemestre/<int:formsemestre_id>/resultats")

View File

@ -102,6 +102,8 @@ class User(UserMixin, ScoDocModel):
token = db.Column(db.Text(), index=True, unique=True)
token_expiration = db.Column(db.DateTime)
# Define the back reference from User to ModuleImpl
modimpls = db.relationship("ModuleImpl", back_populates="responsable")
roles = db.relationship("Role", secondary="user_role", viewonly=True)
Permission = Permission
@ -245,24 +247,26 @@ class User(UserMixin, ScoDocModel):
def to_dict(self, include_email=True):
"""l'utilisateur comme un dict, avec des champs supplémentaires"""
data = {
"date_expiration": self.date_expiration.isoformat() + "Z"
if self.date_expiration
else None,
"date_modif_passwd": self.date_modif_passwd.isoformat() + "Z"
"date_expiration": (
self.date_expiration.isoformat() + "Z" if self.date_expiration else None
),
"date_modif_passwd": (
self.date_modif_passwd.isoformat() + "Z"
if self.date_modif_passwd
else None,
"date_created": self.date_created.isoformat() + "Z"
if self.date_created
else None,
else None
),
"date_created": (
self.date_created.isoformat() + "Z" if self.date_created else None
),
"dept": self.dept,
"id": self.id,
"active": self.active,
"cas_id": self.cas_id,
"cas_allow_login": self.cas_allow_login,
"cas_allow_scodoc_login": self.cas_allow_scodoc_login,
"cas_last_login": self.cas_last_login.isoformat() + "Z"
if self.cas_last_login
else None,
"cas_last_login": (
self.cas_last_login.isoformat() + "Z" if self.cas_last_login else None
),
"edt_id": self.edt_id,
"status_txt": "actif" if self.active else "fermé",
"last_seen": self.last_seen.isoformat() + "Z" if self.last_seen else None,
@ -477,8 +481,8 @@ class User(UserMixin, ScoDocModel):
return f"{nom} {scu.format_prenom(self.prenom)} ({self.user_name})"
@staticmethod
def get_user_id_from_nomplogin(nomplogin: str) -> Optional[int]:
"""Returns id from the string "Dupont Pierre (dupont)"
def get_user_from_nomplogin(nomplogin: str) -> Optional["User"]:
"""Returns User instance from the string "Dupont Pierre (dupont)"
or None if user does not exist
"""
match = re.match(r".*\((.*)\)", nomplogin.strip())
@ -486,7 +490,7 @@ class User(UserMixin, ScoDocModel):
user_name = match.group(1)
u = User.query.filter_by(user_name=user_name).first()
if u:
return u.id
return u
return None
def get_nom_fmt(self):

View File

@ -35,7 +35,6 @@ from app.decorators import (
permission_required,
)
from app.models import FormSemestre, FormSemestreInscription, Identite
from app.scodoc import sco_bulletins_pdf
from app.scodoc.codes_cursus import UE_STANDARD
from app.scodoc.sco_exceptions import ScoNoReferentielCompetences, ScoValueError
from app.scodoc.sco_logos import find_logo

View File

@ -194,7 +194,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
"""Génère la partie "titre" du bulletin de notes.
Renvoie une liste d'objets platypus
"""
# comme les bulletins standard, mais avec notre préférence
# comme les bulletins standards, mais avec notre préférence
return super().bul_title_pdf(preference_field=preference_field)
def bul_part_below(self, fmt="pdf") -> list:
@ -406,6 +406,8 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
def boite_identite(self) -> list:
"Les informations sur l'identité et l'inscription de l'étudiant"
parcour = self.formsemestre.etuds_inscriptions[self.etud.id].parcour
return [
Paragraph(
SU(f"""{self.etud.nomprenom}"""),
@ -416,6 +418,7 @@ class BulletinGeneratorBUTCourt(BulletinGeneratorStandard):
f"""
<b>{self.bul["demission"]}</b><br/>
Formation: {self.formsemestre.titre_num()}<br/>
{'Parcours ' + parcour.code + '<br/>' if parcour else ''}
Année universitaire: {self.formsemestre.annee_scolaire_str()}<br/>
"""
),

View File

@ -97,7 +97,7 @@ def show_etud(deca: DecisionsProposeesAnnee, read_only: bool = True) -> str:
<span class="avertissement_redoublement">{formsemestre_2.annee_scolaire_str()
if formsemestre_2 else ""}</span>
</div>
<div class="titre">RCUE</div>
<div class="titre" title="Décisions sur RCUEs enregistrées sur l'ensemble du cursus">RCUE</div>
"""
)
for dec_rcue in deca.get_decisions_rcues_annee():

View File

@ -178,19 +178,25 @@ class ModuleImplResults:
eval_notes_inscr = evals_notes[str(evaluation.id)][list(inscrits_module)]
# Nombre de notes (non vides, incluant ATT etc) des inscrits:
nb_notes = eval_notes_inscr.notna().sum()
# Etudiants avec notes en attente:
# = ceux avec note ATT
eval_etudids_attente = set(
eval_notes_inscr.iloc[
(eval_notes_inscr == scu.NOTES_ATTENTE).to_numpy()
].index
)
if evaluation.publish_incomplete:
# et en "imédiat", tous ceux sans note
eval_etudids_attente |= etudids_sans_note
# Synthèse pour état du module:
self.etudids_attente |= eval_etudids_attente
self.evaluations_etat[evaluation.id] = EvaluationEtat(
evaluation_id=evaluation.id,
nb_attente=len(eval_etudids_attente),
nb_notes=nb_notes,
nb_notes=int(nb_notes),
is_complete=is_complete,
)
# au moins une note en ATT dans ce modimpl:
# au moins une note en attente (ATT ou manquante en mode "immédiat") dans ce modimpl:
self.en_attente = bool(self.etudids_attente)
# Force columns names to integers (evaluation ids)

View File

@ -52,7 +52,7 @@ class ScoDocModel(db.Model):
def create_from_dict(cls, data: dict) -> "ScoDocModel":
"""Create a new instance of the model with attributes given in dict.
The instance is added to the session (but not flushed nor committed).
Use only relevant arributes for the given model and ignore others.
Use only relevant attributes for the given model and ignore others.
"""
if data:
args = cls.convert_dict_fields(cls.filter_model_attributes(data))

View File

@ -445,10 +445,11 @@ class Identite(models.ScoDocModel):
"prenom_etat_civil": self.prenom_etat_civil,
}
def to_dict_scodoc7(self, restrict=False) -> dict:
def to_dict_scodoc7(self, restrict=False, with_inscriptions=False) -> dict:
"""Représentation dictionnaire,
compatible ScoDoc7 mais sans infos admission.
Si restrict, cache les infos "personnelles" si pas permission ViewEtudData
Si with_inscriptions, inclut les champs "inscription"
"""
e_dict = self.__dict__.copy() # dict(self.__dict__)
e_dict.pop("_sa_instance_state", None)
@ -460,6 +461,8 @@ class Identite(models.ScoDocModel):
adresse = self.adresses.first()
if adresse:
e_dict.update(adresse.to_dict(restrict=restrict))
if with_inscriptions:
e_dict.update(self.inscription_descr())
return {k: v or "" for k, v in e_dict.items()} # convert_null_outputs_to_empty
def to_dict_bul(self, include_urls=True):
@ -574,7 +577,9 @@ class Identite(models.ScoDocModel):
return r[0] if r else None
def inscription_descr(self) -> dict:
"""Description de l'état d'inscription"""
"""Description de l'état d'inscription
avec champs compatibles templates ScoDoc7
"""
inscription_courante = self.inscription_courante()
if inscription_courante:
titre_sem = inscription_courante.formsemestre.titre_mois()
@ -585,7 +590,7 @@ class Identite(models.ScoDocModel):
else:
inscr_txt = "Inscrit en"
return {
result = {
"etat_in_cursem": inscription_courante.etat,
"inscription_courante": inscription_courante,
"inscription": titre_sem,
@ -608,15 +613,20 @@ class Identite(models.ScoDocModel):
inscription = "ancien"
situation = "ancien élève"
else:
inscription = ("non inscrit",)
inscription = "non inscrit"
situation = inscription
return {
result = {
"etat_in_cursem": "?",
"inscription_courante": None,
"inscription": inscription,
"inscription_str": inscription,
"situation": situation,
}
# aliases pour compat templates ScoDoc7
result["etatincursem"] = result["etat_in_cursem"]
result["inscriptionstr"] = result["inscription_str"]
return result
def inscription_etat(self, formsemestre_id: int) -> str:
"""État de l'inscription de cet étudiant au semestre:
@ -749,9 +759,7 @@ def check_etud_duplicate_code(args, code_name, edit=True):
).all()
duplicate = False
if edit:
duplicate = (len(etuds) > 1) or (
(len(etuds) == 1) and etuds[0].id != args["etudid"]
)
duplicate = (len(etuds) > 1) or ((len(etuds) == 1) and etuds[0].id != etudid)
else:
duplicate = len(etuds) > 0
if duplicate:

View File

@ -5,7 +5,7 @@
import datetime
from operator import attrgetter
from flask import g, url_for
from flask import abort, g, url_for
from flask_login import current_user
import sqlalchemy as sa
@ -241,6 +241,25 @@ class Evaluation(db.Model):
if k != "_sa_instance_state" and k != "id" and k in data:
setattr(self, k, data[k])
@classmethod
def get_evaluation(
cls, evaluation_id: int | str, dept_id: int = None
) -> "Evaluation":
"""Evaluation ou 404, cherche uniquement dans le département spécifié ou le courant."""
from app.models import FormSemestre, ModuleImpl
if not isinstance(evaluation_id, int):
try:
evaluation_id = int(evaluation_id)
except (TypeError, ValueError):
abort(404, "evaluation_id invalide")
if g.scodoc_dept:
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
query = cls.query.filter_by(id=evaluation_id)
if dept_id is not None:
query = query.join(ModuleImpl).join(FormSemestre).filter_by(dept_id=dept_id)
return query.first_or_404()
@classmethod
def get_max_numero(cls, moduleimpl_id: int) -> int:
"""Return max numero among evaluations in this
@ -265,7 +284,9 @@ class Evaluation(db.Model):
evaluations = moduleimpl.evaluations.order_by(
Evaluation.date_debut, Evaluation.numero
).all()
all_numbered = all(e.numero is not None for e in evaluations)
numeros_distincts = {e.numero for e in evaluations if e.numero is not None}
# pas de None, pas de dupliqués
all_numbered = len(numeros_distincts) == len(evaluations)
if all_numbered and only_if_unumbered:
return # all ok

View File

@ -3,12 +3,13 @@
"""
import pandas as pd
from flask import abort, g
from flask_login import current_user
from flask_sqlalchemy.query import Query
from app import db
from app.auth.models import User
from app.comp import df_cache
from app.models import APO_CODE_STR_LEN
from app.models import APO_CODE_STR_LEN, ScoDocModel
from app.models.etudiants import Identite
from app.models.evaluations import Evaluation
from app.models.modules import Module
@ -17,7 +18,7 @@ from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_utils as scu
class ModuleImpl(db.Model):
class ModuleImpl(ScoDocModel):
"""Mise en oeuvre d'un module pour une annee/semestre"""
__tablename__ = "notes_moduleimpl"
@ -36,7 +37,10 @@ class ModuleImpl(db.Model):
index=True,
nullable=False,
)
responsable_id = db.Column("responsable_id", db.Integer, db.ForeignKey("user.id"))
responsable_id = db.Column(
"responsable_id", db.Integer, db.ForeignKey("user.id", ondelete="SET NULL")
)
responsable = db.relationship("User", back_populates="modimpls")
# formule de calcul moyenne:
computation_expr = db.Column(db.Text())
@ -52,8 +56,8 @@ class ModuleImpl(db.Model):
secondary="notes_modules_enseignants",
lazy="dynamic",
backref="moduleimpl",
viewonly=True,
)
"enseignants du module (sans le responsable)"
def __repr__(self):
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
@ -85,7 +89,7 @@ class ModuleImpl(db.Model):
@classmethod
def get_modimpl(cls, moduleimpl_id: int | str, dept_id: int = None) -> "ModuleImpl":
"""FormSemestre ou 404, cherche uniquement dans le département spécifié ou le courant."""
"""ModuleImpl ou 404, cherche uniquement dans le département spécifié ou le courant."""
from app.models.formsemestre import FormSemestre
if not isinstance(moduleimpl_id, int):
@ -187,7 +191,7 @@ class ModuleImpl(db.Model):
return allow_ens and user.id in (ens.id for ens in self.enseignants)
return True
def can_change_ens_by(self, user: User, raise_exc=False) -> bool:
def can_change_responsable(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)
@ -208,6 +212,27 @@ class ModuleImpl(db.Model):
raise AccessDenied(f"Modification impossible pour {user}")
return False
def can_change_ens(self, user: User | None = None, raise_exc=True) -> bool:
"""check if user can modify ens list (raise exception if not)"
if user is None, current user.
"""
user = current_user if user is None else user
if not self.formsemestre.etat:
if raise_exc:
raise ScoLockedSemError("Modification impossible: semestre verrouille")
return False
# -- check access
# admin, resp. module ou resp. semestre
if (
user.id != self.responsable_id
and not user.has_permission(Permission.EditFormSemestre)
and user.id not in (u.id for u in self.formsemestre.responsables)
):
if raise_exc:
raise AccessDenied(f"Modification impossible pour {user}")
return False
return True
def est_inscrit(self, etud: Identite) -> bool:
"""
Vérifie si l'étudiant est bien inscrit au moduleimpl (même si DEM ou DEF au semestre).

View File

@ -33,7 +33,7 @@ from flask import g, request
from flask_login import current_user
from app import db
from app.models import Evaluation, GroupDescr, ModuleImpl, Partition
from app.models import Evaluation, GroupDescr, Identite, ModuleImpl, Partition
import app.scodoc.sco_utils as scu
from app.scodoc import sco_preferences
from app.scodoc.sco_permissions import Permission
@ -160,26 +160,31 @@ def sidebar(etudid: int = None):
etudid = request.form.get("etudid", None)
if etudid is not None:
etud = sco_etud.get_etud_info(filled=True, etudid=etudid)[0]
params.update(etud)
params["fiche_url"] = url_for(
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
)
etud = Identite.get_etud(etudid)
# compte les absences du semestre en cours
H.append(
"""<h2 id="insidebar-etud"><a href="%(fiche_url)s" class="sidebar">
<font color="#FF0000">%(civilite_str)s %(nom_disp)s</font></a>
f"""<h2 id="insidebar-etud"><a href="{
url_for(
"scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid
)
}" class="sidebar">
<font color="#FF0000">{etud.civilite_str} {etud.nom_disp()}</font></a>
</h2>
<b>Absences</b>"""
% params
)
if etud["cursem"]:
cur_sem = etud["cursem"]
nbabs, nbabsjust = sco_assiduites.get_assiduites_count(etudid, cur_sem)
inscription = etud.inscription_courante()
if inscription:
formsemestre = inscription.formsemestre
nbabs, nbabsjust = sco_assiduites.formsemestre_get_assiduites_count(
etudid, formsemestre
)
nbabsnj = nbabs - nbabsjust
H.append(
f"""<span title="absences du { cur_sem["date_debut"] } au {
cur_sem["date_fin"] }">({
f"""<span title="absences du {
formsemestre.date_debut.strftime("%d/%m/%Y")
} au {
formsemestre.date_fin.strftime("%d/%m/%Y")
}">({
sco_preferences.get_preference("assi_metrique", None)})
<br>{ nbabsjust } J., { nbabsnj } N.J.</span>"""
)
@ -189,10 +194,12 @@ def sidebar(etudid: int = None):
cur_formsemestre_id = retreive_formsemestre_from_request()
H.append(
f"""
<li><a href="{ url_for('assiduites.ajout_assiduite_etud',
<li><a href="{
url_for('assiduites.ajout_assiduite_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Ajouter</a></li>
<li><a href="{ url_for('assiduites.ajout_justificatif_etud',
<li><a href="{
url_for('assiduites.ajout_justificatif_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid,
formsemestre_id=cur_formsemestre_id,
)
@ -201,7 +208,8 @@ def sidebar(etudid: int = None):
)
if sco_preferences.get_preference("handle_billets_abs"):
H.append(
f"""<li><a href="{ url_for('absences.billets_etud',
f"""<li><a href="{
url_for('absences.billets_etud',
scodoc_dept=g.scodoc_dept, etudid=etudid)
}">Billets</a></li>"""
)

File diff suppressed because it is too large Load Diff

View File

@ -268,10 +268,12 @@ def abs_notification_message(
"""
from app.scodoc import sco_bulletins
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
etud = Identite.get_etud(etudid)
# Variables accessibles dans les balises du template: %(nom_variable)s :
values = sco_bulletins.make_context_dict(formsemestre, etud)
values = sco_bulletins.make_context_dict(
formsemestre, etud.to_dict_scodoc7(with_inscriptions=True)
)
values["nbabs"] = nbabs
values["nbabsjust"] = nbabsjust
@ -287,7 +289,7 @@ def abs_notification_message(
log("abs_notification_message: empty template, not sending message")
return None
subject = f"""[ScoDoc] Trop d'absences pour {etud["nomprenom"]}"""
subject = f"""[ScoDoc] Trop d'absences pour {etud.nomprenom}"""
msg = Message(subject, sender=email.get_from_addr(formsemestre.departement.acronym))
msg.body = txt
return msg

View File

@ -138,21 +138,18 @@ def etud_upload_file_form(etudid):
"""Page with a form to choose and upload a file, with a description."""
# check permission
if not can_edit_etud_archive(current_user):
raise AccessDenied("opération non autorisée pour %s" % current_user)
etuds = sco_etud.get_etud_info(filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
raise AccessDenied(f"opération non autorisée pour {current_user}")
etud = Identite.get_etud(etudid)
H = [
html_sco_header.sco_header(
page_title="Chargement d'un document associé à %(nomprenom)s" % etud,
page_title=f"Chargement d'un document associé à {etud.nomprenom}",
),
"""<h2>Chargement d'un document associé à %(nomprenom)s</h2>
"""
% etud,
"""<p>Le fichier ne doit pas dépasser %sMo.</p>
"""
% (scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)),
f"""<h2>Chargement d'un document associé à {etud.nomprenom}</h2>
<p>Le fichier ne doit pas dépasser {
scu.CONFIG.ETUD_MAX_FILE_SIZE // (1024 * 1024)}Mo.</p>
""",
]
tf = TrivialFormulator(
request.base_url,
@ -176,20 +173,13 @@ def etud_upload_file_form(etudid):
if tf[0] == 0:
return "\n".join(H) + tf[1] + html_sco_header.sco_footer()
elif tf[0] == -1:
return flask.redirect(
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
else:
return flask.redirect(etud.url_fiche())
data = tf[2]["datafile"].read()
descr = tf[2]["description"]
filename = tf[2]["datafile"].filename
etud_archive_id = etud["etudid"]
_store_etud_file_to_new_archive(
etud_archive_id, data, filename, description=descr
)
return flask.redirect(
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
etud_archive_id = (etudid,)
_store_etud_file_to_new_archive(etud_archive_id, data, filename, description=descr)
return flask.redirect(etud.url_fiche())
def _store_etud_file_to_new_archive(
@ -209,23 +199,20 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
# check permission
if not can_edit_etud_archive(current_user):
raise AccessDenied(f"opération non autorisée pour {current_user}")
etuds = sco_etud.get_etud_info(filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["etudid"]
etud = Identite.get_etud(etudid)
etud_archive_id = etudid
archive_id = ETUDS_ARCHIVER.get_id_from_name(
etud_archive_id, archive_name, dept_id=etud["dept_id"]
etud_archive_id, archive_name, dept_id=etud.dept_id
)
if not dialog_confirmed:
return scu.confirm_dialog(
"""<h2>Confirmer la suppression des fichiers ?</h2>
<p>Fichier associé le %s à l'étudiant %s</p>
<p>La suppression sera définitive.</p>"""
% (
ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M"),
etud["nomprenom"],
),
f"""<h2>Confirmer la suppression des fichiers ?</h2>
<p>Fichier associé le {
ETUDS_ARCHIVER.get_archive_date(archive_id).strftime("%d/%m/%Y %H:%M")
} à l'étudiant {etud.nomprenom}
</p>
<p>La suppression sera définitive.</p>
""",
dest_url="",
cancel_url=url_for(
"scolar.fiche_etud",
@ -236,22 +223,17 @@ def etud_delete_archive(etudid, archive_name, dialog_confirmed=False):
parameters={"etudid": etudid, "archive_name": archive_name},
)
ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud["dept_id"])
ETUDS_ARCHIVER.delete_archive(archive_id, dept_id=etud.dept_id)
flash("Archive supprimée")
return flask.redirect(
url_for("scolar.fiche_etud", scodoc_dept=g.scodoc_dept, etudid=etudid)
)
return flask.redirect(etud.url_fiche())
def etud_get_archived_file(etudid, archive_name, filename):
"""Send file to client."""
etuds = sco_etud.get_etud_info(etudid=etudid, filled=True)
if not etuds:
raise ScoValueError("étudiant inexistant")
etud = etuds[0]
etud_archive_id = etud["etudid"]
etud = Identite.get_etud(etudid)
etud_archive_id = etud.id
return ETUDS_ARCHIVER.get_archived_file(
etud_archive_id, archive_name, filename, dept_id=etud["dept_id"]
etud_archive_id, archive_name, filename, dept_id=etud.dept_id
)

View File

@ -735,17 +735,18 @@ def fill_etuds_info(etuds: list[dict], add_admission=True):
etud["nomlycee"] = etud.get("nomlycee", "") or ""
# voir Identite.inscription_descr et Identite.to_dict_scodoc7(with_inscriptions=True)
def etud_inscriptions_infos(etudid: int, ne="") -> dict:
"""Dict avec les informations sur les semestres passés et courant.
{
"sems" : , # trie les semestres par date de debut, le plus recent d'abord
"ins" : ,
"cursem" : ,
"inscription" : ,
"inscriptionstr" : ,
"inscription_formsemestre_id" : ,
"etatincursem" : ,
"situation" : ,
"inscription" : , # cursem["titremois"]
"inscriptionstr" : , # "Inscrit en " + cursem["titremois"]
"inscription_formsemestre_id" : , # cursem["formsemestre_id"]
"etatincursem" : , # curi["etat"]
"situation" : , # descr_situation_etud(etudid, ne)
}
"""
from app.scodoc import sco_formsemestre

View File

@ -162,50 +162,48 @@ def do_evaluation_get_all_notes(
return d
def moduleimpl_evaluation_move(evaluation_id: int, after=0, redirect=1):
def moduleimpl_evaluation_move(evaluation_id: int, after=0):
"""Move before/after previous one (decrement/increment numero)
(published)
"""
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
redirect = int(redirect)
evaluation = Evaluation.get_evaluation(evaluation_id)
# access: can change eval ?
if not evaluation.moduleimpl.can_edit_evaluation(current_user):
raise AccessDenied(
f"Modification évaluation impossible pour {current_user.get_nomplogin()}"
)
Evaluation.moduleimpl_evaluation_renumber(
evaluation.moduleimpl, only_if_unumbered=True
)
e = get_evaluations_dict(args={"evaluation_id": evaluation_id})[0]
after = int(after) # 0: deplace avant, 1 deplace apres
if after not in (0, 1):
raise ValueError('invalid value for "after"')
mod_evals = get_evaluations_dict({"moduleimpl_id": e["moduleimpl_id"]})
if len(mod_evals) > 1:
idx = [p["evaluation_id"] for p in mod_evals].index(evaluation_id)
Evaluation.moduleimpl_evaluation_renumber(
evaluation.moduleimpl, only_if_unumbered=True
)
mod_evaluations = evaluation.moduleimpl.evaluations.all()
if len(mod_evaluations) > 1:
idx = [e.id for e in mod_evaluations].index(evaluation.id)
neigh = None # object to swap with
if after == 0 and idx > 0:
neigh = mod_evals[idx - 1]
elif after == 1 and idx < len(mod_evals) - 1:
neigh = mod_evals[idx + 1]
neigh = mod_evaluations[idx - 1]
elif after == 1 and idx < len(mod_evaluations) - 1:
neigh = mod_evaluations[idx + 1]
if neigh: #
if neigh["numero"] == e["numero"]:
if neigh.numero == evaluation.numero:
log("Warning: moduleimpl_evaluation_move: forcing renumber")
Evaluation.moduleimpl_evaluation_renumber(
evaluation.moduleimpl, only_if_unumbered=False
)
else:
# swap numero with neighbor
e["numero"], neigh["numero"] = neigh["numero"], e["numero"]
do_evaluation_edit(e)
do_evaluation_edit(neigh)
evaluation.numero, neigh.numero = neigh.numero, evaluation.numero
db.session.add(evaluation)
db.session.add(neigh)
db.session.commit()
# redirect to moduleimpl page:
if redirect:
return flask.redirect(
url_for(
"notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept,
moduleimpl_id=e["moduleimpl_id"],
moduleimpl_id=evaluation.moduleimpl.id,
)
)

View File

@ -37,7 +37,7 @@ from flask_login import current_user
from flask import request
from app import db
from app.models import Evaluation, FormSemestre, ModuleImpl
from app.models import Evaluation, Module, ModuleImpl
from app.models.evaluations import heure_to_time
import app.scodoc.sco_utils as scu
@ -47,7 +47,6 @@ from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
from app.scodoc import sco_evaluations
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_preferences
@ -58,27 +57,20 @@ def evaluation_create_form(
page_title="Évaluation",
):
"Formulaire création/édition d'une évaluation (pas de ses notes)"
evaluation: Evaluation
if evaluation_id is not None:
evaluation: Evaluation = db.session.get(Evaluation, evaluation_id)
evaluation = db.session.get(Evaluation, evaluation_id)
if evaluation is None:
raise ScoValueError("Cette évaluation n'existe pas ou plus !")
moduleimpl_id = evaluation.moduleimpl_id
#
modimpl: ModuleImpl = (
ModuleImpl.query.filter_by(id=moduleimpl_id)
.join(FormSemestre)
.filter_by(dept_id=g.scodoc_dept_id)
.first_or_404()
)
modimpl_o = sco_moduleimpl.moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[
0
]
mod = modimpl_o["module"]
formsemestre_id = modimpl_o["formsemestre_id"]
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
formsemestre_id = modimpl.formsemestre_id
formsemestre = modimpl.formsemestre
module: Module = modimpl.module
sem_ues = formsemestre.get_ues(with_sport=False)
is_malus = mod["module_type"] == ModuleType.MALUS
is_apc = mod["module_type"] in (ModuleType.RESSOURCE, ModuleType.SAE)
is_malus = module.module_type == ModuleType.MALUS
is_apc = module.module_type in (ModuleType.RESSOURCE, ModuleType.SAE)
preferences = sco_preferences.SemPreferences(formsemestre.id)
can_edit_poids = not preferences["but_disable_edit_poids_evaluations"]
min_note_max = scu.NOTES_PRECISION # le plus petit bareme possible
@ -98,9 +90,11 @@ def evaluation_create_form(
# création nouvel
if moduleimpl_id is None:
raise ValueError("missing moduleimpl_id parameter")
numeros = [(e.numero or 0) for e in modimpl.evaluations]
initvalues = {
"note_max": 20,
"jour": time.strftime("%d/%m/%Y", time.localtime()),
"note_max": 20,
"numero": (max(numeros) + 1) if numeros else 0,
"publish_incomplete": is_malus,
"visibulletin": 1,
}
@ -128,18 +122,7 @@ def evaluation_create_form(
min_note_max_str = scu.fmt_note(min_note_max)
else:
min_note_max_str = "0"
#
H = [
f"""<h3>{action} en
{scu.MODULE_TYPE_NAMES[mod["module_type"]]} <a class="stdlink" href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}">{mod["code"] or "module sans code"} {mod["titre"]}</a> {link}</h3>
"""
]
heures = [f"{h:02d}h{m:02d}" for h in range(8, 19) for m in (0, 30)]
#
initvalues["coefficient"] = initvalues.get("coefficient", 1.0)
vals = scu.get_request_args()
#
@ -164,6 +147,7 @@ def evaluation_create_form(
("evaluation_id", {"default": evaluation_id, "input_type": "hidden"}),
("formsemestre_id", {"default": formsemestre_id, "input_type": "hidden"}),
("moduleimpl_id", {"default": moduleimpl_id, "input_type": "hidden"}),
("numero", {"default": initvalues["numero"], "input_type": "hidden"}),
(
"jour",
{
@ -323,6 +307,16 @@ def evaluation_create_form(
dest_url = url_for(
"notes.moduleimpl_status", scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id
)
H = [
f"""<h3>{action} en
{scu.MODULE_TYPE_NAMES[module.module_type]} <a class="stdlink" href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}">{module.code or "module sans code"} {
module.titre or module.abbrev or "(sans titre)"
}</a> {link}</h3>
"""
]
if tf[0] == 0:
head = html_sco_header.sco_header(page_title=page_title)
return (

View File

@ -280,18 +280,21 @@ def do_evaluation_etat(
}
def _summarize_evals_etats(evals: list[dict]) -> dict:
def _summarize_evals_etats(etat_evals: list[dict]) -> dict:
"""Synthétise les états d'une liste d'évaluations
evals: list of mappings (etats),
utilise e["etat"]["evalcomplete"], e["etat"]["nb_notes"], e["etat"]["last_modif"]
-> nb_eval_completes, nb_evals_en_cours,
nb_evals_vides, date derniere modif
->
nb_eval_completes (= prises en compte)
nb_evals_en_cours (= avec des notes, mais pas complete)
nb_evals_vides (= sans aucune note)
date derniere modif
Une eval est "complete" ssi tous les etudiants *inscrits* ont une note.
"""
nb_evals_completes, nb_evals_en_cours, nb_evals_vides = 0, 0, 0
dates = []
for e in evals:
for e in etat_evals:
if e["etat"]["evalcomplete"]:
nb_evals_completes += 1
elif e["etat"]["nb_notes"] == 0:
@ -345,8 +348,8 @@ def do_evaluation_etat_in_sem(formsemestre: FormSemestre) -> dict:
def do_evaluation_etat_in_mod(nt, modimpl: ModuleImpl):
"""état des évaluations dans ce module"""
evals = nt.get_mod_evaluation_etat_list(modimpl)
etat = _summarize_evals_etats(evals)
etat_evals = nt.get_mod_evaluation_etat_list(modimpl)
etat = _summarize_evals_etats(etat_evals)
# Il y a-t-il des notes en attente dans ce module ?
etat["attente"] = nt.modimpls_results[modimpl.id].en_attente
return etat

View File

@ -241,7 +241,7 @@ def do_formsemestre_create(args, silent=False):
write_formsemestre_etapes(args)
if args["responsables"]:
args["formsemestre_id"] = formsemestre_id
write_formsemestre_responsables(args)
_write_formsemestre_responsables(args)
# create default partition
partition_id = sco_groups.partition_create(
@ -275,7 +275,7 @@ def do_formsemestre_edit(sem, cnx=None, **kw):
_formsemestreEditor.edit(cnx, sem, **kw)
write_formsemestre_etapes(sem)
write_formsemestre_responsables(sem)
_write_formsemestre_responsables(sem)
sco_cache.invalidate_formsemestre(
formsemestre_id=sem["formsemestre_id"]
@ -296,8 +296,12 @@ def read_formsemestre_responsables(formsemestre_id: int) -> list[int]: # py3.9+
return [x["responsable_id"] for x in r]
def write_formsemestre_responsables(sem):
return _write_formsemestre_aux(sem, "responsables", "responsable_id")
def _write_formsemestre_responsables(sem): # TODO old, à ré-écrire avec models
if sem and "responsables" in sem:
sem["responsables"] = [
uid for uid in sem["responsables"] if (uid is not None) and (uid != -1)
]
_write_formsemestre_aux(sem, "responsables", "responsable_id")
# ---------------------- Coefs des UE
@ -362,10 +366,11 @@ def read_formsemestre_etapes(formsemestre_id): # OBSOLETE
return [ApoEtapeVDI(x["etape_apo"]) for x in r if x["etape_apo"]]
def write_formsemestre_etapes(sem):
def write_formsemestre_etapes(sem): # TODO old, à ré-écrire avec models
return _write_formsemestre_aux(sem, "etapes", "etape_apo")
# TODO old, à ré-écrire avec models
def _write_formsemestre_aux(sem, fieldname, valuename):
"""fieldname: 'etapes' ou 'responsables'
valuename: 'etape_apo' ou 'responsable_id'

View File

@ -857,17 +857,20 @@ def do_formsemestre_createwithmodules(edit=False, formsemestre: FormSemestre = N
tf[2]["bul_hide_xml"] = True
# remap les identifiants de responsables:
for field in resp_fields:
tf[2][field] = User.get_user_id_from_nomplogin(tf[2][field])
resp = User.get_user_from_nomplogin(tf[2][field])
tf[2][field] = resp.id if resp else -1
tf[2]["responsables"] = []
for field in resp_fields:
if tf[2][field]:
tf[2]["responsables"].append(tf[2][field])
for module_id in tf[2]["tf-checked"]:
mod_resp_id = User.get_user_id_from_nomplogin(tf[2][module_id])
if mod_resp_id is None:
mod_resp = User.get_user_from_nomplogin(tf[2][module_id])
if mod_resp is None:
# Si un module n'a pas de responsable (ou inconnu),
# l'affecte au 1er directeur des etudes:
mod_resp_id = tf[2]["responsable_id"]
else:
mod_resp_id = mod_resp.id
tf[2][module_id] = mod_resp_id
# etapes:
@ -1227,9 +1230,12 @@ def formsemestre_clone(formsemestre_id):
"formsemestre_status?formsemestre_id=%s" % formsemestre_id
)
else:
resp = User.get_user_from_nomplogin(tf[2]["responsable_id"])
if not resp:
raise ScoValueError("id responsable invalide")
new_formsemestre_id = do_formsemestre_clone(
formsemestre_id,
User.get_user_id_from_nomplogin(tf[2]["responsable_id"]),
resp.id,
tf[2]["date_debut"],
tf[2]["date_fin"],
clone_evaluations=tf[2]["clone_evaluations"],
@ -1273,29 +1279,24 @@ def do_formsemestre_clone(
log(f"created formsemestre {formsemestre_id}")
formsemestre: FormSemestre = db.session.get(FormSemestre, formsemestre_id)
# 2- create moduleimpls
mods_orig = sco_moduleimpl.moduleimpl_list(formsemestre_id=orig_formsemestre_id)
for mod_orig in mods_orig:
args = mod_orig.copy()
modimpl_orig: ModuleImpl
for modimpl_orig in formsemestre_orig.modimpls:
args = modimpl_orig.to_dict(with_module=False)
args["formsemestre_id"] = formsemestre_id
mid = sco_moduleimpl.do_moduleimpl_create(args)
# copy notes_modules_enseignants
ens = sco_moduleimpl.do_ens_list(
args={"moduleimpl_id": mod_orig["moduleimpl_id"]}
)
for e in ens:
args = e.copy()
args["moduleimpl_id"] = mid
sco_moduleimpl.do_ens_create(args)
modimpl_new = ModuleImpl.create_from_dict(args)
db.session.flush()
# copy enseignants
for ens in modimpl_orig.enseignants:
modimpl_new.enseignants.append(ens)
db.session.add(modimpl_new)
# optionally, copy evaluations
if clone_evaluations:
for e in Evaluation.query.filter_by(
moduleimpl_id=mod_orig["moduleimpl_id"]
):
for e in Evaluation.query.filter_by(moduleimpl_id=modimpl_orig.id):
# copie en enlevant la date
new_eval = e.clone(
not_copying=("date_debut", "date_fin", "moduleimpl_id")
)
new_eval.moduleimpl_id = mid
new_eval.moduleimpl_id = modimpl_new.id
# Copie les poids APC de l'évaluation
new_eval.set_ue_poids_dict(e.get_ue_poids_dict())
db.session.commit()

View File

@ -1262,6 +1262,7 @@ def formsemestre_tableau_modules(
etat["nb_evals_completes"] > 0
and etat["nb_evals_en_cours"] == 0
and etat["nb_evals_vides"] == 0
and not etat["attente"]
):
H.append(f'<tr class="formsemestre_status_green{fontorange}">')
else:
@ -1315,6 +1316,7 @@ def formsemestre_tableau_modules(
if nb_evals != 0:
H.append(
f"""<a href="{moduleimpl_status_url}"
title="les évaluations 'ok' sont celles prises en compte dans les calculs"
class="formsemestre_status_link">{nb_evals} prévues,
{etat["nb_evals_completes"]} ok</a>"""
)

View File

@ -25,22 +25,18 @@
#
##############################################################################
"""Fonctions sur les moduleimpl
"""Fonctions sur les moduleimpl (legacy: use models.moduleimpls instead)
"""
from flask_login import current_user
import psycopg2
from app import db
from app.models import Formation
from app.scodoc import scolog
from app.scodoc import sco_formsemestre
from app.scodoc import sco_cache
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, AccessDenied
from app.scodoc.sco_exceptions import ScoValueError
# --- Gestion des "Implémentations de Modules"
# Un "moduleimpl" correspond a la mise en oeuvre d'un module
@ -57,15 +53,6 @@ _moduleimplEditor = ndb.EditableTable(
),
)
_modules_enseignantsEditor = ndb.EditableTable(
"notes_modules_enseignants",
None, # pas d'id dans cette Table d'association
(
"moduleimpl_id", # associe moduleimpl
"ens_id", # a l'id de l'enseignant (User.id)
),
)
def do_moduleimpl_create(args):
"create a moduleimpl"
@ -108,9 +95,6 @@ def moduleimpl_list(moduleimpl_id=None, formsemestre_id=None, module_id=None):
args = locals()
cnx = ndb.GetDBConnexion()
modimpls = _moduleimplEditor.list(cnx, args)
# Ajoute la liste des enseignants
for mo in modimpls:
mo["ens"] = do_ens_list(args={"moduleimpl_id": mo["moduleimpl_id"]})
return modimpls
@ -342,65 +326,3 @@ def do_moduleimpl_inscrit_etuds(moduleimpl_id, formsemestre_id, etudids, reset=F
sco_cache.invalidate_formsemestre(
formsemestre_id=formsemestre_id
) # > moduleimpl_inscrit_etuds
def do_ens_list(*args, **kw):
"liste les enseignants d'un moduleimpl (pas le responsable)"
cnx = ndb.GetDBConnexion()
ens = _modules_enseignantsEditor.list(cnx, *args, **kw)
return ens
def do_ens_edit(*args, **kw):
"edit ens"
cnx = ndb.GetDBConnexion()
_modules_enseignantsEditor.edit(cnx, *args, **kw)
def do_ens_create(args):
"create ens"
cnx = ndb.GetDBConnexion()
r = _modules_enseignantsEditor.create(cnx, args)
return r
def can_change_module_resp(moduleimpl_id):
"""Check if current user can modify module resp. (raise exception if not).
= Admin, et dir des etud. (si option l'y autorise)
"""
M = moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
# -- check lock
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if not sem["etat"]:
raise ScoValueError("Modification impossible: semestre verrouille")
# -- check access
# admin ou resp. semestre avec flag resp_can_change_resp
if not current_user.has_permission(Permission.EditFormSemestre) and (
(current_user.id not in sem["responsables"]) or (not sem["resp_can_change_ens"])
):
raise AccessDenied(f"Modification impossible pour {current_user}")
return M, sem
def can_change_ens(moduleimpl_id, raise_exc=True):
"check if current user can modify ens list (raise exception if not)"
M = moduleimpl_withmodule_list(moduleimpl_id=moduleimpl_id)[0]
# -- check lock
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if not sem["etat"]:
if raise_exc:
raise ScoValueError("Modification impossible: semestre verrouille")
else:
return False
# -- check access
# admin, resp. module ou resp. semestre
if (
current_user.id != M["responsable_id"]
and not current_user.has_permission(Permission.EditFormSemestre)
and (current_user.id not in sem["responsables"])
):
if raise_exc:
raise AccessDenied("Modification impossible pour %s" % current_user)
else:
return False
return M, sem

View File

@ -209,15 +209,10 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
#
sem_locked = not formsemestre.etat
can_edit_evals = (
sco_permissions_check.can_edit_notes(
current_user, moduleimpl_id, allow_ens=formsemestre.ens_can_edit_eval
)
and not sem_locked
)
can_edit_notes = (
sco_permissions_check.can_edit_notes(current_user, moduleimpl_id)
modimpl.can_edit_notes(current_user, allow_ens=formsemestre.ens_can_edit_eval)
and not sem_locked
)
can_edit_notes = modimpl.can_edit_notes(current_user) and not sem_locked
arrow_up, arrow_down, arrow_none = sco_groups.get_arrow_icons_tags()
#
module_resp = db.session.get(User, modimpl.responsable_id)
@ -244,7 +239,7 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
<span class="blacktt">({module_resp.user_name})</span>
""",
]
if modimpl.can_change_ens_by(current_user):
if modimpl.can_change_responsable(current_user):
H.append(
f"""<a class="stdlink" href="{url_for("notes.edit_moduleimpl_resp",
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
@ -253,14 +248,15 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
H.append("""</td><td>""")
H.append(", ".join([u.get_nomprenom() for u in modimpl.enseignants]))
H.append("""</td><td>""")
try:
sco_moduleimpl.can_change_ens(moduleimpl_id)
if modimpl.can_change_ens(raise_exc=False):
H.append(
"""<a class="stdlink" href="edit_enseignants_form?moduleimpl_id=%s">modifier les enseignants</a>"""
% moduleimpl_id
f"""<a class="stdlink" href="{
url_for("notes.edit_enseignants_form",
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id
)
except:
pass
}">modifier les enseignants</a>"""
)
H.append("""</td></tr>""")
# 2ieme ligne: Semestre, Coef

View File

@ -8,50 +8,11 @@ from flask_login import current_user
from app import db
from app.auth.models import User
from app.models import FormSemestre
import app.scodoc.notesdb as ndb
from app.scodoc.sco_permissions import Permission
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_exceptions
from app.scodoc import sco_moduleimpl
def can_edit_notes(authuser, moduleimpl_id, allow_ens=True):
"""True if authuser can enter or edit notes in this module.
If allow_ens, grant access to all ens in this module
Si des décisions de jury ont déjà été saisies dans ce semestre,
seul le directeur des études peut saisir des notes (et il ne devrait pas).
"""
from app.scodoc import sco_formsemestre
from app.scodoc import sco_cursus_dut
M = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
sem = sco_formsemestre.get_formsemestre(M["formsemestre_id"])
if not sem["etat"]:
return False # semestre verrouillé
if sco_cursus_dut.formsemestre_has_decisions(sem["formsemestre_id"]):
# il y a des décisions de jury dans ce semestre !
return (
authuser.has_permission(Permission.EditAllNotes)
or authuser.id in sem["responsables"]
)
else:
if (
(not authuser.has_permission(Permission.EditAllNotes))
and authuser.id != M["responsable_id"]
and authuser.id not in sem["responsables"]
):
# enseignant (chargé de TD) ?
if allow_ens:
for ens in M["ens"]:
if ens["ens_id"] == authuser.id:
return True
return False
else:
return True
def can_suppress_annotation(annotation_id):

View File

@ -48,6 +48,7 @@ from wtforms import (
HiddenField,
SelectMultipleField,
)
from app.models import ModuleImpl
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import ScoValueError
@ -58,7 +59,6 @@ from app.scodoc import sco_evaluation_db
from app.scodoc import sco_excel
from app.scodoc.sco_excel import ScoExcelBook, COLORS
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_groups
from app.scodoc import sco_moduleimpl
from app.scodoc import sco_permissions_check
@ -247,6 +247,8 @@ class PlacementRunner:
# gr_title = sco_groups.listgroups_abbrev(d['groups'])
self.current_user = current_user
self.moduleimpl_id = self.eval_data["moduleimpl_id"]
self.moduleimpl: ModuleImpl = ModuleImpl.query.get_or_404(self.moduleimpl_id)
# TODO: à revoir pour utiliser modèle ModuleImpl
self.moduleimpl_data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=self.moduleimpl_id
)[0]
@ -280,9 +282,7 @@ class PlacementRunner:
def check_placement(self):
"""Vérifie que l'utilisateur courant a le droit d'édition sur les notes"""
# Check access (admin, respformation, and responsable_id)
return sco_permissions_check.can_edit_notes(
self.current_user, self.moduleimpl_id
)
return self.moduleimpl.can_edit_notes(self.current_user)
def exec_placement(self):
"""Excéute l'action liée au formulaire"""

View File

@ -198,8 +198,8 @@ def do_evaluation_upload_xls():
evaluation_id = int(vals["evaluation_id"])
comment = vals["comment"]
evaluation: Evaluation = Evaluation.query.get_or_404(evaluation_id)
# Check access (admin, respformation, and responsable_id)
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
# Check access (admin, respformation, responsable_id, ens)
if not evaluation.moduleimpl.can_edit_notes(current_user):
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
#
diag, lines = sco_excel.excel_file_to_list(vals["notefile"])
@ -315,7 +315,7 @@ def do_evaluation_set_etud_note(evaluation: Evaluation, etud: Identite, value) -
"""Enregistre la note d'un seul étudiant
value: valeur externe (float ou str)
"""
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl.id):
if not evaluation.moduleimpl.can_edit_notes(current_user):
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
# Convert and check value
L, invalids, _, _, _ = _check_notes([(etud.id, value)], evaluation)
@ -336,7 +336,7 @@ def do_evaluation_set_missing(
modimpl = evaluation.moduleimpl
# Check access
# (admin, respformation, and responsable_id)
if not sco_permissions_check.can_edit_notes(current_user, modimpl.id):
if not modimpl.can_edit_notes(current_user):
raise AccessDenied(f"Modification des notes impossible pour {current_user}")
#
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
@ -433,15 +433,11 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
"suppress all notes in this eval"
evaluation = Evaluation.query.get_or_404(evaluation_id)
if sco_permissions_check.can_edit_notes(
current_user, evaluation.moduleimpl_id, allow_ens=False
):
if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False):
# On a le droit de modifier toutes les notes
# recupere les etuds ayant une note
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
elif sco_permissions_check.can_edit_notes(
current_user, evaluation.moduleimpl_id, allow_ens=True
):
elif evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=True):
# Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
evaluation_id, by_uid=current_user.id
@ -682,7 +678,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
evaluation = Evaluation.query.get_or_404(evaluation_id)
moduleimpl_id = evaluation.moduleimpl.id
formsemestre_id = evaluation.moduleimpl.formsemestre_id
if not sco_permissions_check.can_edit_notes(current_user, moduleimpl_id):
if not evaluation.moduleimpl.can_edit_notes(current_user):
return (
html_sco_header.sco_header()
+ f"""
@ -813,9 +809,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=()):
#
H.append("""</div><h3>Autres opérations</h3><ul>""")
if sco_permissions_check.can_edit_notes(
current_user, moduleimpl_id, allow_ens=False
):
if evaluation.moduleimpl.can_edit_notes(current_user, allow_ens=False):
H.append(
f"""
<li>
@ -967,7 +961,7 @@ def saisie_notes(evaluation_id: int, group_ids: list = None):
)
# Check access
# (admin, respformation, and responsable_id)
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
if not evaluation.moduleimpl.can_edit_notes(current_user):
return f"""
{html_sco_header.sco_header()}
<h2>Modification des notes impossible pour {current_user.user_name}</h2>
@ -1361,7 +1355,7 @@ def save_notes(
_external=True,
)
# Check access: admin, respformation, or responsable_id
if not sco_permissions_check.can_edit_notes(current_user, evaluation.moduleimpl_id):
if not evaluation.moduleimpl.can_edit_notes(current_user):
return json_error(403, "modification notes non autorisee pour cet utilisateur")
#
valid_notes, _, _, _, _ = _check_notes(notes, evaluation)

View File

@ -1834,9 +1834,9 @@ def signale_evaluation_abs(etudid: int = None, evaluation_id: int = None):
)
# Sinon on créé l'assiduité
assiduite_unique: Assiduite | None = None
try:
assiduite_unique: Assiduite = Assiduite.create_assiduite(
assiduite_unique = Assiduite.create_assiduite(
etud=etud,
date_debut=scu.localize_datetime(evaluation.date_debut),
date_fin=scu.localize_datetime(evaluation.date_fin),
@ -1862,7 +1862,7 @@ def signale_evaluation_abs(etudid: int = None, evaluation_id: int = None):
duplication="oui",
)
raise ScoValueError(msg, dest) from exc
if assiduite_unique is not None:
db.session.add(assiduite_unique)
db.session.commit()

View File

@ -539,9 +539,9 @@ def ue_sharing_code():
return sco_edit_ue.ue_sharing_code(
ue_code=ue_code,
ue_id=None if ((ue_id is None) or ue_id == "") else int(ue_id),
hide_ue_id=None
if ((hide_ue_id is None) or hide_ue_id == "")
else int(hide_ue_id),
hide_ue_id=(
None if ((hide_ue_id is None) or hide_ue_id == "") else int(hide_ue_id)
),
)
@ -961,12 +961,15 @@ def formsemestre_custommenu_edit(formsemestre_id):
@scodoc7func
def edit_enseignants_form(moduleimpl_id):
"modif liste enseignants/moduleimpl"
M, sem = sco_moduleimpl.can_change_ens(moduleimpl_id)
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
modimpl.can_change_ens(raise_exc=True)
# --
header = html_sco_header.html_sem_header(
'Enseignants du <a href="moduleimpl_status?moduleimpl_id=%s">module %s</a>'
% (moduleimpl_id, M["module"]["titre"]),
page_title="Enseignants du module %s" % M["module"]["titre"],
f"""Enseignants du <a href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=modimpl.id)
}">module {modimpl.module.titre or modimpl.module.code}</a>""",
page_title=f"Enseignants du module {modimpl.module.titre or modimpl.module.code}",
javascripts=["libjs/AutoSuggest.js"],
cssstyles=["css/autosuggest_inquisitor.css"],
bodyOnLoad="init_tf_form('')",
@ -981,21 +984,18 @@ def edit_enseignants_form(moduleimpl_id):
allowed_user_names = list(uid2display.values())
H = [
"<ul><li><b>%s</b> (responsable)</li>"
% uid2display.get(M["responsable_id"], M["responsable_id"])
f"""<ul><li><b>{
uid2display.get(modimpl.responsable_id, modimpl.responsable_id)
}</b> (responsable)</li>"""
]
for ens in M["ens"]:
u = db.session.get(User, ens["ens_id"])
if u:
nom = u.get_nomcomplet()
else:
nom = "? (compte inconnu)"
u: User
for u in modimpl.enseignants:
H.append(
f"""
<li>{nom} (<a class="stdlink" href="{
<li>{u.get_nomcomplet()} (<a class="stdlink" href="{
url_for('notes.edit_enseignants_form_delete',
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id,
ens_id=ens["ens_id"])
ens_id=u.id)
}">supprimer</a>)
</li>"""
)
@ -1006,7 +1006,7 @@ def edit_enseignants_form(moduleimpl_id):
<p class="help">Pour changer le responsable du module, passez par la
page "<a class="stdlink" href="{
url_for("notes.formsemestre_editwithmodules", scodoc_dept=g.scodoc_dept,
formsemestre_id=M["formsemestre_id"])
formsemestre_id=modimpl.formsemestre_id)
}">Modification du semestre</a>",
accessible uniquement au responsable de la formation (chef de département)
</p>
@ -1053,24 +1053,24 @@ def edit_enseignants_form(moduleimpl_id):
)
)
else:
ens_id = User.get_user_id_from_nomplogin(tf[2]["ens_id"])
if not ens_id:
ens = User.get_user_from_nomplogin(tf[2]["ens_id"])
if ens is None:
H.append(
'<p class="help">Pour ajouter un enseignant, choisissez un nom dans le menu</p>'
)
else:
# et qu'il n'est pas deja:
if (
ens_id in [x["ens_id"] for x in M["ens"]]
or ens_id == M["responsable_id"]
ens.id in (x.id for x in modimpl.enseignants)
or ens.id == modimpl.responsable_id
):
H.append(
f"""<p class="help">Enseignant {ens_id} déjà dans la liste !</p>"""
f"""<p class="help">Enseignant {ens.user_name} déjà dans la liste !</p>"""
)
else:
sco_moduleimpl.do_ens_create(
{"moduleimpl_id": moduleimpl_id, "ens_id": ens_id}
)
modimpl.enseignants.append(ens)
db.session.add(modimpl)
db.session.commit()
return flask.redirect(
url_for(
"notes.edit_enseignants_form",
@ -1090,10 +1090,10 @@ def edit_moduleimpl_resp(moduleimpl_id: int):
Accessible par Admin et dir des etud si flag resp_can_change_ens
"""
modimpl: ModuleImpl = ModuleImpl.query.get_or_404(moduleimpl_id)
modimpl.can_change_ens_by(current_user, raise_exc=True) # access control
modimpl.can_change_responsable(current_user, raise_exc=True) # access control
H = [
html_sco_header.html_sem_header(
f"""Modification du responsable du <a href="{
f"""Modification du responsable du <a class="stdlink" href="{
url_for("notes.moduleimpl_status",
scodoc_dept=g.scodoc_dept, moduleimpl_id=moduleimpl_id)
}">module {modimpl.module.titre or ""}</a>""",
@ -1156,8 +1156,8 @@ def edit_moduleimpl_resp(moduleimpl_id: int):
)
)
else:
responsable_id = User.get_user_id_from_nomplogin(tf[2]["responsable_id"])
if not responsable_id:
responsable = User.get_user_from_nomplogin(tf[2]["responsable_id"])
if not responsable:
# presque impossible: tf verifie les valeurs (mais qui peuvent changer entre temps)
return flask.redirect(
url_for(
@ -1167,7 +1167,7 @@ def edit_moduleimpl_resp(moduleimpl_id: int):
)
)
modimpl.responsable_id = responsable_id
modimpl.responsable = responsable
db.session.add(modimpl)
db.session.commit()
flash("Responsable modifié")
@ -1192,8 +1192,6 @@ def view_module_abs(moduleimpl_id, fmt="html"):
.filter_by(dept_id=g.scodoc_dept_id)
).first_or_404()
debut_sem = modimpl.formsemestre.date_debut
fin_sem = modimpl.formsemestre.date_fin
inscrits: list[Identite] = sorted(
[i.etud for i in modimpl.inscriptions], key=lambda e: e.sort_key
)
@ -1286,9 +1284,8 @@ def formsemestre_enseignants_list(formsemestre_id, fmt="html"):
"""
formsemestre = FormSemestre.get_formsemestre(formsemestre_id)
# resp. de modules et charges de TD
sem_ens: dict[
int, list[ModuleImpl]
] = {} # uid : { "mods" : liste des modimpls, ... }
# uid : { "mods" : liste des modimpls, ... }
sem_ens: dict[int, list[ModuleImpl]] = {}
modimpls = formsemestre.modimpls_sorted
for modimpl in modimpls:
if not modimpl.responsable_id in sem_ens:
@ -1372,22 +1369,17 @@ def edit_enseignants_form_delete(moduleimpl_id, ens_id: int):
ens_id: user.id
"""
M, _ = sco_moduleimpl.can_change_ens(moduleimpl_id)
modimpl = ModuleImpl.get_modimpl(moduleimpl_id)
modimpl.can_change_ens(raise_exc=True)
# search ens_id
ok = False
for ens in M["ens"]:
if ens["ens_id"] == ens_id:
ok = True
ens: User | None = None
for ens in modimpl.enseignants:
if ens.id == ens_id:
break
if not ok:
if ens is None:
raise ScoValueError(f"invalid ens_id ({ens_id})")
ndb.SimpleQuery(
"""DELETE FROM notes_modules_enseignants
WHERE moduleimpl_id = %(moduleimpl_id)s
AND ens_id = %(ens_id)s
""",
{"moduleimpl_id": moduleimpl_id, "ens_id": ens_id},
)
modimpl.enseignants.remove(ens)
db.session.commit()
return flask.redirect(
url_for(
"notes.edit_enseignants_form",
@ -1399,18 +1391,6 @@ def edit_enseignants_form_delete(moduleimpl_id, ens_id: int):
# --- Gestion des inscriptions aux semestres
# Ancienne API, pas certain de la publier en ScoDoc8
# sco_publish(
# "/do_formsemestre_inscription_create",
# sco_formsemestre_inscriptions.do_formsemestre_inscription_create,
# Permission.EtudInscrit,
# )
# sco_publish(
# "/do_formsemestre_inscription_edit",
# sco_formsemestre_inscriptions.do_formsemestre_inscription_edit,
# Permission.EtudInscrit,
# )
sco_publish(
"/do_formsemestre_inscription_list",
sco_formsemestre_inscriptions.do_formsemestre_inscription_list,

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.6.92"
SCOVERSION = "9.6.934"
SCONAME = "ScoDoc"

View File

@ -619,7 +619,6 @@ def test_formsemestre_programme(api_headers):
assert verify_fields(sae, MODIMPL_FIELDS)
@pytest.mark.skip # XXX WIP
def test_etat_evals(api_headers): # voir si on maintient cette route ?
"""
Route : /formsemestre/<int:formsemestre_id>/etat_evals

View File

@ -375,7 +375,6 @@ def test_import_formation(test_client, filename="formation-exemple-1.xml"):
formsemestre_id=formsemestre_ids[mod["semestre_id"] - 1],
)
mi = sco_moduleimpl.moduleimpl_list(moduleimpl_id=moduleimpl_id)[0]
assert mi["ens"] == []
assert mi["module_id"] == mod["module_id"]
# --- Export formation en XML