Compare commits

...

6 Commits

17 changed files with 203 additions and 149 deletions

View File

@ -295,7 +295,7 @@ def dept_formsemestres_courants_by_id(dept_id: int):
if date_courante: if date_courante:
test_date = datetime.fromisoformat(date_courante) test_date = datetime.fromisoformat(date_courante)
else: else:
test_date = app.db.func.now() test_date = db.func.current_date()
# Les semestres en cours de ce département # Les semestres en cours de ce département
formsemestres = FormSemestre.query.filter( formsemestres = FormSemestre.query.filter(
FormSemestre.dept_id == dept.id, FormSemestre.dept_id == dept.id,

View File

@ -349,19 +349,12 @@ class BulletinBUT:
raise ScoValueError("bulletin_etud: version de bulletin demandée invalide") raise ScoValueError("bulletin_etud: version de bulletin demandée invalide")
res = self.res res = self.res
formsemestre = res.formsemestre formsemestre = res.formsemestre
etat_inscription = etud.inscription_etat(formsemestre.id)
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
published = (not formsemestre.bul_hide_xml) or force_publishing
if formsemestre.formation.referentiel_competence is None:
etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
else:
etud_ues_ids = res.etud_ues_ids(etud.id)
d = { d = {
"version": "0", "version": "0",
"type": "BUT", "type": "BUT",
"date": datetime.datetime.utcnow().isoformat() + "Z", "date": datetime.datetime.utcnow().isoformat() + "Z",
"publie": not formsemestre.bul_hide_xml, "publie": not formsemestre.bul_hide_xml,
"etat_inscription": etud.inscription_etat(formsemestre.id),
"etudiant": etud.to_dict_bul(), "etudiant": etud.to_dict_bul(),
"formation": { "formation": {
"id": formsemestre.formation.id, "id": formsemestre.formation.id,
@ -370,14 +363,20 @@ class BulletinBUT:
"titre": formsemestre.formation.titre, "titre": formsemestre.formation.titre,
}, },
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etat_inscription": etat_inscription,
"options": sco_preferences.bulletin_option_affichage( "options": sco_preferences.bulletin_option_affichage(
formsemestre, self.prefs formsemestre, self.prefs
), ),
} }
if not published: published = (not formsemestre.bul_hide_xml) or force_publishing
if not published or d["etat_inscription"] is False:
return d return d
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
if formsemestre.formation.referentiel_competence is None:
etud_ues_ids = {ue.id for ue in res.ues if res.modimpls_in_ue(ue, etud.id)}
else:
etud_ues_ids = res.etud_ues_ids(etud.id)
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id) nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
etud_groups = sco_groups.get_etud_formsemestre_groups( etud_groups = sco_groups.get_etud_formsemestre_groups(
etud, formsemestre, only_to_show=True etud, formsemestre, only_to_show=True
@ -410,7 +409,7 @@ class BulletinBUT:
semestre_infos.update( semestre_infos.update(
sco_bulletins_json.dict_decision_jury(etud, formsemestre) sco_bulletins_json.dict_decision_jury(etud, formsemestre)
) )
if etat_inscription == scu.INSCRIT: if d["etat_inscription"] == scu.INSCRIT:
# moyenne des moyennes générales du semestre # moyenne des moyennes générales du semestre
semestre_infos["notes"] = { semestre_infos["notes"] = {
"value": fmt_note(res.etud_moy_gen[etud.id]), "value": fmt_note(res.etud_moy_gen[etud.id]),

View File

@ -102,7 +102,7 @@ class AjoutAssiOrJustForm(FlaskForm):
) )
entry_date = StringField( entry_date = StringField(
"Date de dépot ou saisie", "Date de dépôt ou saisie",
validators=[validators.Length(max=10)], validators=[validators.Length(max=10)],
render_kw={ render_kw={
"class": "datepicker", "class": "datepicker",
@ -110,6 +110,16 @@ class AjoutAssiOrJustForm(FlaskForm):
"id": "entry_date", "id": "entry_date",
}, },
) )
entry_time = StringField(
"Heure dépôt",
default="",
validators=[validators.Length(max=5)],
render_kw={
"class": "timepicker",
"size": 5,
"id": "assi_heure_fin",
},
)
submit = SubmitField("Enregistrer") submit = SubmitField("Enregistrer")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True}) cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -418,7 +418,7 @@ class Justificatif(ScoDocModel):
entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now()) entry_date = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
"date de création de l'élément: date de saisie" "date de création de l'élément: date de saisie"
# pourrait devenir date de dépot au secrétariat, si différente # pourrait devenir date de dépôt au secrétariat, si différente
user_id = db.Column( user_id = db.Column(
db.Integer, db.Integer,

View File

@ -673,7 +673,7 @@ class FormSemestre(db.Model):
) -> db.Query: ) -> db.Query:
"""Liste (query) ordonnée des formsemestres courants, c'est """Liste (query) ordonnée des formsemestres courants, c'est
à dire contenant la date courant (si None, la date actuelle)""" à dire contenant la date courant (si None, la date actuelle)"""
date_courante = date_courante or db.func.now() date_courante = date_courante or db.func.current_date()
# Les semestres en cours de ce département # Les semestres en cours de ce département
formsemestres = FormSemestre.query.filter( formsemestres = FormSemestre.query.filter(
FormSemestre.dept_id == dept.id, FormSemestre.dept_id == dept.id,

View File

@ -1,6 +1,6 @@
"""ScoDoc 9 models : Modules """ScoDoc 9 models : Modules
""" """
from flask import current_app from flask import current_app, g
from app import db from app import db
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
@ -310,6 +310,14 @@ class Module(db.Model):
return [] return []
return self.parcours return self.parcours
def add_tag(self, tag: "NotesTag"):
"""Add tag to module. Check if already has it."""
if tag.id in {t.id for t in self.tags}:
return
self.tags.append(tag)
db.session.add(self)
db.session.flush()
class ModuleUECoef(db.Model): class ModuleUECoef(db.Model):
"""Coefficients des modules vers les UE (APC, BUT) """Coefficients des modules vers les UE (APC, BUT)
@ -372,6 +380,19 @@ class NotesTag(db.Model):
dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True) dept_id = db.Column(db.Integer, db.ForeignKey("departement.id"), index=True)
title = db.Column(db.Text(), nullable=False) title = db.Column(db.Text(), nullable=False)
@classmethod
def get_or_create(cls, title: str, dept_id: int | None = None) -> "NotesTag":
"""Get tag, or create it if it doesn't yet exists.
If dept_id unspecified, use current dept.
"""
dept_id = dept_id if dept_id is not None else g.scodoc_dept_id
tag = NotesTag.query.filter_by(dept_id=dept_id, title=title).first()
if tag is None:
tag = NotesTag(dept_id=dept_id, title=title)
db.session.add(tag)
db.session.flush()
return tag
# Association tag <-> module # Association tag <-> module
notes_modules_tags = db.Table( notes_modules_tags = db.Table(

View File

@ -38,27 +38,24 @@ Created on 17/01/2024
import pandas as pd import pandas as pd
from app.models import FormSemestre, Identite from app.models import FormSemestre, Identite
import app.pe.pe_affichage as pe_affichage
import app.pe.pe_comp as pe_comp
from app.pe import pe_comp, pe_affichage from app.pe import pe_comp, pe_affichage
class EtudiantsJuryPE: class EtudiantsJuryPE:
"""Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE"""
def __init__(self, annee_diplome: int): def __init__(self, annee_diplome: int):
""" """
Classe centralisant la gestion des étudiants à prendre en compte dans un jury de PE
Args: Args:
annee_diplome: L'année de diplomation annee_diplome: L'année de diplomation
""" """
self.annee_diplome = annee_diplome self.annee_diplome = annee_diplome
"""L'année du diplôme""" """L'année du diplôme"""
self.identites = {} # ex. ETUDINFO_DICT self.identites: dict[int, Identite] = {} # ex. ETUDINFO_DICT
"Les identités des étudiants traités pour le jury" "Les identités des étudiants traités pour le jury"
self.cursus = {} self.cursus: dict[int, dict] = {}
"Les cursus (semestres suivis, abandons) des étudiants" "Les cursus (semestres suivis, abandons) des étudiants"
self.trajectoires = {} self.trajectoires = {}
@ -67,15 +64,17 @@ class EtudiantsJuryPE:
(par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements)""" (par ex: 3S=S1+S2+S3 à prendre en compte avec d'éventuels redoublements)"""
self.etudiants_diplomes = {} self.etudiants_diplomes = {}
"""Les identités des étudiants à considérer au jury (ceux qui seront effectivement diplômés)""" """Les identités des étudiants à considérer au jury (ceux qui seront effectivement
diplômés)"""
self.diplomes_ids = {} self.diplomes_ids = {}
"""Les etudids des étudiants diplômés""" """Les etudids des étudiants diplômés"""
self.etudiants_ids = {} self.etudiants_ids = {}
"""Les etudids des étudiants dont il faut calculer les moyennes/classements (même si d'éventuels abandons). """Les etudids des étudiants dont il faut calculer les moyennes/classements
Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi d'autres ayant (même si d'éventuels abandons).
été réorientés ou ayant abandonnés)""" Il s'agit des étudiants inscrits dans les co-semestres (ceux du jury mais aussi
d'autres ayant été réorientés ou ayant abandonnés)"""
self.cosemestres: dict[int, FormSemestre] = None self.cosemestres: dict[int, FormSemestre] = None
"Les cosemestres donnant lieu à même année de diplome" "Les cosemestres donnant lieu à même année de diplome"
@ -107,18 +106,15 @@ class EtudiantsJuryPE:
pe_affichage.pe_print("2) Liste des étudiants dans les différents co-semestres") pe_affichage.pe_print("2) Liste des étudiants dans les différents co-semestres")
self.etudiants_ids = get_etudiants_dans_semestres(cosemestres) self.etudiants_ids = get_etudiants_dans_semestres(cosemestres)
pe_affichage.pe_print( pe_affichage.pe_print(
" => %d étudiants trouvés dans les cosemestres" % len(self.etudiants_ids) f" => {len(self.etudiants_ids)} étudiants trouvés dans les cosemestres"
) )
# Analyse des parcours étudiants pour déterminer leur année effective de diplome # Analyse des parcours étudiants pour déterminer leur année effective de diplome
# avec prise en compte des redoublements, des abandons, .... # avec prise en compte des redoublements, des abandons, ....
pe_affichage.pe_print("3) Analyse des parcours individuels des étudiants") pe_affichage.pe_print("3) Analyse des parcours individuels des étudiants")
no_etud = 0 for etudid in self.etudiants_ids:
for no_etud, etudid in enumerate(self.etudiants_ids): self.identites[etudid] = Identite.get_etud(etudid)
identite = Identite.get_etud(etudid)
self.identites[etudid] = identite
"""identités des étudiants"""
# Analyse son cursus # Analyse son cursus
self.analyse_etat_etudiant(etudid, cosemestres) self.analyse_etat_etudiant(etudid, cosemestres)
@ -131,7 +127,6 @@ class EtudiantsJuryPE:
self.diplomes_ids = set(self.etudiants_diplomes.keys()) self.diplomes_ids = set(self.etudiants_diplomes.keys())
self.etudiants_ids = set(self.identites.keys()) self.etudiants_ids = set(self.identites.keys())
"""Les étudiants dont il faut calculer les moyennes"""
self.formsemestres_jury_ids = self.get_formsemestres() self.formsemestres_jury_ids = self.get_formsemestres()
"""Les formsemestres (des étudiants) dont il faut calculer les moyennes""" """Les formsemestres (des étudiants) dont il faut calculer les moyennes"""
@ -162,8 +157,6 @@ class EtudiantsJuryPE:
# + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)]) # + ", ".join([str(fid) for fid in list(self.formsemestres_jury_ids)])
# ) # )
def get_etudiants_diplomes(self) -> dict[int, Identite]: def get_etudiants_diplomes(self) -> dict[int, Identite]:
"""Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}` """Identités des étudiants (sous forme d'un dictionnaire `{etudid: Identite(etudid)}`
qui vont être à traiter au jury PE pour qui vont être à traiter au jury PE pour
@ -175,9 +168,9 @@ class EtudiantsJuryPE:
""" """
etudids = [ etudids = [
etudid etudid
for etudid in self.cursus for etudid, cursus_etud in self.cursus.items()
if self.cursus[etudid]["diplome"] == self.annee_diplome if cursus_etud["diplome"] == self.annee_diplome
and self.cursus[etudid]["abandon"] is False and cursus_etud["abandon"] is False
] ]
etudiants = {etudid: self.identites[etudid] for etudid in etudids} etudiants = {etudid: self.identites[etudid] for etudid in etudids}
return etudiants return etudiants
@ -192,9 +185,9 @@ class EtudiantsJuryPE:
""" """
etudids = [ etudids = [
etudid etudid
for etudid in self.cursus for etudid, cursus_etud in self.cursus.items()
if self.cursus[etudid]["diplome"] != self.annee_diplome if cursus_etud["diplome"] != self.annee_diplome
or self.cursus[etudid]["abandon"] is True or cursus_etud["abandon"] is True
] ]
etudiants = {etudid: self.identites[etudid] for etudid in etudids} etudiants = {etudid: self.identites[etudid] for etudid in etudids}
return etudiants return etudiants
@ -224,9 +217,9 @@ class EtudiantsJuryPE:
formsemestres = identite.get_formsemestres() formsemestres = identite.get_formsemestres()
semestres_etudiant = { semestres_etudiant = {
frmsem.formsemestre_id: frmsem formsemestre.formsemestre_id: formsemestre
for frmsem in formsemestres for formsemestre in formsemestres
if frmsem.formation.is_apc() if formsemestre.formation.is_apc()
} }
self.cursus[etudid] = { self.cursus[etudid] = {
@ -282,11 +275,12 @@ class EtudiantsJuryPE:
# Tri des semestres par numéro de semestre # Tri des semestres par numéro de semestre
for nom_sem in pe_comp.TOUS_LES_SEMESTRES: for nom_sem in pe_comp.TOUS_LES_SEMESTRES:
i = int(nom_sem[1]) # le n° du semestre i = int(nom_sem[1]) # le n° du semestre
# les semestres de n°i de l'étudiant:
semestres_i = { semestres_i = {
fid: semestres_significatifs[fid] fid: sem_sig
for fid in semestres_significatifs for fid, sem_sig in semestres_significatifs.items()
if semestres_significatifs[fid].semestre_id == i if sem_sig.semestre_id == i
} # les semestres de n°i de l'étudiant }
self.cursus[etudid][nom_sem] = semestres_i self.cursus[etudid][nom_sem] = semestres_i
def get_trajectoire( def get_trajectoire(
@ -323,7 +317,7 @@ class EtudiantsJuryPE:
int(sem[-1]) for sem in pe_comp.PARCOURS[nom_aggregat]["aggregat"] int(sem[-1]) for sem in pe_comp.PARCOURS[nom_aggregat]["aggregat"]
] ]
assert numero_semestre_terminal in numero_semestres_possibles assert numero_semestre_terminal in numero_semestres_possibles
else: # les xS = tous les semestres jusqu'à Sx (pax ex: des S1, S2, S3 pour un S3 terminal) else: # les xS = tous les semestres jusqu'à Sx (eg S1, S2, S3 pour un S3 terminal)
numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1)) numero_semestres_possibles = list(range(1, numero_semestre_terminal + 1))
semestres_aggreges = {} semestres_aggreges = {}
@ -380,23 +374,23 @@ class EtudiantsJuryPE:
* '3S', '4S' : pour obtenir les combinaisons de semestres définies par les aggrégats * '3S', '4S' : pour obtenir les combinaisons de semestres définies par les aggrégats
Returns: Returns:
Un dictionnaire de la forme ``{fid: FormSemestre(fid)}`` Un dictionnaire de la forme `{fid: FormSemestre(fid)}`
Remarque: Remarque:
Une liste de la forme ``[ 'Si', 'iA' , ... ]`` (combinant les formats précédents) est possible. Une liste de la forme `[ 'Si', 'iA' , ... ]` (combinant les formats précédents) est possible.
""" """
if semestres_recherches is None: if semestres_recherches is None:
# Appel récursif pour obtenir tous les semestres (validants) # Appel récursif pour obtenir tous les semestres (validants)
semestres = self.get_formsemestres(pe_comp.AGGREGAT_DIPLOMANT) semestres = self.get_formsemestres(pe_comp.AGGREGAT_DIPLOMANT)
return semestres return semestres
elif isinstance(semestres_recherches, list): if isinstance(semestres_recherches, list):
# Appel récursif sur tous les éléments de la liste # Appel récursif sur tous les éléments de la liste
semestres = {} semestres = {}
for elmt in semestres_recherches: for elmt in semestres_recherches:
semestres_elmt = self.get_formsemestres(elmt) semestres_elmt = self.get_formsemestres(elmt)
semestres = semestres | semestres_elmt semestres = semestres | semestres_elmt
return semestres return semestres
elif ( if (
isinstance(semestres_recherches, str) isinstance(semestres_recherches, str)
and semestres_recherches in pe_comp.TOUS_LES_AGGREGATS and semestres_recherches in pe_comp.TOUS_LES_AGGREGATS
): ):
@ -405,7 +399,7 @@ class EtudiantsJuryPE:
pe_comp.PARCOURS[semestres_recherches]["aggregat"] pe_comp.PARCOURS[semestres_recherches]["aggregat"]
) )
return semestres return semestres
elif ( if (
isinstance(semestres_recherches, str) isinstance(semestres_recherches, str)
and semestres_recherches in pe_comp.TOUS_LES_SEMESTRES and semestres_recherches in pe_comp.TOUS_LES_SEMESTRES
): ):
@ -418,8 +412,8 @@ class EtudiantsJuryPE:
if self.cursus[etudid][nom_sem]: if self.cursus[etudid][nom_sem]:
semestres = semestres | self.cursus[etudid][nom_sem] semestres = semestres | self.cursus[etudid][nom_sem]
return semestres return semestres
else:
raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids") raise ValueError("Probleme de paramètres d'appel dans get_formsemestreids")
def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int: def nbre_etapes_max_diplomes(self, etudids: list[int]) -> int:
"""Partant d'un ensemble d'étudiants, """Partant d'un ensemble d'étudiants,
@ -433,8 +427,7 @@ class EtudiantsJuryPE:
nbres_semestres.append(self.cursus[etudid]["nb_semestres"]) nbres_semestres.append(self.cursus[etudid]["nb_semestres"])
if not nbres_semestres: if not nbres_semestres:
return 0 return 0
else: return max(nbres_semestres)
return max(nbres_semestres)
def df_administratif(self, etudids: list[int]) -> pd.DataFrame: def df_administratif(self, etudids: list[int]) -> pd.DataFrame:
"""Synthétise toutes les données administratives d'un groupe """Synthétise toutes les données administratives d'un groupe
@ -461,13 +454,16 @@ class EtudiantsJuryPE:
diplome = "indéterminé" diplome = "indéterminé"
administratif[etudid] = { administratif[etudid] = {
"etudid": etudiant.id,
"INE": etudiant.code_ine or "",
"NIP": etudiant.code_nip or "",
"Nom": etudiant.nom, "Nom": etudiant.nom,
"Prenom": etudiant.prenom, "Prenom": etudiant.prenom,
"Civilite": etudiant.civilite_str, "Civilite": etudiant.civilite_str,
"Age": pe_comp.calcul_age(etudiant.date_naissance), "Age": pe_comp.calcul_age(etudiant.date_naissance),
"Date d'entree": cursus["entree"], "Date entree": cursus["entree"],
"Date de diplome": diplome, "Date diplome": diplome,
"Nbre de semestres": len(formsemestres), "Nb semestres": len(formsemestres),
} }
# Ajout des noms de semestres parcourus # Ajout des noms de semestres parcourus

View File

@ -19,7 +19,7 @@ class Trace:
"""gestionnaire de la trace des fichiers justificatifs """gestionnaire de la trace des fichiers justificatifs
Role des fichiers traces : Role des fichiers traces :
- Sauvegarder la date de dépot du fichier - Sauvegarder la date de dépôt du fichier
- Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif) - Sauvegarder la date de suppression du fichier (dans le cas de plusieurs fichiers pour un même justif)
- Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView) - Sauvegarder l'user_id de l'utilisateur ayant déposé le fichier (=> permet de montrer les fichiers qu'aux personnes qui l'on déposé / qui ont le rôle AssiJustifView)
@ -116,7 +116,7 @@ class JustificatifArchiver(BaseArchiver):
TOTALK: TOTALK:
- oid -> etudid - oid -> etudid
- archive_id -> date de création de l'archive (une archive par dépot de document) - archive_id -> date de création de l'archive (une archive par dépôt de document)
justificatif justificatif
<dept_id> <dept_id>

View File

@ -670,6 +670,7 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
semestre_idx = None semestre_idx = None
else: else:
semestre_idx = int(semestre_idx) semestre_idx = int(semestre_idx)
show_tags = scu.to_bool(request.args.get("show_tags", 0))
locked = formation.has_locked_sems(semestre_idx) locked = formation.has_locked_sems(semestre_idx)
semestre_ids = range(1, parcours.NB_SEM + 1) semestre_ids = range(1, parcours.NB_SEM + 1)
# transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7 # transition: on requete ici via l'ORM mais on utilise les fonctions ScoDoc7
@ -875,11 +876,13 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
) )
# Description des UE/matières/modules # Description des UE/matières/modules
H.append( H.append(
""" f"""
<div class="formation_ue_list"> <div class="formation_ue_list">
<div class="ue_list_tit">Programme pédagogique:</div> <div class="ue_list_tit">Programme pédagogique:</div>
<form> <form>
<input type="checkbox" class="sco_tag_checkbox">montrer les tags des modules</input> <input type="checkbox" class="sco_tag_checkbox"
{'checked' if show_tags else ''}
>montrer les tags des modules</input>
</form> </form>
""" """
) )
@ -978,6 +981,11 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
formation_id=formation_id) formation_id=formation_id)
}">Table récapitulative de la formation</a> }">Table récapitulative de la formation</a>
</li> </li>
<li><a class="stdlink" href="{
url_for('notes.formation_tag_modules_by_type', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, semestre_idx=semestre_idx)
}">Tagguer tous les modules par leur type</a> (tag <tt>res</tt>, <tt>sae</tt>).
</li>
<li><a class="stdlink" href="{ <li><a class="stdlink" href="{
url_for('notes.formation_export', scodoc_dept=g.scodoc_dept, url_for('notes.formation_export', scodoc_dept=g.scodoc_dept,
formation_id=formation_id, fmt='xml') formation_id=formation_id, fmt='xml')
@ -1061,6 +1069,7 @@ def _html_select_semestre_idx(formation_id, semestre_ids, semestre_idx):
htm += f""" htm += f"""
</select> </select>
<input type="hidden" name="formation_id" value="{formation_id}"></input> <input type="hidden" name="formation_id" value="{formation_id}"></input>
<input type="hidden" name="show_tags" value="0"></input>
</form>""" </form>"""
return htm return htm

View File

@ -103,6 +103,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
"heures_cours": mod.heures_cours, "heures_cours": mod.heures_cours,
"heures_td": mod.heures_td, "heures_td": mod.heures_td,
"heures_tp": mod.heures_tp, "heures_tp": mod.heures_tp,
"tags": ", ".join(t.title for t in mod.tags if t.title),
"_css_row_class": f"mod {mod.type_abbrv()}", "_css_row_class": f"mod {mod.type_abbrv()}",
} }
) )
@ -117,6 +118,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
"heures_cours", "heures_cours",
"heures_td", "heures_td",
"heures_tp", "heures_tp",
"tags",
] ]
if not formation.is_apc(): if not formation.is_apc():
columns_ids.insert(columns_ids.index("ects"), "coef") columns_ids.insert(columns_ids.index("ects"), "coef")
@ -132,6 +134,7 @@ def formation_table_recap(formation_id, fmt="html") -> Response:
"heures_cours": "Cours (h)", "heures_cours": "Cours (h)",
"heures_td": "TD (h)", "heures_td": "TD (h)",
"heures_tp": "TP (h)", "heures_tp": "TP (h)",
"tags": "Tags",
"ects": "ECTS", "ects": "ECTS",
} }

View File

@ -38,17 +38,14 @@ import re
from flask import g from flask import g
from app.comp import res_sem from app import db, log
from app.comp.res_compat import NotesTableCompat from app.models import Formation, NotesTag
from app.models import FormSemestre from app.scodoc import sco_edit_module
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log
from app.scodoc import sco_edit_module
from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import ScoValueError from app.scodoc.sco_exceptions import ScoValueError
# Opérations à implementer: # Opérations implementées:
# + liste des modules des formations de code donné (formation_code) avec ce tag # + liste des modules des formations de code donné (formation_code) avec ce tag
# + liste de tous les noms de tag # + liste de tous les noms de tag
# + tag pour un nom # + tag pour un nom
@ -62,6 +59,7 @@ from app.scodoc.sco_exceptions import ScoValueError
# module_tag_set( module_id, taglist ) -> modifie les tags # module_tag_set( module_id, taglist ) -> modifie les tags
# NOTA: ancien code, n'utile pas de modèles SQLAlchemy
class ScoTag(object): class ScoTag(object):
"""Generic tags for ScoDoc""" """Generic tags for ScoDoc"""
@ -232,7 +230,7 @@ def module_tag_search(term: str | int):
return scu.sendJSON(data) return scu.sendJSON(data)
def module_tag_list(module_id=""): def module_tag_list(module_id="") -> list[str]:
"""les noms de tags associés à ce module""" """les noms de tags associés à ce module"""
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT t.title """SELECT t.title
@ -249,6 +247,7 @@ def module_tag_set(module_id="", taglist=None):
"""taglist may either be: """taglist may either be:
a string with tag names separated by commas ("un,deux") a string with tag names separated by commas ("un,deux")
or a list of strings (["un", "deux"]) or a list of strings (["un", "deux"])
Remplace les tags existants
""" """
if not taglist: if not taglist:
taglist = [] taglist = []
@ -284,34 +283,6 @@ def module_tag_set(module_id="", taglist=None):
return "", http.HTTPStatus.NO_CONTENT return "", http.HTTPStatus.NO_CONTENT
def get_etud_tagged_modules(etudid, tagname):
"""Liste d'infos sur les modules de ce semestre avec ce tag.
Cherche dans tous les semestres dans lesquel l'étudiant est ou a été inscrit.
Construit la liste des modules avec le tag donné par tagname
"""
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
R = []
for sem in etud["sems"]:
formsemestre = FormSemestre.query.get_or_404(sem["formsemestre_id"])
nt: NotesTableCompat = res_sem.load_formsemestre_results(formsemestre)
modimpls = nt.get_modimpls_dict()
for modimpl in modimpls:
tags = module_tag_list(module_id=modimpl["module_id"])
if tagname in tags:
moy = nt.get_etud_mod_moy(
modimpl["moduleimpl_id"], etudid
) # ou NI si non inscrit
R.append(
{
"sem": sem,
"moy": moy, # valeur réelle, ou NI (non inscrit au module ou NA (pas de note)
"moduleimpl": modimpl,
"tags": tags,
}
)
return R
def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]: def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]:
"""Découpage d'un tag, tel que saisi par un utilisateur dans le programme, """Découpage d'un tag, tel que saisi par un utilisateur dans le programme,
pour en extraire : pour en extraire :
@ -335,42 +306,27 @@ def split_tagname_coeff(tag: str, separateur=":") -> tuple[str, float]:
try: try:
pond = float(temp[1]) pond = float(temp[1])
return (temp[0], pond) return (temp[0], pond)
except: except (IndexError, ValueError, TypeError):
"""Renvoie tout le tag si le découpage à échouer""" # Renvoie tout le tag si le découpage a échoué
return (tag, 1.0) return (tag, 1.0)
else: else:
"""initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération n'est indiqué dans le tag""" # initialise le coeff de pondération à 1 lorsqu'aucun coeff de pondération
# n'ait indiqué dans le tag
return (tag, 1.0) return (tag, 1.0)
"""Tests: def formation_tag_modules_by_type(formation: Formation):
from debug import * """Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus'
from app.scodoc.sco_tag_module import * Ne taggue pas les modules standards.
_ = go_dept(app, 'RT').Notes """
tag_titles = {
t = ModuleTag( 'essai') m.type_abbrv() for m in formation.modules
t.tag_module('totoro') # error (module invalide) } # usually {'res', 'mod', 'sae'}
t.tag_module('MOD21460') tag_by_type = {
t.delete() # detruit tag et assoc tag_title: NotesTag.get_or_create(title=tag_title, dept_id=formation.dept_id)
t = ModuleTag( 'essai2') for tag_title in tag_titles
t.tag_module('MOD21460') }
t.tag_module('MOD21464') for module in formation.modules:
t.list_modules() if module.module_type != scu.ModuleType.STANDARD:
t.list_modules(formation_code='ccc') # empty list module.add_tag(tag_by_type[module.type_abbrv()])
t.list_modules(formation_code='FCOD2') db.session.commit()
Un essai de get_etud_tagged_modules:
from debug import *
from app.scodoc.sco_tag_module import *
_ = go_dept(app, 'GEA').Notes
etudid='GEAEID80687'
etud = sco_etud.get_etud_info( etudid=etudid, filled=True)[0]
sem = etud['sems'][0]
[ tm['moy'] for tm in get_etud_tagged_modules( etudid, 'allo') ]
# si besoin après modif par le Web:
# sco_cache.invalidate_formsemestre()
"""

View File

@ -23,11 +23,36 @@ $(function () {
// version readonly // version readonly
readOnlyTags($(".module_tag_editor_ro")); readOnlyTags($(".module_tag_editor_ro"));
$(".sco_tag_checkbox").click(function () { // $(".sco_tag_checkbox").click(function () {
if ($(this).is(":checked")) { // if ($(this).is(":checked")) {
$(".sco_tag_edit").show(); // $(".sco_tag_edit").show();
} else { // } else {
$(".sco_tag_edit").hide(); // $(".sco_tag_edit").hide();
} // }
}); // });
}); });
// tags
function toggleEditDisplay(checkbox) {
const isChecked = checkbox.checked;
document.querySelectorAll('.sco_tag_edit').forEach(el => {
el.style.display = isChecked ? 'block' : 'none';
});
// form semection de semestres:
const showTagsInput = document.querySelector('input[name="show_tags"]');
if (showTagsInput) {
showTagsInput.value = isChecked ? '1' : '0';
}
}
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.sco_tag_checkbox').forEach(checkbox => {
// Set up initial state for each checkbox
toggleEditDisplay(checkbox);
// Add click event listener to each checkbox
checkbox.addEventListener('click', function() {
toggleEditDisplay(this);
});
});
});

View File

@ -100,7 +100,7 @@ div.submit > input {
{{ form.description() }} {{ form.description() }}
{{ render_field_errors(form, 'description') }} {{ render_field_errors(form, 'description') }}
</div> </div>
{# Date dépot #} {# Date dépôt #}
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }} {{ form.entry_date.label }}&nbsp;: {{ form.entry_date }}
<span class="help">laisser vide pour date courante</span> <span class="help">laisser vide pour date courante</span>
{{ render_field_errors(form, 'entry_date') }} {{ render_field_errors(form, 'entry_date') }}

View File

@ -123,9 +123,9 @@ div.submit > input {
{{ render_field_errors(form, 'fichiers') }} {{ render_field_errors(form, 'fichiers') }}
</div> </div>
</div> </div>
{# Date dépot #} {# Date dépôt #}
{{ form.entry_date.label }}&nbsp;: {{ form.entry_date }} {{ form.entry_date.label }}&nbsp;: {{ form.entry_date }} à {{ form.entry_time }}
<span class="help">laisser vide pour date courante</span> <span class="help" style="margin-left: 12px;">laisser vide pour date courante</span>
{{ render_field_errors(form, 'entry_date') }} {{ render_field_errors(form, 'entry_date') }}
{# Submit #} {# Submit #}

View File

@ -334,7 +334,7 @@ def _get_dates_from_assi_form(
dt_fin = datetime.datetime.combine(date_fin or date_debut, heure_fin) dt_fin = datetime.datetime.combine(date_fin or date_debut, heure_fin)
if dt_fin <= dt_debut: if dt_fin <= dt_debut:
form.set_error("dates début/fin incohérentes") form.set_error("dates début/fin incohérentes")
# La date de dépot (si vide, la date actuelle) # La date de dépôt (si vide, la date actuelle)
try: try:
dt_entry_date = ( dt_entry_date = (
datetime.datetime.strptime(form.entry_date.data, "%d/%m/%Y") datetime.datetime.strptime(form.entry_date.data, "%d/%m/%Y")
@ -344,7 +344,16 @@ def _get_dates_from_assi_form(
except ValueError: except ValueError:
dt_entry_date = None dt_entry_date = None
form.set_error("format de date de dépôt invalide", form.entry_date) form.set_error("format de date de dépôt invalide", form.entry_date)
# L'heure de dépôt
try:
entry_time = datetime.time.fromisoformat(
form.entry_time.data or datetime.datetime.now().time().isoformat("seconds")
)
except ValueError:
dt_entry_date = None
form.set_error("format d'heure de dépôt invalide", form.entry_date)
if dt_entry_date:
dt_entry_date = datetime.datetime.combine(dt_entry_date, entry_time)
# Ajoute time zone serveur # Ajoute time zone serveur
dt_debut_tz_server = scu.TIME_ZONE.localize(dt_debut) dt_debut_tz_server = scu.TIME_ZONE.localize(dt_debut)
dt_fin_tz_server = scu.TIME_ZONE.localize(dt_fin) dt_fin_tz_server = scu.TIME_ZONE.localize(dt_fin)
@ -576,6 +585,9 @@ def edit_justificatif_etud(justif_id: int):
form.entry_date.data = ( form.entry_date.data = (
justif.entry_date.strftime("%d/%m/%Y") if justif.entry_date else "" justif.entry_date.strftime("%d/%m/%Y") if justif.entry_date else ""
) )
form.entry_time.data = (
justif.entry_date.strftime("%H:%M") if justif.entry_date else ""
)
form.etat.data = str(justif.etat) form.etat.data = str(justif.etat)
redirect_url = url_for( redirect_url = url_for(

View File

@ -623,6 +623,29 @@ sco_publish("/module_list", sco_edit_module.module_table, Permission.ScoView)
sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView) sco_publish("/module_tag_search", sco_tag_module.module_tag_search, Permission.ScoView)
@bp.route("/formation_tag_modules_by_type/<int:formation_id>/<int:semestre_idx>")
@scodoc
@permission_required(Permission.EditFormationTags)
def formation_tag_modules_by_type(formation_id: int, semestre_idx: int):
"""Taggue tous les modules de la formation en fonction de leur type : 'res', 'sae', 'malus'
Ne taggue pas les modules standards.
"""
formation = Formation.query.filter_by(
id=formation_id, dept_id=g.scodoc_dept_id
).first_or_404()
sco_tag_module.formation_tag_modules_by_type(formation)
flash("Formation tagguée")
return flask.redirect(
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
semestre_idx=semestre_idx,
formation_id=formation.id,
show_tags=1,
)
)
@bp.route("/module_tag_set", methods=["POST"]) @bp.route("/module_tag_set", methods=["POST"])
@scodoc @scodoc
@permission_required(Permission.EditFormationTags) @permission_required(Permission.EditFormationTags)

View File

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