Merge branch 'dev92' of https://scodoc.org/git/ScoDoc/ScoDoc into api

This commit is contained in:
leonard_montalbano 2022-03-09 16:08:49 +01:00
commit 28ec8a482a
44 changed files with 1378 additions and 533 deletions

View File

@ -20,10 +20,10 @@ Flask, SQLAlchemy, au lien de Python2/Zope dans les versions précédentes).
### État actuel (26 jan 22) ### État actuel (26 jan 22)
- 9.1 (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf: - 9.1.5x (master) reproduit l'ensemble des fonctions de ScoDoc 7 (donc pas de BUT), sauf:
- ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT. - ancien module "Entreprises" (obsolète) et ajoute la gestion du BUT.
- 9.2 (branche refactor_nt) est la version de développement. - 9.2 (branche dev92) est la version de développement.
### Lignes de commandes ### Lignes de commandes

View File

@ -295,10 +295,12 @@ def create_app(config_class=DevConfig):
from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.but.bulletin_but_pdf import BulletinGeneratorStandardBUT
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
# l'ordre est important, le premier sera le "défaut" pour les nouveaux départements. # l'ordre est important, le premier sera le "défaut" pour les nouveaux départements.
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandardBUT)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC)
if app.testing or app.debug: if app.testing or app.debug:

View File

@ -9,14 +9,15 @@
import datetime import datetime
from flask import url_for, g from flask import url_for, g
from app.models.formsemestre import FormSemestre
from app.scodoc import sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import fmt_note
from app.comp.res_but import ResultatsSemestreBUT from app.comp.res_but import ResultatsSemestreBUT
from app.models import FormSemestre, Identite, formsemestre
from app.scodoc import sco_bulletins, sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_utils import fmt_note
class BulletinBUT: class BulletinBUT:
@ -28,6 +29,7 @@ class BulletinBUT:
def __init__(self, formsemestre: FormSemestre): def __init__(self, formsemestre: FormSemestre):
""" """ """ """
self.res = ResultatsSemestreBUT(formsemestre) self.res = ResultatsSemestreBUT(formsemestre)
self.prefs = sco_preferences.SemPreferences(formsemestre.id)
def etud_ue_mod_results(self, etud, ue, modimpls) -> dict: def etud_ue_mod_results(self, etud, ue, modimpls) -> dict:
"dict synthèse résultats dans l'UE pour les modules indiqués" "dict synthèse résultats dans l'UE pour les modules indiqués"
@ -84,7 +86,7 @@ class BulletinBUT:
"saes": self.etud_ue_mod_results(etud, ue, res.saes), "saes": self.etud_ue_mod_results(etud, ue, res.saes),
} }
if ue.type != UE_SPORT: if ue.type != UE_SPORT:
if sco_preferences.get_preference("bul_show_ue_rangs", res.formsemestre.id): if self.prefs["bul_show_ue_rangs"]:
rangs, effectif = res.ue_rangs[ue.id] rangs, effectif = res.ue_rangs[ue.id]
rang = rangs[etud.id] rang = rangs[etud.id]
else: else:
@ -109,9 +111,10 @@ class BulletinBUT:
d["modules"] = self.etud_mods_results(etud, modimpls_spo) d["modules"] = self.etud_mods_results(etud, modimpls_spo)
return d return d
def etud_mods_results(self, etud, modimpls) -> dict: def etud_mods_results(self, etud, modimpls, version="long") -> dict:
"""dict synthèse résultats des modules indiqués, """dict synthèse résultats des modules indiqués,
avec évaluations de chacun.""" avec évaluations de chacun (sauf si version == "short")
"""
res = self.res res = self.res
d = {} d = {}
# etud_idx = self.etud_index[etud.id] # etud_idx = self.etud_index[etud.id]
@ -152,14 +155,14 @@ class BulletinBUT:
"evaluations": [ "evaluations": [
self.etud_eval_results(etud, e) self.etud_eval_results(etud, e)
for e in modimpl.evaluations for e in modimpl.evaluations
if e.visibulletin if (e.visibulletin or version == "long")
and ( and (
modimpl_results.evaluations_etat[e.id].is_complete modimpl_results.evaluations_etat[e.id].is_complete
or sco_preferences.get_preference( or self.prefs["bul_show_all_evals"]
"bul_show_all_evals", res.formsemestre.id
) )
) ]
], if version != "short"
else [],
} }
return d return d
@ -216,13 +219,23 @@ class BulletinBUT:
else: else:
return f"Bonus de {fmt_note(bonus_vect.iloc[0])}" return f"Bonus de {fmt_note(bonus_vect.iloc[0])}"
def bulletin_etud(self, etud, formsemestre, force_publishing=False) -> dict: def bulletin_etud(
"""Le bulletin de l'étudiant dans ce semestre. self,
Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai etud: Identite,
formsemestre: FormSemestre,
force_publishing=False,
version="long",
) -> dict:
"""Le bulletin de l'étudiant dans ce semestre: dict pour la version JSON / HTML.
- version:
"long", "selectedevals": toutes les infos (notes des évaluations)
"short" : ne descend pas plus bas que les modules.
- Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
(bulletins non publiés). (bulletins non publiés).
""" """
res = self.res res = self.res
etat_inscription = etud.etat_inscription(formsemestre.id) etat_inscription = etud.inscription_etat(formsemestre.id)
nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT] nb_inscrits = self.res.get_inscriptions_counts()[scu.INSCRIT]
published = (not formsemestre.bul_hide_xml) or force_publishing published = (not formsemestre.bul_hide_xml) or force_publishing
d = { d = {
@ -239,7 +252,9 @@ class BulletinBUT:
}, },
"formsemestre_id": formsemestre.id, "formsemestre_id": formsemestre.id,
"etat_inscription": etat_inscription, "etat_inscription": etat_inscription,
"options": sco_preferences.bulletin_option_affichage(formsemestre.id), "options": sco_preferences.bulletin_option_affichage(
formsemestre.id, self.prefs
),
} }
if not published: if not published:
return d return d
@ -278,8 +293,10 @@ class BulletinBUT:
) )
d.update( d.update(
{ {
"ressources": self.etud_mods_results(etud, res.ressources), "ressources": self.etud_mods_results(
"saes": self.etud_mods_results(etud, res.saes), etud, res.ressources, version=version
),
"saes": self.etud_mods_results(etud, res.saes, version=version),
"ues": { "ues": {
ue.acronyme: self.etud_ue_results(etud, ue) ue.acronyme: self.etud_ue_results(etud, ue)
for ue in res.ues for ue in res.ues
@ -312,3 +329,54 @@ class BulletinBUT:
) )
return d return d
def bulletin_etud_complet(self, etud: Identite) -> dict:
"""Bulletin dict complet avec toutes les infos pour les bulletins BUT pdf
Résultat compatible avec celui de sco_bulletins.formsemestre_bulletinetud_dict
"""
d = self.bulletin_etud(etud, self.res.formsemestre, force_publishing=True)
d["etudid"] = etud.id
d["etud"] = d["etudiant"]
d["etud"]["nomprenom"] = etud.nomprenom
d.update(self.res.sem)
etud_etat = self.res.get_etud_etat(etud.id)
d["filigranne"] = sco_bulletins_pdf.get_filigranne(
etud_etat,
self.prefs,
decision_sem=d["semestre"].get("decision_sem"),
)
if etud_etat == scu.DEMISSION:
d["demission"] = "(Démission)"
elif etud_etat == DEF:
d["demission"] = "(Défaillant)"
else:
d["demission"] = ""
# --- Absences
d["nbabs"], d["nbabsjust"] = self.res.formsemestre.get_abs_count(etud.id)
# --- Decision Jury
infos, dpv = sco_bulletins.etud_descr_situation_semestre(
etud.id,
self.res.formsemestre.id,
format="html",
show_date_inscr=self.prefs["bul_show_date_inscr"],
show_decisions=self.prefs["bul_show_decision"],
show_uevalid=self.prefs["bul_show_uevalid"],
show_mention=self.prefs["bul_show_mention"],
)
d.update(infos)
# --- Rangs
d[
"rang_nt"
] = f"{d['semestre']['rang']['value']} / {d['semestre']['rang']['total']}"
d["rang_txt"] = "Rang " + d["rang_nt"]
# --- Appréciations
d.update(
sco_bulletins.get_appreciations_list(self.res.formsemestre.id, etud.id)
)
d.update(sco_bulletins.make_context_dict(self.res.formsemestre, d["etud"]))
return d

116
app/but/bulletin_but_pdf.py Normal file
View File

@ -0,0 +1,116 @@
##############################################################################
# ScoDoc
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
# See LICENSE
##############################################################################
"""Génération bulletin BUT au format PDF standard
"""
import datetime
from app.scodoc.sco_pdf import blue, cm, mm
from flask import url_for, g
from app.models.formsemestre import FormSemestre
from app.scodoc import gen_tables
from app.scodoc import sco_utils as scu
from app.scodoc import sco_bulletins_json
from app.scodoc import sco_preferences
from app.scodoc.sco_codes_parcours import UE_SPORT
from app.scodoc.sco_utils import fmt_note
from app.comp.res_but import ResultatsSemestreBUT
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
class BulletinGeneratorStandardBUT(BulletinGeneratorStandard):
"""Génération du bulletin de BUT au format PDF.
self.infos est le dict issu de BulletinBUT.bulletin_etud_complet()
"""
list_in_menu = False # spécialisation du BulletinGeneratorStandard, ne pas présenter à l'utilisateur
def bul_table(self, format="html"):
"""Génère la table centrale du bulletin de notes
Renvoie:
- en HTML: une chaine
- en PDF: une liste d'objets PLATYPUS (eg instance de Table).
"""
formsemestre_id = self.infos["formsemestre_id"]
(
synth_col_keys,
synth_P,
synth_pdf_style,
synth_col_widths,
) = self.but_table_synthese()
#
table_synthese = gen_tables.GenTable(
rows=synth_P,
columns_ids=synth_col_keys,
pdf_table_style=synth_pdf_style,
pdf_col_widths=[synth_col_widths[k] for k in synth_col_keys],
preferences=self.preferences,
html_class="notes_bulletin",
html_class_ignore_default=True,
html_with_td_classes=True,
)
# Ici on ajoutera table des ressources, tables des UE
# TODO
# XXX à modifier pour générer plusieurs tables:
return table_synthese.gen(format=format)
def but_table_synthese(self):
"""La table de synthèse; pour chaque UE, liste des ressources et SAÉs avec leurs notes
et leurs coefs.
Renvoie: colkeys, P, pdf_style, colWidths
- colkeys: nom des colonnes de la table (clés)
- P : table (liste de dicts de chaines de caracteres)
- pdf_style : commandes table Platypus
- largeurs de colonnes pour PDF
"""
col_widths = {
"titre": None,
"moyenne": 2 * cm,
"coef": 2 * cm,
}
P = [] # elems pour générer table avec gen_table (liste de dicts)
col_keys = ["titre", "moyenne"] # noms des colonnes à afficher
for ue_acronym, ue in self.infos["ues"].items():
# 1er ligne titre UE
moy_ue = ue.get("moyenne")
t = {
"titre": f"{ue_acronym} - {ue['titre']}",
"moyenne": moy_ue.get("value", "-") if moy_ue is not None else "-",
"_css_row_class": "note_bold",
"_pdf_row_markup": ["b"],
"_pdf_style": [],
}
P.append(t)
# 2eme ligne titre UE (bonus/malus/ects)
t = {
"titre": "",
"moyenne": f"""Bonus: {ue['bonus']} - Malus: {
ue["malus"]} - ECTS: {ue["ECTS"]["acquis"]} / {ue["ECTS"]["total"]}""",
"_css_row_class": "note_bold",
"_pdf_row_markup": ["b"],
"_pdf_style": [
(
"LINEBELOW",
(0, 0),
(-1, 0),
self.PDF_LINEWIDTH,
self.PDF_LINECOLOR,
)
],
}
P.append(t)
# Global pdf style commands:
pdf_style = [
("VALIGN", (0, 0), (-1, -1), "TOP"),
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
]
return col_keys, P, pdf_style, col_widths

View File

@ -72,7 +72,7 @@ def bulletin_but_xml_compat(
etud: Identite = Identite.query.get_or_404(etudid) etud: Identite = Identite.query.get_or_404(etudid)
results = bulletin_but.ResultatsSemestreBUT(formsemestre) results = bulletin_but.ResultatsSemestreBUT(formsemestre)
nb_inscrits = results.get_inscriptions_counts()[scu.INSCRIT] nb_inscrits = results.get_inscriptions_counts()[scu.INSCRIT]
# etat_inscription = etud.etat_inscription(formsemestre.id) # etat_inscription = etud.inscription_etat(formsemestre.id)
etat_inscription = results.formsemestre.etuds_inscriptions[etudid].etat etat_inscription = results.formsemestre.etuds_inscriptions[etudid].etat
if (not formsemestre.bul_hide_xml) or force_publishing: if (not formsemestre.bul_hide_xml) or force_publishing:
published = 1 published = 1

View File

@ -4,12 +4,14 @@
et données rattachées (adresses, annotations, ...) et données rattachées (adresses, annotations, ...)
""" """
import datetime
from functools import cached_property from functools import cached_property
from flask import abort, url_for from flask import abort, url_for
from flask import g, request from flask import g, request
import sqlalchemy import sqlalchemy
from sqlalchemy import desc, text
from app import db from app import db, log
from app import models from app import models
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
@ -82,6 +84,11 @@ class Identite(db.Model):
return scu.suppress_accents(s) return scu.suppress_accents(s)
return s return s
@property
def e(self):
"terminaison en français: 'ne', '', 'ou '(e)'"
return {"M": "", "F": "e"}.get(self.civilite, "(e)")
def nom_disp(self) -> str: def nom_disp(self) -> str:
"Nom à afficher" "Nom à afficher"
if self.nom_usuel: if self.nom_usuel:
@ -123,7 +130,7 @@ class Identite(db.Model):
def get_first_email(self, field="email") -> str: def get_first_email(self, field="email") -> str:
"Le mail associé à la première adrese de l'étudiant, ou None" "Le mail associé à la première adrese de l'étudiant, ou None"
return self.adresses[0].email or None if self.adresses.count() > 0 else None return getattr(self.adresses[0], field) if self.adresses.count() > 0 else None
def to_dict_scodoc7(self): def to_dict_scodoc7(self):
"""Représentation dictionnaire, """Représentation dictionnaire,
@ -134,7 +141,7 @@ class Identite(db.Model):
# ScoDoc7 output_formators: (backward compat) # ScoDoc7 output_formators: (backward compat)
e["etudid"] = self.id e["etudid"] = self.id
e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"]) e["date_naissance"] = ndb.DateISOtoDMY(e["date_naissance"])
e["ne"] = {"M": "", "F": "ne"}.get(self.civilite, "(e)") e["ne"] = self.e
return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty return {k: e[k] or "" for k in e} # convert_null_outputs_to_empty
def to_dict_bul(self, include_urls=True): def to_dict_bul(self, include_urls=True):
@ -153,6 +160,7 @@ class Identite(db.Model):
"etudid": self.id, "etudid": self.id,
"nom": self.nom_disp(), "nom": self.nom_disp(),
"prenom": self.prenom, "prenom": self.prenom,
"nomprenom": self.nomprenom,
} }
if include_urls: if include_urls:
d["fiche_url"] = url_for( d["fiche_url"] = url_for(
@ -172,6 +180,23 @@ class Identite(db.Model):
] ]
return r[0] if r else None return r[0] if r else None
def inscriptions_courantes(self) -> list: # -> list[FormSemestreInscription]:
"""Liste des inscriptions à des semestres _courants_
(il est rare qu'il y en ai plus d'une, mais c'est possible).
Triées par date de début de semestre décroissante (le plus récent en premier).
"""
from app.models.formsemestre import FormSemestre, FormSemestreInscription
return (
FormSemestreInscription.query.join(FormSemestreInscription.formsemestre)
.filter(
FormSemestreInscription.etudid == self.id,
text("date_debut < now() and date_fin > now()"),
)
.order_by(desc(FormSemestre.date_debut))
.all()
)
def inscription_courante_date(self, date_debut, date_fin): def inscription_courante_date(self, date_debut, date_fin):
"""La première inscription à un formsemestre incluant la """La première inscription à un formsemestre incluant la
période [date_debut, date_fin] période [date_debut, date_fin]
@ -183,8 +208,8 @@ class Identite(db.Model):
] ]
return r[0] if r else None return r[0] if r else None
def etat_inscription(self, formsemestre_id): def inscription_etat(self, formsemestre_id):
"""etat de l'inscription de cet étudiant au semestre: """État de l'inscription de cet étudiant au semestre:
False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF False si pas inscrit, ou scu.INSCRIT, DEMISSION, DEF
""" """
# voir si ce n'est pas trop lent: # voir si ce n'est pas trop lent:
@ -195,6 +220,110 @@ class Identite(db.Model):
return ins.etat return ins.etat
return False return False
def inscription_descr(self) -> dict:
"""Description de l'état d'inscription"""
inscription_courante = self.inscription_courante()
if inscription_courante:
titre_sem = inscription_courante.formsemestre.titre_mois()
return {
"etat_in_cursem": inscription_courante.etat,
"inscription_courante": inscription_courante,
"inscription": titre_sem,
"inscription_str": "Inscrit en " + titre_sem,
"situation": self.descr_situation_etud(),
}
else:
if self.formsemestre_inscriptions:
# cherche l'inscription la plus récente:
fin_dernier_sem = max(
[
inscr.formsemestre.date_debut
for inscr in self.formsemestre_inscriptions
]
)
if fin_dernier_sem > datetime.date.today():
inscription = "futur"
situation = "futur élève"
else:
inscription = "ancien"
situation = "ancien élève"
else:
inscription = ("non inscrit",)
situation = inscription
return {
"etat_in_cursem": "?",
"inscription_courante": None,
"inscription": inscription,
"inscription_str": inscription,
"situation": situation,
}
def descr_situation_etud(self) -> str:
"""Chaîne décrivant la situation _actuelle_ de l'étudiant.
Exemple:
"inscrit en BUT R&T semestre 2 FI (Jan 2022 - Jul 2022) le 16/01/2022"
ou
"non inscrit"
"""
inscriptions_courantes = self.inscriptions_courantes()
if inscriptions_courantes:
inscr = inscriptions_courantes[0]
if inscr.etat == scu.INSCRIT:
situation = f"inscrit{self.e} en {inscr.formsemestre.titre_mois()}"
# Cherche la date d'inscription dans scolar_events:
events = models.ScolarEvent.query.filter_by(
etudid=self.id,
formsemestre_id=inscr.formsemestre.id,
event_type="INSCRIPTION",
).all()
if not events:
log(
f"*** situation inconsistante pour {self} (inscrit mais pas d'event)"
)
date_ins = "???" # ???
else:
date_ins = events[0].event_date
situation += date_ins.strftime(" le %d/%m/%Y")
else:
situation = f"démission de {inscr.formsemestre.titre_mois()}"
# Cherche la date de demission dans scolar_events:
events = models.ScolarEvent.query.filter_by(
etudid=self.id,
formsemestre_id=inscr.formsemestre.id,
event_type="DEMISSION",
).all()
if not events:
log(
f"*** situation inconsistante pour {self} (demission mais pas d'event)"
)
date_dem = "???" # ???
else:
date_dem = events[0].event_date
situation += date_dem.strftime(" le %d/%m/%Y")
else:
situation = "non inscrit" + self.e
return situation
def photo_html(self, title=None, size="small") -> str:
"""HTML img tag for the photo, either in small size (h90)
or original size (size=="orig")
"""
from app.scodoc import sco_photos
# sco_photo traite des dicts:
return sco_photos.etud_photo_html(
etud=dict(
etudid=self.id,
code_nip=self.code_nip,
nomprenom=self.nomprenom,
nom_disp=self.nom_disp(),
photo_filename=self.photo_filename,
),
title=title,
size=size,
)
def make_etud_args( def make_etud_args(
etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True etudid=None, code_nip=None, use_request=True, raise_exc=False, abort_404=True

View File

@ -12,7 +12,6 @@ from app import log
from app.models import APO_CODE_STR_LEN from app.models import APO_CODE_STR_LEN
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
from app.models import CODE_STR_LEN from app.models import CODE_STR_LEN
from app.models import UniteEns
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.models.ues import UniteEns from app.models.ues import UniteEns
@ -23,6 +22,7 @@ from app.scodoc import sco_codes_parcours
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_utils import MONTH_NAMES_ABBREV
class FormSemestre(db.Model): class FormSemestre(db.Model):
@ -122,6 +122,7 @@ class FormSemestre(db.Model):
return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>" return f"<{self.__class__.__name__} {self.id} {self.titre_num()}>"
def to_dict(self): def to_dict(self):
"dict (compatible ScoDoc7)"
d = dict(self.__dict__) d = dict(self.__dict__)
d.pop("_sa_instance_state", None) d.pop("_sa_instance_state", None)
# ScoDoc7 output_formators: (backward compat) # ScoDoc7 output_formators: (backward compat)
@ -162,8 +163,8 @@ class FormSemestre(db.Model):
d["periode"] = 2 # typiquement, début en février: S2, S4... d["periode"] = 2 # typiquement, début en février: S2, S4...
d["titre_num"] = self.titre_num() d["titre_num"] = self.titre_num()
d["titreannee"] = self.titre_annee() d["titreannee"] = self.titre_annee()
d["mois_debut"] = f"{self.date_debut.month} {self.date_debut.year}" d["mois_debut"] = self.mois_debut()
d["mois_fin"] = f"{self.date_fin.month} {self.date_fin.year}" d["mois_fin"] = self.mois_fin()
d["titremois"] = "%s %s (%s - %s)" % ( d["titremois"] = "%s %s (%s - %s)" % (
d["titre_num"], d["titre_num"],
self.modalite or "", self.modalite or "",
@ -293,6 +294,7 @@ class FormSemestre(db.Model):
"""chaîne "J. Dupond, X. Martin" """chaîne "J. Dupond, X. Martin"
ou "Jacques Dupond, Xavier Martin" ou "Jacques Dupond, Xavier Martin"
""" """
# was "nomcomplet"
if not self.responsables: if not self.responsables:
return "" return ""
if abbrev_prenom: if abbrev_prenom:
@ -304,6 +306,14 @@ class FormSemestre(db.Model):
"2021 - 2022" "2021 - 2022"
return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month) return scu.annee_scolaire_repr(self.date_debut.year, self.date_debut.month)
def mois_debut(self) -> str:
"Oct 2021"
return f"{MONTH_NAMES_ABBREV[self.date_debut.month - 1]} {self.date_debut.year}"
def mois_fin(self) -> str:
"Jul 2022"
return f"{MONTH_NAMES_ABBREV[self.date_fin.month - 1]} {self.date_debut.year}"
def session_id(self) -> str: def session_id(self) -> str:
"""identifiant externe de semestre de formation """identifiant externe de semestre de formation
Exemple: RT-DUT-FI-S1-ANNEE Exemple: RT-DUT-FI-S1-ANNEE
@ -364,7 +374,7 @@ class FormSemestre(db.Model):
def get_abs_count(self, etudid): def get_abs_count(self, etudid):
"""Les comptes d'absences de cet étudiant dans ce semestre: """Les comptes d'absences de cet étudiant dans ce semestre:
tuple (nb abs non justifiées, nb abs justifiées) tuple (nb abs, nb abs justifiées)
Utilise un cache. Utilise un cache.
""" """
from app.scodoc import sco_abs from app.scodoc import sco_abs

View File

@ -6,7 +6,8 @@ import flask_sqlalchemy
from app import db from app import db
from app.comp import df_cache from app.comp import df_cache
from app.models import Identite, Module from app.models.etudiants import Identite
from app.models.modules import Module
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu

View File

@ -233,6 +233,9 @@ class GenTable(object):
colspan_count -= 1 colspan_count -= 1
# if colspan_count > 0: # if colspan_count > 0:
# continue # skip cells after a span # continue # skip cells after a span
if pdf_mode:
content = row.get(f"_{cid}_pdf", "") or row.get(cid, "") or ""
else:
content = row.get(cid, "") or "" # nota: None converted to '' content = row.get(cid, "") or "" # nota: None converted to ''
colspan = row.get("_%s_colspan" % cid, 0) colspan = row.get("_%s_colspan" % cid, 0)
if colspan > 1: if colspan > 1:
@ -547,9 +550,16 @@ class GenTable(object):
omit_hidden_lines=True, omit_hidden_lines=True,
) )
try: try:
Pt = [ Pt = []
[Paragraph(SU(str(x)), CellStyle) for x in line] for line in data_list for line in data_list:
Pt.append(
[
Paragraph(SU(str(x)), CellStyle)
if (not isinstance(x, Paragraph))
else x
for x in line
] ]
)
except ValueError as exc: except ValueError as exc:
raise ScoPDFFormatError(str(exc)) from exc raise ScoPDFFormatError(str(exc)) from exc
pdf_style_list += self.pdf_table_style pdf_style_list += self.pdf_table_style

View File

@ -1037,7 +1037,7 @@ def get_abs_count(etudid, sem):
def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso): def get_abs_count_in_interval(etudid, date_debut_iso, date_fin_iso):
"""Les comptes d'absences de cet étudiant entre ces deux dates, incluses: """Les comptes d'absences de cet étudiant entre ces deux dates, incluses:
tuple (nb abs non justifiées, nb abs justifiées) tuple (nb abs, nb abs justifiées)
Utilise un cache. Utilise un cache.
""" """
key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso key = str(etudid) + "_" + date_debut_iso + "_" + date_fin_iso

View File

@ -35,6 +35,7 @@ import datetime
from flask import g, url_for from flask import g, url_for
from flask_mail import Message from flask_mail import Message
from app.models.formsemestre import FormSemestre
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -55,27 +56,30 @@ def abs_notify(etudid, date):
""" """
from app.scodoc import sco_abs from app.scodoc import sco_abs
sem = retreive_current_formsemestre(etudid, date) formsemestre = retreive_current_formsemestre(etudid, date)
if not sem: if not formsemestre:
return # non inscrit a la date, pas de notification return # non inscrit a la date, pas de notification
nbabs, nbabsjust = sco_abs.get_abs_count(etudid, sem) nbabs, nbabsjust = sco_abs.get_abs_count_in_interval(
do_abs_notify(sem, etudid, date, nbabs, nbabsjust) etudid, formsemestre.date_debut.isoformat(), formsemestre.date_fin.isoformat()
)
do_abs_notify(formsemestre, etudid, date, nbabs, nbabsjust)
def do_abs_notify(sem, etudid, date, nbabs, nbabsjust): def do_abs_notify(formsemestre: FormSemestre, etudid, date, nbabs, nbabsjust):
"""Given new counts of absences, check if notifications are requested and send them.""" """Given new counts of absences, check if notifications are requested and send them."""
# prefs fallback to global pref if sem is None: # prefs fallback to global pref if sem is None:
if sem: if formsemestre:
formsemestre_id = sem["formsemestre_id"] formsemestre_id = formsemestre.id
else: else:
formsemestre_id = None formsemestre_id = None
prefs = sco_preferences.SemPreferences(formsemestre_id=sem["formsemestre_id"]) prefs = sco_preferences.SemPreferences(formsemestre_id=formsemestre_id)
destinations = abs_notify_get_destinations( destinations = abs_notify_get_destinations(
sem, prefs, etudid, date, nbabs, nbabsjust formsemestre, prefs, etudid, date, nbabs, nbabsjust
) )
msg = abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust)
msg = abs_notification_message(formsemestre, prefs, etudid, nbabs, nbabsjust)
if not msg: if not msg:
return # abort return # abort
@ -131,19 +135,19 @@ def abs_notify_send(destinations, etudid, msg, nbabs, nbabsjust, formsemestre_id
) )
def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust): def abs_notify_get_destinations(
formsemestre: FormSemestre, prefs, etudid, date, nbabs, nbabsjust
) -> set:
"""Returns set of destination emails to be notified""" """Returns set of destination emails to be notified"""
formsemestre_id = sem["formsemestre_id"]
destinations = [] # list of email address to notify destinations = [] # list of email address to notify
if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre_id): if abs_notify_is_above_threshold(etudid, nbabs, nbabsjust, formsemestre.id):
if sem and prefs["abs_notify_respsem"]: if prefs["abs_notify_respsem"]:
# notifie chaque responsable du semestre # notifie chaque responsable du semestre
for responsable_id in sem["responsables"]: for responsable in formsemestre.responsables:
u = sco_users.user_info(responsable_id) if responsable.email:
if u["email"]: destinations.append(responsable.email)
destinations.append(u["email"])
if prefs["abs_notify_chief"] and prefs["email_chefdpt"]: if prefs["abs_notify_chief"] and prefs["email_chefdpt"]:
destinations.append(prefs["email_chefdpt"]) destinations.append(prefs["email_chefdpt"])
if prefs["abs_notify_email"]: if prefs["abs_notify_email"]:
@ -156,7 +160,7 @@ def abs_notify_get_destinations(sem, prefs, etudid, date, nbabs, nbabsjust):
# Notification (à chaque fois) des resp. de modules ayant des évaluations # Notification (à chaque fois) des resp. de modules ayant des évaluations
# à cette date # à cette date
# nb: on pourrait prevoir d'utiliser un autre format de message pour ce cas # nb: on pourrait prevoir d'utiliser un autre format de message pour ce cas
if sem and prefs["abs_notify_respeval"]: if prefs["abs_notify_respeval"]:
mods = mod_with_evals_at_date(date, etudid) mods = mod_with_evals_at_date(date, etudid)
for mod in mods: for mod in mods:
u = sco_users.user_info(mod["responsable_id"]) u = sco_users.user_info(mod["responsable_id"])
@ -232,7 +236,9 @@ def user_nbdays_since_last_notif(email_addr, etudid):
return None return None
def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust): def abs_notification_message(
formsemestre: FormSemestre, prefs, etudid, nbabs, nbabsjust
):
"""Mime notification message based on template. """Mime notification message based on template.
returns a Message instance returns a Message instance
or None if sending should be canceled (empty template). or None if sending should be canceled (empty template).
@ -242,7 +248,7 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust):
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
# Variables accessibles dans les balises du template: %(nom_variable)s : # Variables accessibles dans les balises du template: %(nom_variable)s :
values = sco_bulletins.make_context_dict(sem, etud) values = sco_bulletins.make_context_dict(formsemestre, etud)
values["nbabs"] = nbabs values["nbabs"] = nbabs
values["nbabsjust"] = nbabsjust values["nbabsjust"] = nbabsjust
@ -264,9 +270,11 @@ def abs_notification_message(sem, prefs, etudid, nbabs, nbabsjust):
return msg return msg
def retreive_current_formsemestre(etudid, cur_date): def retreive_current_formsemestre(etudid: int, cur_date) -> FormSemestre:
"""Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée """Get formsemestre dans lequel etudid est (ou était) inscrit a la date indiquée
date est une chaine au format ISO (yyyy-mm-dd) date est une chaine au format ISO (yyyy-mm-dd)
Result: FormSemestre ou None si pas inscrit à la date indiquée
""" """
req = """SELECT i.formsemestre_id req = """SELECT i.formsemestre_id
FROM notes_formsemestre_inscription i, notes_formsemestre sem FROM notes_formsemestre_inscription i, notes_formsemestre sem
@ -278,8 +286,8 @@ def retreive_current_formsemestre(etudid, cur_date):
if not r: if not r:
return None return None
# s'il y a plusieurs semestres, prend le premier (rarissime et non significatif): # s'il y a plusieurs semestres, prend le premier (rarissime et non significatif):
sem = sco_formsemestre.get_formsemestre(r[0]["formsemestre_id"]) formsemestre = FormSemestre.query.get(r[0]["formsemestre_id"])
return sem return formsemestre
def mod_with_evals_at_date(date_abs, etudid): def mod_with_evals_at_date(date_abs, etudid):

View File

@ -28,30 +28,19 @@
"""Génération des bulletins de notes """Génération des bulletins de notes
""" """
from app.models import formsemestre
import time
import pprint
import email import email
from email.mime.multipart import MIMEMultipart import time
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email.header import Header
from reportlab.lib.colors import Color
import urllib
from flask import g, request from flask import g, request
from flask import url_for from flask import render_template, url_for
from flask_login import current_user from flask_login import current_user
from flask_mail import Message
from app.models.moduleimpls import ModuleImplInscription
import app.scodoc.sco_utils as scu from app import email
from app.scodoc.sco_utils import ModuleType
import app.scodoc.notesdb as ndb
from app import log from app import log
from app.but import bulletin_but
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre, Identite, ModuleImplInscription
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError from app.scodoc.sco_exceptions import AccessDenied, ScoValueError
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
@ -60,9 +49,9 @@ from app.scodoc import sco_abs
from app.scodoc import sco_abs_views from app.scodoc import sco_abs_views
from app.scodoc import sco_bulletins_generator from app.scodoc import sco_bulletins_generator
from app.scodoc import sco_bulletins_json from app.scodoc import sco_bulletins_json
from app.scodoc import sco_bulletins_pdf
from app.scodoc import sco_bulletins_xml from app.scodoc import sco_bulletins_xml
from app.scodoc import sco_codes_parcours from app.scodoc import sco_codes_parcours
from app.scodoc import sco_cache
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_evaluation_db from app.scodoc import sco_evaluation_db
from app.scodoc import sco_formations from app.scodoc import sco_formations
@ -73,7 +62,9 @@ from app.scodoc import sco_photos
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_pvjury from app.scodoc import sco_pvjury
from app.scodoc import sco_users from app.scodoc import sco_users
from app import email import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType, fmt_note
import app.scodoc.notesdb as ndb
# ----- CLASSES DE BULLETINS DE NOTES # ----- CLASSES DE BULLETINS DE NOTES
from app.scodoc import sco_bulletins_standard from app.scodoc import sco_bulletins_standard
@ -85,33 +76,20 @@ from app.scodoc import sco_bulletins_legacy
from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun from app.scodoc import sco_bulletins_ucac # format expérimental UCAC Cameroun
def make_context_dict(sem, etud): def make_context_dict(formsemestre: FormSemestre, etud: dict) -> dict:
"""Construit dictionnaire avec valeurs pour substitution des textes """Construit dictionnaire avec valeurs pour substitution des textes
(preferences bul_pdf_*) (preferences bul_pdf_*)
""" """
C = sem.copy() C = formsemestre.get_infos_dict()
C["responsable"] = " ,".join( C["responsable"] = formsemestre.responsables_str()
[ C["anneesem"] = C["annee"] # backward compat
sco_users.user_info(responsable_id)["prenomnom"]
for responsable_id in sem["responsables"]
]
)
annee_debut = sem["date_debut"].split("/")[2]
annee_fin = sem["date_fin"].split("/")[2]
if annee_debut != annee_fin:
annee = "%s - %s" % (annee_debut, annee_fin)
else:
annee = annee_debut
C["anneesem"] = annee
C.update(etud) C.update(etud)
# copie preferences # copie preferences
# XXX devrait acceder directement à un dict de preferences, à revoir
for name in sco_preferences.get_base_preferences().prefs_name: for name in sco_preferences.get_base_preferences().prefs_name:
C[name] = sco_preferences.get_preference(name, sem["formsemestre_id"]) C[name] = sco_preferences.get_preference(name, formsemestre.id)
# ajoute groupes et group_0, group_1, ... # ajoute groupes et group_0, group_1, ...
sco_groups.etud_add_group_infos(etud, sem) sco_groups.etud_add_group_infos(etud, formsemestre.id)
C["groupes"] = etud["groupes"] C["groupes"] = etud["groupes"]
n = 0 n = 0
for partition_id in etud["partitions"]: for partition_id in etud["partitions"]:
@ -132,7 +110,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
Le contenu du dictionnaire dépend des options (rangs, ...) Le contenu du dictionnaire dépend des options (rangs, ...)
et de la version choisie (short, long, selectedevals). et de la version choisie (short, long, selectedevals).
Cette fonction est utilisée pour les bulletins HTML et PDF, mais pas ceux en XML. Cette fonction est utilisée pour les bulletins CLASSIQUES (DUT, ...)
en HTML et PDF, mais pas ceux en XML.
""" """
from app.scodoc import sco_abs from app.scodoc import sco_abs
@ -190,39 +169,23 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
show_mention=prefs["bul_show_mention"], show_mention=prefs["bul_show_mention"],
) )
if dpv:
I["decision_sem"] = dpv["decisions"][0]["decision_sem"]
else:
I["decision_sem"] = ""
I.update(infos) I.update(infos)
I["etud_etat_html"] = _get_etud_etat_html( I["etud_etat_html"] = _get_etud_etat_html(
formsemestre.etuds_inscriptions[etudid].etat formsemestre.etuds_inscriptions[etudid].etat
) )
I["etud_etat"] = nt.get_etud_etat(etudid) I["etud_etat"] = nt.get_etud_etat(etudid)
I["filigranne"] = "" I["filigranne"] = sco_bulletins_pdf.get_filigranne(
I["etud_etat"], prefs, decision_sem=I["decision_sem"]
)
I["demission"] = "" I["demission"] = ""
if I["etud_etat"] == "D": if I["etud_etat"] == scu.DEMISSION:
I["demission"] = "(Démission)" I["demission"] = "(Démission)"
I["filigranne"] = "Démission"
elif I["etud_etat"] == sco_codes_parcours.DEF: elif I["etud_etat"] == sco_codes_parcours.DEF:
I["demission"] = "(Défaillant)" I["demission"] = "(Défaillant)"
I["filigranne"] = "Défaillant"
elif (prefs["bul_show_temporary"] and not I["decision_sem"]) or prefs[
"bul_show_temporary_forced"
]:
I["filigranne"] = prefs["bul_temporary_txt"]
# --- Appreciations # --- Appreciations
cnx = ndb.GetDBConnexion() I.update(get_appreciations_list(formsemestre_id, etudid))
apprecs = sco_etud.appreciations_list(
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
)
I["appreciations_list"] = apprecs
I["appreciations_txt"] = [x["date"] + ": " + x["comment"] for x in apprecs]
I["appreciations"] = I[
"appreciations_txt"
] # deprecated / keep it for backward compat in templates
# --- Notes # --- Notes
ues = nt.get_ues_stat_dict() ues = nt.get_ues_stat_dict()
@ -316,7 +279,7 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
else: else:
u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs" u["cur_moy_ue_txt"] = "bonus appliqué sur les UEs"
else: else:
u["cur_moy_ue_txt"] = "bonus de %.3g points" % x u["cur_moy_ue_txt"] = f"bonus de {fmt_note(x)} points"
if nt.bonus_ues is not None: if nt.bonus_ues is not None:
u["cur_moy_ue_txt"] += " (+ues)" u["cur_moy_ue_txt"] += " (+ues)"
u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"]) u["moy_ue_txt"] = scu.fmt_note(ue_status["moy"])
@ -407,13 +370,28 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid)) I["matieres_modules"].update(_sort_mod_by_matiere(modules, nt, etudid))
# #
C = make_context_dict(I["sem"], I["etud"]) C = make_context_dict(formsemestre, I["etud"])
C.update(I) C.update(I)
# #
# log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo # log( 'C = \n%s\n' % pprint.pformat(C) ) # tres pratique pour voir toutes les infos dispo
return C return C
def get_appreciations_list(formsemestre_id: int, etudid: int) -> dict:
"""Appréciations pour cet étudiant dans ce semestre"""
cnx = ndb.GetDBConnexion()
apprecs = sco_etud.appreciations_list(
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
)
d = {
"appreciations_list": apprecs,
"appreciations_txt": [x["date"] + ": " + x["comment"] for x in apprecs],
}
# deprecated / keep it for backward compat in templates:
d["appreciations"] = d["appreciations_txt"]
return d
def _get_etud_etat_html(etat: str) -> str: def _get_etud_etat_html(etat: str) -> str:
"""chaine html représentant l'état (backward compat sco7)""" """chaine html représentant l'état (backward compat sco7)"""
if etat == scu.INSCRIT: # "I" if etat == scu.INSCRIT: # "I"
@ -691,6 +669,7 @@ def etud_descr_situation_semestre(
descr_defaillance : "Défaillant" ou vide si non défaillant. descr_defaillance : "Défaillant" ou vide si non défaillant.
decision_jury : "Validé", "Ajourné", ... (code semestre) decision_jury : "Validé", "Ajourné", ... (code semestre)
descr_decision_jury : "Décision jury: Validé" (une phrase) descr_decision_jury : "Décision jury: Validé" (une phrase)
decision_sem :
decisions_ue : noms (acronymes) des UE validées, séparées par des virgules. decisions_ue : noms (acronymes) des UE validées, séparées par des virgules.
descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid descr_decisions_ue : ' UE acquises: UE1, UE2', ou vide si pas de dec. ou si pas show_uevalid
descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention descr_mention : 'Mention Bien', ou vide si pas de mention ou si pas show_mention
@ -700,7 +679,7 @@ def etud_descr_situation_semestre(
# --- Situation et décisions jury # --- Situation et décisions jury
# demission/inscription ? # démission/inscription ?
events = sco_etud.scolar_events_list( events = sco_etud.scolar_events_list(
cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id} cnx, args={"etudid": etudid, "formsemestre_id": formsemestre_id}
) )
@ -767,11 +746,15 @@ def etud_descr_situation_semestre(
infos["situation"] += " " + infos["descr_defaillance"] infos["situation"] += " " + infos["descr_defaillance"]
dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid]) dpv = sco_pvjury.dict_pvjury(formsemestre_id, etudids=[etudid])
if dpv:
infos["decision_sem"] = dpv["decisions"][0]["decision_sem"]
else:
infos["decision_sem"] = ""
if not show_decisions: if not show_decisions:
return infos, dpv return infos, dpv
# Decisions de jury: # Décisions de jury:
pv = dpv["decisions"][0] pv = dpv["decisions"][0]
dec = "" dec = ""
if pv["decision_sem_descr"]: if pv["decision_sem_descr"]:
@ -810,24 +793,21 @@ def etud_descr_situation_semestre(
def formsemestre_bulletinetud( def formsemestre_bulletinetud(
etudid=None, etudid=None,
formsemestre_id=None, formsemestre_id=None,
format="html", format=None,
version="long", version="long",
xml_with_decisions=False, xml_with_decisions=False,
force_publishing=False, # force publication meme si semestre non publie sur "portail" force_publishing=False, # force publication meme si semestre non publie sur "portail"
prefer_mail_perso=False, prefer_mail_perso=False,
): ):
"page bulletin de notes" "page bulletin de notes"
try: format = format or "html"
etud = sco_etud.get_etud_info(filled=True)[0] etud: Identite = Identite.query.get_or_404(etudid)
etudid = etud["etudid"] formsemestre: FormSemestre = FormSemestre.query.get(formsemestre_id)
except: if not formsemestre:
sco_etud.log_unknown_etud() raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
raise ScoValueError("étudiant inconnu")
# API, donc erreurs admises en ScoValueError
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
bulletin = do_formsemestre_bulletinetud( bulletin = do_formsemestre_bulletinetud(
formsemestre_id, formsemestre,
etudid, etudid,
format=format, format=format,
version=version, version=version,
@ -836,52 +816,22 @@ def formsemestre_bulletinetud(
prefer_mail_perso=prefer_mail_perso, prefer_mail_perso=prefer_mail_perso,
)[0] )[0]
if format not in {"html", "pdfmail"}: if format not in {"html", "pdfmail"}:
filename = scu.bul_filename(sem, etud, format) filename = scu.bul_filename(formsemestre, etud, format)
return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0]) return scu.send_file(bulletin, filename, mime=scu.get_mime_suffix(format)[0])
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
H = [ H = [
_formsemestre_bulletinetud_header_html( _formsemestre_bulletinetud_header_html(etud, formsemestre, format, version),
etud, etudid, sem, formsemestre_id, format, version
),
bulletin, bulletin,
render_template(
"bul_foot.html",
etud=etud,
formsemestre=formsemestre,
inscription_courante=etud.inscription_courante(),
inscription_str=etud.inscription_descr()["inscription_str"],
),
html_sco_header.sco_footer(),
] ]
H.append("""<p>Situation actuelle: """)
if etud["inscription_formsemestre_id"]:
H.append(
f"""<a class="stdlink" href="{url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=etud["inscription_formsemestre_id"])
}">"""
)
H.append(etud["inscriptionstr"])
if etud["inscription_formsemestre_id"]:
H.append("""</a>""")
H.append("""</p>""")
if sem["modalite"] == "EXT":
H.append(
"""<p><a
href="formsemestre_ext_edit_ue_validations?formsemestre_id=%s&etudid=%s"
class="stdlink">
Editer les validations d'UE dans ce semestre extérieur
</a></p>"""
% (formsemestre_id, etudid)
)
# Place du diagramme radar
H.append(
"""<form id="params">
<input type="hidden" name="etudid" id="etudid" value="%s"/>
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="%s"/>
</form>"""
% (etudid, formsemestre_id)
)
H.append('<div id="radar_bulletin"></div>')
# --- Pied de page
H.append(html_sco_header.sco_footer())
return "".join(H) return "".join(H)
@ -896,23 +846,24 @@ def can_send_bulletin_by_mail(formsemestre_id):
def do_formsemestre_bulletinetud( def do_formsemestre_bulletinetud(
formsemestre_id, formsemestre: FormSemestre,
etudid, etudid: int,
version="long", # short, long, selectedevals version="long", # short, long, selectedevals
format="html", format=None,
nohtml=False, nohtml=False,
xml_with_decisions=False, # force decisions dans XML xml_with_decisions=False, # force décisions dans XML
force_publishing=False, # force publication meme si semestre non publie sur "portail" force_publishing=False, # force publication meme si semestre non publié sur "portail"
prefer_mail_perso=False, # mails envoyes sur adresse perso si non vide prefer_mail_perso=False, # mails envoyés sur adresse perso si non vide
): ):
"""Génère le bulletin au format demandé. """Génère le bulletin au format demandé.
Retourne: (bul, filigranne) Retourne: (bul, filigranne)
bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json) bul est str ou bytes au format demandé (html, pdf, pdfmail, pdfpart, xml, json)
et filigranne est un message à placer en "filigranne" (eg "Provisoire"). et filigranne est un message à placer en "filigranne" (eg "Provisoire").
""" """
format = format or "html"
if format == "xml": if format == "xml":
bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud( bul = sco_bulletins_xml.make_xml_formsemestre_bulletinetud(
formsemestre_id, formsemestre.id,
etudid, etudid,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
@ -923,7 +874,7 @@ def do_formsemestre_bulletinetud(
elif format == "json": elif format == "json":
bul = sco_bulletins_json.make_json_formsemestre_bulletinetud( bul = sco_bulletins_json.make_json_formsemestre_bulletinetud(
formsemestre_id, formsemestre.id,
etudid, etudid,
xml_with_decisions=xml_with_decisions, xml_with_decisions=xml_with_decisions,
force_publishing=force_publishing, force_publishing=force_publishing,
@ -931,7 +882,12 @@ def do_formsemestre_bulletinetud(
) )
return bul, "" return bul, ""
I = formsemestre_bulletinetud_dict(formsemestre_id, etudid) if formsemestre.formation.is_apc():
etud = Identite.query.get(etudid)
r = bulletin_but.BulletinBUT(formsemestre)
I = r.bulletin_etud_complet(etud)
else:
I = formsemestre_bulletinetud_dict(formsemestre.id, etudid)
etud = I["etud"] etud = I["etud"]
if format == "html": if format == "html":
@ -958,7 +914,7 @@ def do_formsemestre_bulletinetud(
elif format == "pdfmail": elif format == "pdfmail":
# format pdfmail: envoie le pdf par mail a l'etud, et affiche le html # format pdfmail: envoie le pdf par mail a l'etud, et affiche le html
# check permission # check permission
if not can_send_bulletin_by_mail(formsemestre_id): if not can_send_bulletin_by_mail(formsemestre.id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
if nohtml: if nohtml:
@ -987,7 +943,7 @@ def do_formsemestre_bulletinetud(
) + htm ) + htm
return h, I["filigranne"] return h, I["filigranne"]
# #
mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr) mail_bulletin(formsemestre.id, I, pdfdata, filename, recipient_addr)
emaillink = '<a class="stdlink" href="mailto:%s">%s</a>' % ( emaillink = '<a class="stdlink" href="mailto:%s">%s</a>' % (
recipient_addr, recipient_addr,
recipient_addr, recipient_addr,
@ -1055,17 +1011,15 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
) )
def _formsemestre_bulletinetud_header_html( def _formsemestre_bulletinetud_header_html_old_XXX(
etud, etud: Identite,
etudid, formsemestre: FormSemestre,
sem,
formsemestre_id=None,
format=None, format=None,
version=None, version=None,
): ):
H = [ H = [
html_sco_header.sco_header( html_sco_header.sco_header(
page_title="Bulletin de %(nomprenom)s" % etud, page_title=f"Bulletin de {etud.nomprenom}",
javascripts=[ javascripts=[
"js/bulletin.js", "js/bulletin.js",
"libjs/d3.v3.min.js", "libjs/d3.v3.min.js",
@ -1073,33 +1027,27 @@ def _formsemestre_bulletinetud_header_html(
], ],
cssstyles=["css/radar_bulletin.css"], cssstyles=["css/radar_bulletin.css"],
), ),
"""<table class="bull_head"><tr><td> f"""<table class="bull_head"><tr><td>
<h2><a class="discretelink" href="%s">%s</a></h2> <h2><a class="discretelink" href="{
"""
% (
url_for( url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud["etudid"] "scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id
), )}">{etud.nomprenom}</a></h2>
etud["nomprenom"],
), <form name="f" method="GET" action="{request.base_url}">
""" Bulletin <span class="bull_liensemestre"><a href="{
<form name="f" method="GET" action="%s">"""
% request.base_url,
f"""Bulletin <span class="bull_liensemestre"><a href="{
url_for("notes.formsemestre_status", url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=sem["formsemestre_id"])} formsemestre_id=formsemestre.id)}
">{sem["titremois"]}</a></span> ">{formsemestre.titre_mois()}</a></span>
<br/>""" <br/>
% sem, <table><tr>
"""<table><tr>""", <td>établi le {time.strftime("%d/%m/%Y à %Hh%M")} (notes sur 20)</td>
"""<td>établi le %s (notes sur 20)</td>""" % time.strftime("%d/%m/%Y à %Hh%M"), <td><span class="rightjust">
"""<td><span class="rightjust"> <input type="hidden" name="formsemestre_id" value="{formsemestre.id}"></input>
<input type="hidden" name="formsemestre_id" value="%s"></input>""" <input type="hidden" name="etudid" value="{etud.id}"></input>
% formsemestre_id, <input type="hidden" name="format" value="{format}"></input>
"""<input type="hidden" name="etudid" value="%s"></input>""" % etudid, <select name="version" onchange="document.f.submit()" class="noprint">
"""<input type="hidden" name="format" value="%s"></input>""" % format, """,
"""<select name="version" onchange="document.f.submit()" class="noprint">""",
] ]
for (v, e) in ( for (v, e) in (
("short", "Version courte"), ("short", "Version courte"),
@ -1114,143 +1062,12 @@ def _formsemestre_bulletinetud_header_html(
H.append("""</select></td>""") H.append("""</select></td>""")
# Menu # Menu
endpoint = "notes.formsemestre_bulletinetud" endpoint = "notes.formsemestre_bulletinetud"
menu_autres_operations = make_menu_autres_operations(
menuBul = [ formsemestre, etud, endpoint, version
{ )
"title": "Réglages bulletins",
"endpoint": "notes.formsemestre_edit_options",
"args": {
"formsemestre_id": formsemestre_id,
# "target_url": url_for(
# "notes.formsemestre_bulletinetud",
# scodoc_dept=g.scodoc_dept,
# formsemestre_id=formsemestre_id,
# etudid=etudid,
# ),
},
"enabled": (current_user.id in sem["responsables"])
or current_user.has_permission(Permission.ScoImplement),
},
{
"title": 'Version papier (pdf, format "%s")'
% sco_bulletins_generator.bulletin_get_class_name_displayed(
formsemestre_id
),
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"version": version,
"format": "pdf",
},
},
{
"title": "Envoi par mail à %s" % etud["email"],
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"version": version,
"format": "pdfmail",
},
# possible slt si on a un mail...
"enabled": etud["email"] and can_send_bulletin_by_mail(formsemestre_id),
},
{
"title": "Envoi par mail à %s (adr. personnelle)" % etud["emailperso"],
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"version": version,
"format": "pdfmail",
"prefer_mail_perso": 1,
},
# possible slt si on a un mail...
"enabled": etud["emailperso"]
and can_send_bulletin_by_mail(formsemestre_id),
},
{
"title": "Version json",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"version": version,
"format": "json",
},
},
{
"title": "Version XML",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
"version": version,
"format": "xml",
},
},
{
"title": "Ajouter une appréciation",
"endpoint": "notes.appreciation_add_form",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
},
"enabled": (
(current_user.id in sem["responsables"])
or (current_user.has_permission(Permission.ScoEtudInscrit))
),
},
{
"title": "Enregistrer un semestre effectué ailleurs",
"endpoint": "notes.formsemestre_ext_create_form",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
},
"enabled": current_user.has_permission(Permission.ScoImplement),
},
{
"title": "Enregistrer une validation d'UE antérieure",
"endpoint": "notes.formsemestre_validate_previous_ue",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
},
{
"title": "Enregistrer note d'une UE externe",
"endpoint": "notes.external_ue_create_form",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
},
{
"title": "Entrer décisions jury",
"endpoint": "notes.formsemestre_validation_etud_form",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
},
{
"title": "Editer PV jury",
"endpoint": "notes.formsemestre_pvjury_pdf",
"args": {
"formsemestre_id": formsemestre_id,
"etudid": etudid,
},
"enabled": True,
},
]
H.append("""<td class="bulletin_menubar"><div class="bulletin_menubar">""") H.append("""<td class="bulletin_menubar"><div class="bulletin_menubar">""")
H.append(htmlutils.make_menu("Autres opérations", menuBul, alone=True)) H.append(menu_autres_operations)
H.append("""</div></td>""") H.append("""</div></td>""")
H.append( H.append(
'<td> <a href="%s">%s</a></td>' '<td> <a href="%s">%s</a></td>'
@ -1258,8 +1075,8 @@ def _formsemestre_bulletinetud_header_html(
url_for( url_for(
"notes.formsemestre_bulletinetud", "notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre.id,
etudid=etudid, etudid=etud.id,
format="pdf", format="pdf",
version=version, version=version,
), ),
@ -1272,8 +1089,8 @@ def _formsemestre_bulletinetud_header_html(
"""</form></span></td><td class="bull_photo"><a href="%s">%s</a> """</form></span></td><td class="bull_photo"><a href="%s">%s</a>
""" """
% ( % (
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid), url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id),
sco_photos.etud_photo_html(etud, title="fiche de " + etud["nom"]), sco_photos.etud_photo_html(etud, title="fiche de " + etud.nomprenom),
) )
) )
H.append( H.append(
@ -1283,3 +1100,177 @@ def _formsemestre_bulletinetud_header_html(
) )
return "".join(H) return "".join(H)
def make_menu_autres_operations(
formsemestre: FormSemestre, etud: Identite, endpoint: str, version: str
) -> str:
etud_email = etud.get_first_email() or ""
etud_perso = etud.get_first_email("emailperso") or ""
menu_items = [
{
"title": "Réglages bulletins",
"endpoint": "notes.formsemestre_edit_options",
"args": {
"formsemestre_id": formsemestre.id,
# "target_url": url_for(
# "notes.formsemestre_bulletinetud",
# scodoc_dept=g.scodoc_dept,
# formsemestre_id=formsemestre_id,
# etudid=etudid,
# ),
},
"enabled": formsemestre.can_be_edited_by(current_user),
},
{
"title": 'Version papier (pdf, format "%s")'
% sco_bulletins_generator.bulletin_get_class_name_displayed(
formsemestre.id
),
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "pdf",
},
},
{
"title": f"Envoi par mail à {etud_email}",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "pdfmail",
},
# possible slt si on a un mail...
"enabled": etud_email and can_send_bulletin_by_mail(formsemestre.id),
},
{
"title": f"Envoi par mail à {etud_perso} (adr. personnelle)",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "pdfmail",
"prefer_mail_perso": 1,
},
# possible slt si on a un mail...
"enabled": etud_perso and can_send_bulletin_by_mail(formsemestre.id),
},
{
"title": "Version json",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "json",
},
},
{
"title": "Version XML",
"endpoint": endpoint,
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
"version": version,
"format": "xml",
},
},
{
"title": "Ajouter une appréciation",
"endpoint": "notes.appreciation_add_form",
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": (
formsemestre.can_be_edited_by(current_user)
or current_user.has_permission(Permission.ScoEtudInscrit)
),
},
{
"title": "Enregistrer un semestre effectué ailleurs",
"endpoint": "notes.formsemestre_ext_create_form",
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": current_user.has_permission(Permission.ScoImplement),
},
{
"title": "Enregistrer une validation d'UE antérieure",
"endpoint": "notes.formsemestre_validate_previous_ue",
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
},
{
"title": "Enregistrer note d'une UE externe",
"endpoint": "notes.external_ue_create_form",
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
},
{
"title": "Entrer décisions jury",
"endpoint": "notes.formsemestre_validation_etud_form",
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": sco_permissions_check.can_validate_sem(formsemestre.id),
},
{
"title": "Éditer PV jury",
"endpoint": "notes.formsemestre_pvjury_pdf",
"args": {
"formsemestre_id": formsemestre.id,
"etudid": etud.id,
},
"enabled": True,
},
]
return htmlutils.make_menu("Autres opérations", menu_items, alone=True)
def _formsemestre_bulletinetud_header_html(
etud,
formsemestre: FormSemestre,
format=None,
version=None,
):
H = [
html_sco_header.sco_header(
page_title=f"Bulletin de {etud.nomprenom}",
javascripts=[
"js/bulletin.js",
"libjs/d3.v3.min.js",
"js/radar_bulletin.js",
],
cssstyles=["css/radar_bulletin.css"],
),
render_template(
"bul_head.html",
etud=etud,
format=format,
formsemestre=formsemestre,
menu_autres_operations=make_menu_autres_operations(
etud=etud,
formsemestre=formsemestre,
endpoint="notes.formsemestre_bulletinetud",
version=version,
),
scu=scu,
time=time,
version=version,
),
]
return "\n".join(H)

View File

@ -63,48 +63,14 @@ from app.scodoc import sco_pdf
from app.scodoc.sco_pdf import PDFLOCK from app.scodoc.sco_pdf import PDFLOCK
import sco_version import sco_version
# Liste des types des classes de générateurs de bulletins PDF:
BULLETIN_CLASSES = collections.OrderedDict()
class BulletinGenerator:
def register_bulletin_class(klass):
BULLETIN_CLASSES[klass.__name__] = klass
def bulletin_class_descriptions():
return [x.description for x in BULLETIN_CLASSES.values()]
def bulletin_class_names():
return list(BULLETIN_CLASSES.keys())
def bulletin_default_class_name():
return bulletin_class_names()[0]
def bulletin_get_class(class_name):
return BULLETIN_CLASSES[class_name]
def bulletin_get_class_name_displayed(formsemestre_id):
"""Le nom du générateur utilisé, en clair"""
from app.scodoc import sco_preferences
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
try:
gen_class = bulletin_get_class(bul_class_name)
return gen_class.description
except:
return "invalide ! (voir paramètres)"
class BulletinGenerator(object):
"Virtual superclass for PDF bulletin generators" "" "Virtual superclass for PDF bulletin generators" ""
# Here some helper methods # Here some helper methods
# see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods # see sco_bulletins_standard.BulletinGeneratorStandard subclass for real methods
supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ] supported_formats = [] # should list supported formats, eg [ 'html', 'pdf' ]
description = "superclass for bulletins" # description for user interface description = "superclass for bulletins" # description for user interface
list_in_menu = True # la classe doit-elle est montrée dans le menu de config ?
def __init__( def __init__(
self, self,
@ -151,7 +117,7 @@ class BulletinGenerator(object):
def get_filename(self): def get_filename(self):
"""Build a filename to be proposed to the web client""" """Build a filename to be proposed to the web client"""
sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"]) sem = sco_formsemestre.get_formsemestre(self.infos["formsemestre_id"])
return scu.bul_filename(sem, self.infos["etud"], "pdf") return scu.bul_filename_old(sem, self.infos["etud"], "pdf")
def generate(self, format="", stand_alone=True): def generate(self, format="", stand_alone=True):
"""Return bulletin in specified format""" """Return bulletin in specified format"""
@ -270,9 +236,14 @@ def make_formsemestre_bulletinetud(
formsemestre_id = infos["formsemestre_id"] formsemestre_id = infos["formsemestre_id"]
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id) bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
try:
gen_class = None
if infos.get("type") == "BUT" and format.startswith("pdf"):
gen_class = bulletin_get_class(bul_class_name + "BUT")
if gen_class is None:
gen_class = bulletin_get_class(bul_class_name) gen_class = bulletin_get_class(bul_class_name)
except:
if gen_class is None:
raise ValueError( raise ValueError(
"Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name "Type de bulletin PDF invalide (paramètre: %s)" % bul_class_name
) )
@ -313,3 +284,52 @@ def make_formsemestre_bulletinetud(
filename = bul_generator.get_filename() filename = bul_generator.get_filename()
return data, filename return data, filename
####
# Liste des types des classes de générateurs de bulletins PDF:
BULLETIN_CLASSES = collections.OrderedDict()
def register_bulletin_class(klass):
BULLETIN_CLASSES[klass.__name__] = klass
def bulletin_class_descriptions():
return [
BULLETIN_CLASSES[class_name].description
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
def bulletin_class_names() -> list[str]:
"Liste les noms des classes de bulletins à présenter à l'utilisateur"
return [
class_name
for class_name in BULLETIN_CLASSES
if BULLETIN_CLASSES[class_name].list_in_menu
]
def bulletin_default_class_name():
return bulletin_class_names()[0]
def bulletin_get_class(class_name: str) -> BulletinGenerator:
"""La class de génération de bulletin de ce nom,
ou None si pas trouvée
"""
return BULLETIN_CLASSES.get(class_name)
def bulletin_get_class_name_displayed(formsemestre_id):
"""Le nom du générateur utilisé, en clair"""
from app.scodoc import sco_preferences
bul_class_name = sco_preferences.get_preference("bul_class_name", formsemestre_id)
gen_class = bulletin_get_class(bul_class_name)
if gen_class is None:
return "invalide ! (voir paramètres)"
return gen_class.description

View File

@ -138,7 +138,7 @@ def formsemestre_bulletinetud_published_dict(
if not published: if not published:
return d # stop ! return d # stop !
etat_inscription = etud.etat_inscription(formsemestre.id) etat_inscription = etud.inscription_etat(formsemestre.id)
if etat_inscription != scu.INSCRIT: if etat_inscription != scu.INSCRIT:
d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True)) d.update(dict_decision_jury(etudid, formsemestre_id, with_decisions=True))
return d return d

View File

@ -61,12 +61,10 @@ from reportlab.platypus.doctemplate import BaseDocTemplate
from flask import g, request from flask import g, request
from app import log, ScoValueError from app import log, ScoValueError
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre
from app.scodoc import sco_cache from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre from app.scodoc import sco_codes_parcours
from app.scodoc import sco_pdf from app.scodoc import sco_pdf
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc import sco_etud from app.scodoc import sco_etud
@ -190,7 +188,7 @@ def get_formsemestre_bulletins_pdf(formsemestre_id, version="selectedevals"):
i = 1 i = 1
for etud in formsemestre.get_inscrits(include_demdef=True, order=True): for etud in formsemestre.get_inscrits(include_demdef=True, order=True):
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud( frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre_id, formsemestre,
etud.id, etud.id,
format="pdfpart", format="pdfpart",
version=version, version=version,
@ -239,8 +237,9 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
filigrannes = {} filigrannes = {}
i = 1 i = 1
for sem in etud["sems"]: for sem in etud["sems"]:
formsemestre = FormSemestre.query.get(sem["formsemestre_id"])
frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud( frag, filigranne = sco_bulletins.do_formsemestre_bulletinetud(
sem["formsemestre_id"], formsemestre,
etudid, etudid,
format="pdfpart", format="pdfpart",
version=version, version=version,
@ -275,3 +274,16 @@ def get_etud_bulletins_pdf(etudid, version="selectedevals"):
) )
return pdfdoc, filename return pdfdoc, filename
def get_filigranne(etud_etat: str, prefs, decision_sem=None) -> str:
"""Texte à placer en "filigranne" sur le bulletin pdf"""
if etud_etat == scu.DEMISSION:
return "Démission"
elif etud_etat == sco_codes_parcours.DEF:
return "Défaillant"
elif (prefs["bul_show_temporary"] and not decision_sem) or prefs[
"bul_show_temporary_forced"
]:
return prefs["bul_temporary_txt"]
return ""

View File

@ -66,7 +66,8 @@ from app.scodoc import sco_groups
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import gen_tables from app.scodoc import gen_tables
# Important: Le nom de la classe ne doit pas changer (bien le choisir), car il sera stocké en base de données (dans les préférences) # Important: Le nom de la classe ne doit pas changer (bien le choisir),
# car il sera stocké en base de données (dans les préférences)
class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator): class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
description = "standard ScoDoc (version 2011)" # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc description = "standard ScoDoc (version 2011)" # la description doit être courte: elle apparait dans le menu de paramètrage ScoDoc
supported_formats = ["html", "pdf"] supported_formats = ["html", "pdf"]
@ -264,11 +265,11 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
def build_bulletin_table(self): def build_bulletin_table(self):
"""Génère la table centrale du bulletin de notes """Génère la table centrale du bulletin de notes
Renvoie: colkeys, P, pdf_style, colWidths Renvoie: col_keys, P, pdf_style, col_widths
- colkeys: nom des colonnes de la table (clés) - col_keys: nom des colonnes de la table (clés)
- table (liste de dicts de chaines de caracteres) - table: liste de dicts de chaines de caractères
- style (commandes table Platypus) - pdf_style: commandes table Platypus
- largeurs de colonnes pour PDF - col_widths: largeurs de colonnes pour PDF
""" """
I = self.infos I = self.infos
P = [] # elems pour générer table avec gen_table (liste de dicts) P = [] # elems pour générer table avec gen_table (liste de dicts)
@ -287,25 +288,25 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
with_col_coef = prefs["bul_show_coef"] or prefs["bul_show_ue_coef"] with_col_coef = prefs["bul_show_coef"] or prefs["bul_show_ue_coef"]
with_col_ects = prefs["bul_show_ects"] with_col_ects = prefs["bul_show_ects"]
colkeys = ["titre", "module"] # noms des colonnes à afficher col_keys = ["titre", "module"] # noms des colonnes à afficher
if with_col_rang: if with_col_rang:
colkeys += ["rang"] col_keys += ["rang"]
if with_col_minmax: if with_col_minmax:
colkeys += ["min"] col_keys += ["min"]
if with_col_moypromo: if with_col_moypromo:
colkeys += ["moy"] col_keys += ["moy"]
if with_col_minmax: if with_col_minmax:
colkeys += ["max"] col_keys += ["max"]
colkeys += ["note"] col_keys += ["note"]
if with_col_coef: if with_col_coef:
colkeys += ["coef"] col_keys += ["coef"]
if with_col_ects: if with_col_ects:
colkeys += ["ects"] col_keys += ["ects"]
if with_col_abs: if with_col_abs:
colkeys += ["abs"] col_keys += ["abs"]
colidx = {} # { nom_colonne : indice à partir de 0 } (pour styles platypus) colidx = {} # { nom_colonne : indice à partir de 0 } (pour styles platypus)
i = 0 i = 0
for k in colkeys: for k in col_keys:
colidx[k] = i colidx[k] = i
i += 1 i += 1
@ -313,7 +314,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
bul_pdf_mod_colwidth = float(prefs["bul_pdf_mod_colwidth"]) * cm bul_pdf_mod_colwidth = float(prefs["bul_pdf_mod_colwidth"]) * cm
else: else:
bul_pdf_mod_colwidth = None bul_pdf_mod_colwidth = None
colWidths = { col_widths = {
"titre": None, "titre": None,
"module": bul_pdf_mod_colwidth, "module": bul_pdf_mod_colwidth,
"min": 1.5 * cm, "min": 1.5 * cm,
@ -541,7 +542,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu: ("BOX", (0, 0), (-1, -1), 0.4, blue), # ajoute cadre extérieur bleu:
] ]
# #
return colkeys, P, pdf_style, colWidths return col_keys, P, pdf_style, col_widths
def _list_modules( def _list_modules(
self, self,

View File

@ -512,8 +512,8 @@ def module_edit(module_id=None):
] ]
else: else:
mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres] mat_names = ["%s / %s" % (mat.ue.acronyme, mat.titre or "") for mat in matieres]
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
ue_mat_ids = ["%s!%s" % (mat.ue.id, mat.id) for mat in matieres]
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"]) module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
semestres_indices = list(range(1, parcours.NB_SEM + 1)) semestres_indices = list(range(1, parcours.NB_SEM + 1))
@ -748,8 +748,11 @@ def module_edit(module_id=None):
else: else:
# l'UE de rattachement peut changer # l'UE de rattachement peut changer
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
x, y = tf[2]["ue_matiere_id"].split("!")
tf[2]["ue_id"] = int(x)
tf[2]["matiere_id"] = int(y)
old_ue_id = a_module.ue.id old_ue_id = a_module.ue.id
new_ue_id = int(tf[2]["ue_id"]) new_ue_id = tf[2]["ue_id"]
if (old_ue_id != new_ue_id) and in_use: if (old_ue_id != new_ue_id) and in_use:
new_ue = UniteEns.query.get_or_404(new_ue_id) new_ue = UniteEns.query.get_or_404(new_ue_id)
if new_ue.semestre_idx != a_module.ue.semestre_idx: if new_ue.semestre_idx != a_module.ue.semestre_idx:

View File

@ -954,13 +954,13 @@ def _ue_table_ues(
if cur_ue_semestre_id != ue["semestre_id"]: if cur_ue_semestre_id != ue["semestre_id"]:
cur_ue_semestre_id = ue["semestre_id"] cur_ue_semestre_id = ue["semestre_id"]
# if iue > 0:
# H.append("</ul>")
if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT: if ue["semestre_id"] == sco_codes_parcours.UE_SEM_DEFAULT:
lab = "Pas d'indication de semestre:" lab = "Pas d'indication de semestre:"
else: else:
lab = "Semestre %s:" % ue["semestre_id"] lab = "Semestre %s:" % ue["semestre_id"]
H.append('<div class="ue_list_tit_sem">%s</div>' % lab) H.append(
'<div class="ue_list_div"><div class="ue_list_tit_sem">%s</div>' % lab
)
H.append('<ul class="notes_ue_list">') H.append('<ul class="notes_ue_list">')
H.append('<li class="notes_ue_list">') H.append('<li class="notes_ue_list">')
if iue != 0 and editable: if iue != 0 and editable:
@ -1028,7 +1028,9 @@ def _ue_table_ues(
H.append( H.append(
f"""</ul><ul><li><a href="{url_for('notes.ue_create', scodoc_dept=g.scodoc_dept, f"""</ul><ul><li><a href="{url_for('notes.ue_create', scodoc_dept=g.scodoc_dept,
formation_id=ue['formation_id'], semestre_idx=ue['semestre_id']) formation_id=ue['formation_id'], semestre_idx=ue['semestre_id'])
}">Ajouter une UE dans le semestre {ue['semestre_id'] or ''}</a></li></ul>""" }">Ajouter une UE dans le semestre {ue['semestre_id'] or ''}</a></li></ul>
</div>
"""
) )
iue += 1 iue += 1

View File

@ -33,8 +33,7 @@ import os
import time import time
from operator import itemgetter from operator import itemgetter
from flask import url_for, g, request from flask import url_for, g
from flask_mail import Message
from app import email from app import email
from app import log from app import log
@ -46,7 +45,6 @@ from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
from app.scodoc import safehtml from app.scodoc import safehtml
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
from app.scodoc.scolog import logdb from app.scodoc.scolog import logdb
from app.scodoc.TrivialFormulator import TrivialFormulator
def format_etud_ident(etud): def format_etud_ident(etud):
@ -860,7 +858,7 @@ def list_scolog(etudid):
return cursor.dictfetchall() return cursor.dictfetchall()
def fill_etuds_info(etuds, add_admission=True): def fill_etuds_info(etuds: list[dict], add_admission=True):
"""etuds est une liste d'etudiants (mappings) """etuds est une liste d'etudiants (mappings)
Pour chaque etudiant, ajoute ou formatte les champs Pour chaque etudiant, ajoute ou formatte les champs
-> informations pour fiche etudiant ou listes diverses -> informations pour fiche etudiant ou listes diverses
@ -977,7 +975,10 @@ def etud_inscriptions_infos(etudid: int, ne="") -> dict:
def descr_situation_etud(etudid: int, ne="") -> str: def descr_situation_etud(etudid: int, ne="") -> str:
"""chaîne décrivant la situation actuelle de l'étudiant""" """Chaîne décrivant la situation actuelle de l'étudiant
XXX Obsolete, utiliser Identite.descr_situation_etud() dans
les nouveaux codes
"""
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()

View File

@ -180,7 +180,9 @@ def search_etud_in_dept(expnom=""):
e["_nomprenom_target"] = target e["_nomprenom_target"] = target
e["inscription_target"] = target e["inscription_target"] = target
e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"]) e["_nomprenom_td_attrs"] = 'id="%s" class="etudinfo"' % (e["etudid"])
sco_groups.etud_add_group_infos(e, e["cursem"]) sco_groups.etud_add_group_infos(
e, e["cursem"]["formsemestre_id"] if e["cursem"] else None
)
tab = GenTable( tab = GenTable(
columns_ids=("nomprenom", "code_nip", "inscription", "groupes"), columns_ids=("nomprenom", "code_nip", "inscription", "groupes"),

View File

@ -31,7 +31,7 @@
from flask import current_app from flask import current_app
from flask import g from flask import g
from flask import request from flask import request
from flask import url_for from flask import render_template, url_for
from flask_login import current_user from flask_login import current_user
from app import log from app import log
@ -411,7 +411,7 @@ def formsemestre_status_menubar(sem):
"enabled": sco_permissions_check.can_validate_sem(formsemestre_id), "enabled": sco_permissions_check.can_validate_sem(formsemestre_id),
}, },
{ {
"title": "Editer les PV et archiver les résultats", "title": "Éditer les PV et archiver les résultats",
"endpoint": "notes.formsemestre_archive", "endpoint": "notes.formsemestre_archive",
"args": {"formsemestre_id": formsemestre_id}, "args": {"formsemestre_id": formsemestre_id},
"enabled": sco_permissions_check.can_edit_pv(formsemestre_id), "enabled": sco_permissions_check.can_edit_pv(formsemestre_id),
@ -445,6 +445,7 @@ def retreive_formsemestre_from_request() -> int:
"""Cherche si on a de quoi déduire le semestre affiché à partir des """Cherche si on a de quoi déduire le semestre affiché à partir des
arguments de la requête: arguments de la requête:
formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id formsemestre_id ou moduleimpl ou evaluation ou group_id ou partition_id
Returns None si pas défini.
""" """
if request.method == "GET": if request.method == "GET":
args = request.args args = request.args
@ -505,34 +506,17 @@ def formsemestre_page_title():
return "" return ""
try: try:
formsemestre_id = int(formsemestre_id) formsemestre_id = int(formsemestre_id)
sem = sco_formsemestre.get_formsemestre(formsemestre_id).copy() formsemestre = FormSemestre.query.get(formsemestre_id)
except: except:
log("can't find formsemestre_id %s" % formsemestre_id) log("can't find formsemestre_id %s" % formsemestre_id)
return "" return ""
fill_formsemestre(sem) h = render_template(
"formsemestre_page_title.html",
h = f"""<div class="formsemestre_page_title"> formsemestre=formsemestre,
<div class="infos"> scu=scu,
<span class="semtitle"><a class="stdlink" title="{sem['session_id']}" sem_menu_bar=formsemestre_status_menubar(formsemestre.to_dict()),
href="{url_for('notes.formsemestre_status', )
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
>{sem['titre']}</a><a
title="{sem['etape_apo_str']}">{sem['num_sem']}</a>{sem['modalitestr']}</span><span
class="dates"><a
title="du {sem['date_debut']} au {sem['date_fin']} "
>{sem['mois_debut']} - {sem['mois_fin']}</a></span><span
class="resp"><a title="{sem['nomcomplet']}">{sem['resp']}</a></span><span
class="nbinscrits"><a class="discretelink"
href="{url_for("scolar.groups_view",
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
>{sem['nbinscrits']} inscrits</a></span><span
class="lock">{sem['locklink']}</span><span
class="eye">{sem['eyelink']}</span>
</div>
{formsemestre_status_menubar(sem)}
</div>
"""
return h return h

View File

@ -321,7 +321,7 @@ def get_group_infos(group_id, etat=None): # was _getlisteetud
t["etath"] = t["etat"] t["etath"] = t["etat"]
# Add membership for all partitions, 'partition_id' : group # Add membership for all partitions, 'partition_id' : group
for etud in members: # long: comment eviter ces boucles ? for etud in members: # long: comment eviter ces boucles ?
etud_add_group_infos(etud, sem) etud_add_group_infos(etud, sem["formsemestre_id"])
if group["group_name"] != None: if group["group_name"] != None:
group_tit = "%s %s" % (group["partition_name"], group["group_name"]) group_tit = "%s %s" % (group["partition_name"], group["group_name"])
@ -413,12 +413,12 @@ def formsemestre_get_etud_groupnames(formsemestre_id, attr="group_name"):
return R return R
def etud_add_group_infos(etud, sem, sep=" "): def etud_add_group_infos(etud, formsemestre_id, sep=" "):
"""Add informations on partitions and group memberships to etud (a dict with an etudid)""" """Add informations on partitions and group memberships to etud (a dict with an etudid)"""
etud[ etud[
"partitions" "partitions"
] = collections.OrderedDict() # partition_id : group + partition_name ] = collections.OrderedDict() # partition_id : group + partition_name
if not sem: if not formsemestre_id:
etud["groupes"] = "" etud["groupes"] = ""
return etud return etud
@ -430,7 +430,7 @@ def etud_add_group_infos(etud, sem, sep=" "):
and p.formsemestre_id = %(formsemestre_id)s and p.formsemestre_id = %(formsemestre_id)s
ORDER BY p.numero ORDER BY p.numero
""", """,
{"etudid": etud["etudid"], "formsemestre_id": sem["formsemestre_id"]}, {"etudid": etud["etudid"], "formsemestre_id": formsemestre_id},
) )
for info in infos: for info in infos:
@ -439,13 +439,13 @@ def etud_add_group_infos(etud, sem, sep=" "):
# resume textuel des groupes: # resume textuel des groupes:
etud["groupes"] = sep.join( etud["groupes"] = sep.join(
[g["group_name"] for g in infos if g["group_name"] != None] [gr["group_name"] for gr in infos if gr["group_name"] is not None]
) )
etud["partitionsgroupes"] = sep.join( etud["partitionsgroupes"] = sep.join(
[ [
g["partition_name"] + ":" + g["group_name"] gr["partition_name"] + ":" + gr["group_name"]
for g in infos for gr in infos
if g["group_name"] != None if gr["group_name"] is not None
] ]
) )

View File

@ -203,7 +203,7 @@ def sco_import_generate_excel_sample(
for field in titles: for field in titles:
if field == "groupes": if field == "groupes":
sco_groups.etud_add_group_infos( sco_groups.etud_add_group_infos(
etud, groups_infos.formsemestre, sep=";" etud, groups_infos.formsemestre_id, sep=";"
) )
l.append(etud["partitionsgroupes"]) l.append(etud["partitionsgroupes"])
else: else:

View File

@ -196,7 +196,10 @@ def do_inscrit(sem, etudids, inscrit_groupes=False):
if len(etud["sems"]) < 2: if len(etud["sems"]) < 2:
continue continue
prev_formsemestre = etud["sems"][1] prev_formsemestre = etud["sems"][1]
sco_groups.etud_add_group_infos(etud, prev_formsemestre) sco_groups.etud_add_group_infos(
etud,
prev_formsemestre["formsemestre_id"] if prev_formsemestre else None,
)
cursem_groups_by_name = dict( cursem_groups_by_name = dict(
[ [

View File

@ -215,7 +215,9 @@ def ficheEtud(etudid=None):
info["modifadresse"] = "" info["modifadresse"] = ""
# Groupes: # Groupes:
sco_groups.etud_add_group_infos(info, info["cursem"]) sco_groups.etud_add_group_infos(
info, info["cursem"]["formsemestre_id"] if info["cursem"] else None
)
# Parcours de l'étudiant # Parcours de l'étudiant
if info["sems"]: if info["sems"]:

View File

@ -175,7 +175,7 @@ def etud_photo_is_local(etud: dict, size="small"):
return photo_pathname(etud["photo_filename"], size=size) return photo_pathname(etud["photo_filename"], size=size)
def etud_photo_html(etud=None, etudid=None, title=None, size="small"): def etud_photo_html(etud: dict = None, etudid=None, title=None, size="small"):
"""HTML img tag for the photo, either in small size (h90) """HTML img tag for the photo, either in small size (h90)
or original size (size=="orig") or original size (size=="orig")
""" """
@ -351,6 +351,7 @@ def copy_portal_photo_to_fs(etud):
"""Copy the photo from portal (distant website) to local fs. """Copy the photo from portal (distant website) to local fs.
Returns rel. path or None if copy failed, with a diagnostic message Returns rel. path or None if copy failed, with a diagnostic message
""" """
if "nomprenom" not in etud:
sco_etud.format_etud_ident(etud) sco_etud.format_etud_ident(etud)
url = photo_portal_url(etud) url = photo_portal_url(etud)
if not url: if not url:

View File

@ -2138,7 +2138,7 @@ class BasePreferences(object):
return form return form
class SemPreferences(object): class SemPreferences:
"""Preferences for a formsemestre""" """Preferences for a formsemestre"""
def __init__(self, formsemestre_id=None): def __init__(self, formsemestre_id=None):
@ -2294,9 +2294,8 @@ def doc_preferences():
return "\n".join([" | ".join(x) for x in L]) return "\n".join([" | ".join(x) for x in L])
def bulletin_option_affichage(formsemestre_id: int) -> dict: def bulletin_option_affichage(formsemestre_id: int, prefs: SemPreferences) -> dict:
"dict avec les options d'affichages (préférences) pour ce semestre" "dict avec les options d'affichages (préférences) pour ce semestre"
prefs = SemPreferences(formsemestre_id)
fields = ( fields = (
"bul_show_abs", "bul_show_abs",
"bul_show_abs_modules", "bul_show_abs_modules",

View File

@ -608,7 +608,7 @@ def is_valid_filename(filename):
return VALID_EXP.match(filename) return VALID_EXP.match(filename)
def bul_filename(sem, etud, format): def bul_filename_old(sem: dict, etud: dict, format):
"""Build a filename for this bulletin""" """Build a filename for this bulletin"""
dt = time.strftime("%Y-%m-%d") dt = time.strftime("%Y-%m-%d")
filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{format}" filename = f"bul-{sem['titre_num']}-{dt}-{etud['nom']}.{format}"
@ -616,6 +616,14 @@ def bul_filename(sem, etud, format):
return filename return filename
def bul_filename(formsemestre, etud, format):
"""Build a filename for this bulletin"""
dt = time.strftime("%Y-%m-%d")
filename = f"bul-{formsemestre.titre_num()}-{dt}-{etud.nom}.{format}"
filename = make_filename(filename)
return filename
def flash_errors(form): def flash_errors(form):
"""Flashes form errors (version sommaire)""" """Flashes form errors (version sommaire)"""
for field, errors in form.errors.items(): for field, errors in form.errors.items():

View File

@ -14,16 +14,25 @@
} }
main{ main{
--couleurPrincipale: rgb(240,250,255); --couleurPrincipale: rgb(240,250,255);
--couleurFondTitresUE: rgb(206,255,235); --couleurFondTitresUE: #b6ebff;
--couleurFondTitresRes: rgb(125, 170, 255); --couleurFondTitresRes: #f8c844;
--couleurFondTitresSAE: rgb(211, 255, 255); --couleurFondTitresSAE: #c6ffab;
--couleurSecondaire: #fec; --couleurSecondaire: #fec;
--couleurIntense: #c09; --couleurIntense: rgb(4, 16, 159);;
--couleurSurlignage: rgba(232, 255, 132, 0.47); --couleurSurlignage: rgba(255, 253, 110, 0.49);
max-width: 1000px; max-width: 1000px;
margin: auto; margin: auto;
display: none; display: none;
} }
.releve a, .releve a:visited {
color: navy;
text-decoration: none;
}
.releve a:hover {
color: red;
text-decoration: underline;
}
.ready .wait{display: none;} .ready .wait{display: none;}
.ready main{display: block;} .ready main{display: block;}
h2{ h2{
@ -97,7 +106,8 @@ section>div:nth-child(1){
.hide_coef .synthese em, .hide_coef .synthese em,
.hide_coef .eval>em, .hide_coef .eval>em,
.hide_date_inscr .dateInscription, .hide_date_inscr .dateInscription,
.hide_ects .ects{ .hide_ects .ects,
.hide_rangs .rang{
display: none; display: none;
} }
@ -151,14 +161,19 @@ section>div:nth-child(1){
column-gap: 4px; column-gap: 4px;
flex: none; flex: none;
} }
.infoSemestre>div:nth-child(1){
margin-right: auto;
}
.infoSemestre>div>div:nth-child(even){ .infoSemestre>div>div:nth-child(even){
text-align: right; text-align: right;
} }
.photo {
border: none;
margin-left: auto;
}
.rang{ .rang{
text-decoration: underline var(--couleurIntense); font-weight: bold;
}
.ue .rang{
font-weight: 400;
} }
.decision{ .decision{
margin: 5px 0; margin: 5px 0;
@ -186,6 +201,9 @@ section>div:nth-child(1){
.synthese h3{ .synthese h3{
background: var(--couleurFondTitresUE); background: var(--couleurFondTitresUE);
} }
.synthese .ue>div{
text-align: right;
}
.synthese em, .synthese em,
.eval em{ .eval em{
opacity: 0.6; opacity: 0.6;
@ -206,7 +224,6 @@ section>div:nth-child(1){
scroll-margin-top: 60px; scroll-margin-top: 60px;
} }
.module, .ue { .module, .ue {
background: var(--couleurSecondaire);
color: #000; color: #000;
padding: 4px 32px; padding: 4px 32px;
border-radius: 4px; border-radius: 4px;
@ -218,6 +235,15 @@ section>div:nth-child(1){
cursor: pointer; cursor: pointer;
position: relative; position: relative;
} }
.ue {
background: var(--couleurFondTitresRes);
}
.module {
background: var(--couleurFondTitresRes);
}
.module h3 {
background: var(--couleurFondTitresRes);
}
.module::before, .ue::before { .module::before, .ue::before {
content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='white'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>"); content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='white'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>");
width: 26px; width: 26px;
@ -308,6 +334,14 @@ h3{
margin-bottom: 8px; margin-bottom: 8px;
} }
@media screen and (max-width: 700px) {
section{
padding: 16px;
}
.syntheseModule, .eval {
margin: 0;
}
}
/*.absences{ /*.absences{
display: grid; display: grid;
grid-template-columns: auto auto; grid-template-columns: auto auto;

View File

@ -1702,7 +1702,7 @@ ul.notes_ue_list {
margin-top: 4px; margin-top: 4px;
margin-right: 1em; margin-right: 1em;
margin-left: 1em; margin-left: 1em;
padding-top: 1em; /* padding-top: 1em; */
padding-bottom: 1em; padding-bottom: 1em;
font-weight: bold; font-weight: bold;
} }
@ -1767,9 +1767,25 @@ ul.notes_module_list {
font-style: normal; font-style: normal;
} }
div.ue_list_div {
border: 3px solid rgb(35, 0, 160);
padding-left: 5px;
padding-top: 5px;
margin-bottom: 5px;
margin-right: 5px;
}
div.ue_list_tit_sem { div.ue_list_tit_sem {
font-size: 120%; font-size: 120%;
font-weight: bold; font-weight: bold;
color: orangered;
display: list-item; /* This has to be "list-item" */
list-style-type: disc; /* See https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type */
list-style-position: inside;
}
input.sco_tag_checkbox {
margin-bottom: 10px;
} }
.notes_ue_list a.stdlink { .notes_ue_list a.stdlink {
@ -1947,7 +1963,20 @@ table.notes_recapcomplet a:hover {
div.notes_bulletin { div.notes_bulletin {
margin-right: 5px; margin-right: 5px;
} }
div.bull_head {
display: grid;
justify-content: space-between;
grid-template-columns: auto auto;
}
div.bull_photo {
display: inline-block;
margin-right: 10px;
}
span.bulletin_menubar_but {
display: inline-block;
margin-left: 2em;
margin-right: 2em;
}
table.notes_bulletin { table.notes_bulletin {
border-collapse: collapse; border-collapse: collapse;
border: 2px solid rgb(100,100,240); border: 2px solid rgb(100,100,240);
@ -2087,12 +2116,6 @@ a.bull_link:hover {
text-decoration: underline; text-decoration: underline;
} }
table.bull_head {
width: 100%;
}
td.bull_photo {
text-align: right;
}
div.bulletin_menubar { div.bulletin_menubar {
padding-left: 25px; padding-left: 25px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -41,7 +41,7 @@ class releveBUT extends HTMLElement {
} }
set showData(data) { set showData(data) {
this.showInformations(data); // this.showInformations(data);
this.showSemestre(data); this.showSemestre(data);
this.showSynthese(data); this.showSynthese(data);
this.showEvaluations(data); this.showEvaluations(data);
@ -68,13 +68,7 @@ class releveBUT extends HTMLElement {
<div> <div>
<div class="wait"></div> <div class="wait"></div>
<main class="releve"> <main class="releve">
<!--------------------------->
<!-- Info. étudiant -->
<!--------------------------->
<section class=etudiant>
<img class=studentPic src="" alt="Photo de l'étudiant" width=100 height=120>
<div class=infoEtudiant></div>
</section>
<!---------------------------------------------------------------------------------------> <!--------------------------------------------------------------------------------------->
<!-- Zone spéciale pour que les IUT puisse ajouter des infos locales sur la passerelle --> <!-- Zone spéciale pour que les IUT puisse ajouter des infos locales sur la passerelle -->
@ -85,8 +79,8 @@ class releveBUT extends HTMLElement {
<!-- Semestre --> <!-- Semestre -->
<!---------------------------> <!--------------------------->
<section> <section>
<h2>Semestre </h2> <h2 id="identite_etudiant"></h2>
<div class=flex> <div>
<div class=infoSemestre></div> <div class=infoSemestre></div>
<div> <div>
<div class=decision></div> <div class=decision></div>
@ -103,7 +97,7 @@ class releveBUT extends HTMLElement {
<section> <section>
<div> <div>
<div> <div>
<h2>Synthèse</h2> <h2>Unités d'enseignement</h2>
<em>La moyenne des ressources dans une UE dépend des poids donnés aux évaluations.</em> <em>La moyenne des ressources dans une UE dépend des poids donnés aux évaluations.</em>
</div> </div>
<div class=CTA_Liste> <div class=CTA_Liste>
@ -132,7 +126,7 @@ class releveBUT extends HTMLElement {
<section> <section>
<div> <div>
<h2>S</h2> <h2>Situations d'apprentissage et d'évaluation (S)</h2>
<div class=CTA_Liste> <div class=CTA_Liste>
Liste <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> Liste <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 15l-6-6-6 6" /> <path d="M18 15l-6-6-6 6" />
@ -198,7 +192,8 @@ class releveBUT extends HTMLElement {
/* Information sur le semestre */ /* Information sur le semestre */
/*******************************/ /*******************************/
showSemestre(data) { showSemestre(data) {
this.shadow.querySelector("h2").innerHTML += data.semestre.numero;
this.shadow.querySelector("#identite_etudiant").innerHTML = ` ${data.etudiant.nomprenom} `;
this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription); this.shadow.querySelector(".dateInscription").innerHTML += this.ISOToDate(data.semestre.inscription);
let output = ` let output = `
<div> <div>
@ -212,7 +207,9 @@ class releveBUT extends HTMLElement {
<div class=enteteSemestre>Absences</div> <div class=enteteSemestre>Absences</div>
<div class=enteteSemestre>N.J. ${data.semestre.absences?.injustifie ?? "-"}</div> <div class=enteteSemestre>N.J. ${data.semestre.absences?.injustifie ?? "-"}</div>
<div style="grid-column: 2">Total ${data.semestre.absences?.total ?? "-"}</div> <div style="grid-column: 2">Total ${data.semestre.absences?.total ?? "-"}</div>
</div>`; </div>
<a class=photo href="${data.etudiant.fiche_url}"><img src="${data.etudiant.photo_url || "default_Student.svg"}" alt="photo de l'étudiant" title="fiche de l'étudiant" height="120" border="0"></a>
`;
/*${data.semestre.groupes.map(groupe => { /*${data.semestre.groupes.map(groupe => {
return ` return `
<div> <div>
@ -254,6 +251,7 @@ class releveBUT extends HTMLElement {
</h3> </h3>
<div> <div>
<div class=moyenne>Moyenne&nbsp;:&nbsp;${dataUE.moyenne?.value || "-"}</div> <div class=moyenne>Moyenne&nbsp;:&nbsp;${dataUE.moyenne?.value || "-"}</div>
<div class=rang>Rang&nbsp;:&nbsp;${dataUE.moyenne?.rang}&nbsp;/&nbsp;${dataUE.moyenne?.total}</div>
<div class=info> <div class=info>
Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;- Bonus&nbsp;:&nbsp;${dataUE.bonus || 0}&nbsp;-
Malus&nbsp;:&nbsp;${dataUE.malus || 0} Malus&nbsp;:&nbsp;${dataUE.malus || 0}

View File

@ -0,0 +1,34 @@
{# -*- mode: jinja-html -*- #}
{# Pied des bulletins HTML #}
<p>Situation actuelle:
{% if inscription_courante %}
<a class="stdlink" href="{{url_for(
"notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=inscription_courante.formsemestre_id)
}}">{{inscription_str}}</a>
{% else %}
{{inscription_str}}
{% endif %}
</p>
{% if formsemestre.modalite == "EXT" %}
<p><a href="{{
url_for('notes.formsemestre_ext_edit_ue_validations',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etud.id)}}"
class="stdlink">
Éditer les validations d'UE dans ce semestre extérieur
</a></p>
{% endif %}
{# Place du diagramme radar #}
<form id="params">
<input type="hidden" name="etudid" id="etudid" value="{{etud.id}}"/>
<input type="hidden" name="formsemestre_id" id="formsemestre_id" value="{{formsemestre.id}}"/>
</form>
<div id="radar_bulletin"></div>

View File

@ -0,0 +1,57 @@
{# -*- mode: jinja-html -*- #}
{# L'en-tête des bulletins HTML #}
{# was _formsemestre_bulletinetud_header_html #}
<div class="bull_head">
<div class="bull_head_text">
{% if not is_apc %}
<h2><a class="discretelink" href="{{
url_for(
"scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etudid,
)}}">{{etud.nomprenom}}</a></h2>
{% endif %}
<form name="f" method="GET" action="{{request.base_url}}">
<input type="hidden" name="formsemestre_id" value="{{formsemestre.id}}"></input>
<input type="hidden" name="etudid" value="{{etud.id}}"></input>
<input type="hidden" name="format" value="{{format}}"></input>
Bulletin
<span class="bull_liensemestre"><a href="{{
url_for("notes.formsemestre_status",
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id)}}">{{formsemestre.titre_mois()
}}</a></span>
<div>
<em>établi le {{time.strftime("%d/%m/%Y à %Hh%M")}} (notes sur 20)</em>
<span class="rightjust">
<select name="version" onchange="document.f.submit()" class="noprint">
{% for (v, e) in (
("short", "Version courte"),
("selectedevals", "Version intermédiaire"),
("long", "Version complète"),
) %}
<option value="{{v}}" {% if (v == version) %}selected{% endif %}>{{e}}</option>
{% endfor %}
</select>
</span>
<span class="bulletin_menubar">
<span class="bulletin_menubar_but">{{menu_autres_operations|safe}}</span>
<a href="{{url_for(
'notes.formsemestre_bulletinetud',
scodoc_dept=g.scodoc_dept,
formsemestre_id=formsemestre.id,
etudid=etud.id,
format='pdf',
version=version,
)}}">{{scu.ICON_PDF|safe}}</a>
</span>
</div>
</form>
</div>
{% if not is_apc %}
<div class="bull_photo"><a href="{{
url_for("scolar.ficheEtud", scodoc_dept=g.scodoc_dept, etudid=etud.id)
}}">{{etud.photo_html(title="fiche de " + etud["nom"])|safe}}</a>
</div>
{% endif %}
</div>

View File

@ -7,8 +7,13 @@
{% block app_content %} {% block app_content %}
{% include 'bul_head.html' %}
<releve-but></releve-but> <releve-but></releve-but>
<script src="/ScoDoc/static/js/releve-but.js"></script> <script src="/ScoDoc/static/js/releve-but.js"></script>
{% include 'bul_foot.html' %}
<script> <script>
let dataSrc = "{{bul_url|safe}}"; let dataSrc = "{{bul_url|safe}}";
fetch(dataSrc) fetch(dataSrc)

View File

@ -0,0 +1,50 @@
{# -*- mode: jinja-html -*- #}
{# Element HTML decrivant un semestre (barre de menu et infos) #}
{# was formsemestre_page_title #}
<div class="formsemestre_page_title">
<div class="infos">
<span class="semtitle"><a class="stdlink"
title="{{formsemestre.session_id}}"
href="{{url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)}}"
>TATO {{formsemestre.titre}}</a>
{%- if formsemestre.semestre_id != -1 -%}
<a
title="{{formsemestre.etapes_apo_str()
}}">, {{
formsemestre.formation.get_parcours().SESSION_NAME}}
{{formsemestre.semestre_id}}</a>
{%- endif -%}
{%- if formsemestre.modalite %} en {{formsemestre.modalite}}
{%- endif %}</span><span
class="dates"><a
title="du {{formsemestre.date_debut.strftime('%d/%m/%Y')}}
au {{formsemestre.date_fin.strftime('%d/%m/%Y')}} "
>{{formsemestre.mois_debut()}} - {{formsemestre.mois_fin()}}</a></span><span
class="resp"><a title="{{formsemestre.responsables_str(abbrev_prenom=False)}}">{{formsemestre.responsables_str()}}</a></span><span
class="nbinscrits"><a class="discretelink"
href="{{url_for('scolar.groups_view',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}}"
>{{formsemestre.etuds_inscriptions|length}} inscrits</a></span><span
class="lock">
{%-if formsemestre.etat -%}
<a href="{{ url_for( 'notes.formsemestre_change_lock',
scodoc_dept=scodoc_dept, formsemestre_id=formsemestre.id )}}">{{
scu.icontag("lock_img", border="0", title="Semestre verrouillé")|safe
}}</a>
{%- endif -%}
</span><span class="eye"><a href="{{
url_for('notes.formsemestre_change_publication_bul',
scodoc_dept=scodoc_dept, formsemestre_id=formsemestre.id )
}}">{%-
if formsemestre.bul_hide_xml -%}}
{{scu.icontag("hide_img", border="0", title="Bulletins NON publiés")|safe}}
{%- else -%}
{{scu.icontag("eye_img", border="0", title="Bulletins publiés")|safe}}
{%- endif -%}
</a></span>
</div>
{{sem_menu_bar|safe}}
</div>

View File

@ -50,27 +50,29 @@ def close_dept_db_connection(arg):
class ScoData: class ScoData:
"""Classe utilisée pour passer des valeurs aux vues (templates)""" """Classe utilisée pour passer des valeurs aux vues (templates)"""
def __init__(self): def __init__(self, etud=None, formsemestre=None):
# Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête) # Champs utilisés par toutes les pages ScoDoc (sidebar, en-tête)
self.Permission = Permission self.Permission = Permission
self.scu = scu self.scu = scu
self.SCOVERSION = sco_version.SCOVERSION self.SCOVERSION = sco_version.SCOVERSION
# -- Informations étudiant courant, si sélectionné: # -- Informations étudiant courant, si sélectionné:
if etud is None:
etudid = g.get("etudid", None) etudid = g.get("etudid", None)
if not etudid: if etudid is None:
if request.method == "GET": if request.method == "GET":
etudid = request.args.get("etudid", None) etudid = request.args.get("etudid", None)
elif request.method == "POST": elif request.method == "POST":
etudid = request.form.get("etudid", None) etudid = request.form.get("etudid", None)
if etudid is not None:
if etudid: etud = Identite.query.get_or_404(etudid)
self.etud = etud
if etud is not None:
# Infos sur l'étudiant courant # Infos sur l'étudiant courant
self.etud = Identite.query.get_or_404(etudid)
ins = self.etud.inscription_courante() ins = self.etud.inscription_courante()
if ins: if ins:
self.etud_cur_sem = ins.formsemestre self.etud_cur_sem = ins.formsemestre
self.nbabs, self.nbabsjust = sco_abs.get_abs_count_in_interval( self.nbabs, self.nbabsjust = sco_abs.get_abs_count_in_interval(
etudid, etud.id,
self.etud_cur_sem.date_debut.isoformat(), self.etud_cur_sem.date_debut.isoformat(),
self.etud_cur_sem.date_fin.isoformat(), self.etud_cur_sem.date_fin.isoformat(),
) )
@ -80,17 +82,22 @@ class ScoData:
else: else:
self.etud = None self.etud = None
# --- Informations sur semestre courant, si sélectionné # --- Informations sur semestre courant, si sélectionné
formsemestre_id = sco_formsemestre_status.retreive_formsemestre_from_request() if formsemestre is None:
if formsemestre_id is None: formsemestre_id = (
sco_formsemestre_status.retreive_formsemestre_from_request()
)
if formsemestre_id is not None:
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
if formsemestre is None:
self.sem = None self.sem = None
self.sem_menu_bar = None self.sem_menu_bar = None
else: else:
self.sem = FormSemestre.query.get_or_404(formsemestre_id) self.sem = formsemestre
self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar( self.sem_menu_bar = sco_formsemestre_status.formsemestre_status_menubar(
self.sem.to_dict() self.sem.to_dict()
) )
# --- Préférences # --- Préférences
self.prefs = sco_preferences.SemPreferences(formsemestre_id) self.prefs = sco_preferences.SemPreferences(formsemestre.id)
from app.views import scodoc, notes, scolar, absences, users, pn_modules, refcomp from app.views import scodoc, notes, scolar, absences, users, pn_modules, refcomp

View File

@ -32,6 +32,7 @@ Emmanuel Viennet, 2021
""" """
from operator import itemgetter from operator import itemgetter
import time
from xml.etree import ElementTree from xml.etree import ElementTree
import flask import flask
@ -276,7 +277,7 @@ sco_publish(
def formsemestre_bulletinetud( def formsemestre_bulletinetud(
etudid=None, etudid=None,
formsemestre_id=None, formsemestre_id=None,
format="html", format=None,
version="long", version="long",
xml_with_decisions=False, xml_with_decisions=False,
force_publishing=False, force_publishing=False,
@ -284,6 +285,7 @@ def formsemestre_bulletinetud(
code_nip=None, code_nip=None,
code_ine=None, code_ine=None,
): ):
format = format or "html"
if not formsemestre_id: if not formsemestre_id:
flask.abort(404, "argument manquant: formsemestre_id") flask.abort(404, "argument manquant: formsemestre_id")
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
@ -311,12 +313,16 @@ def formsemestre_bulletinetud(
if format == "json": if format == "json":
r = bulletin_but.BulletinBUT(formsemestre) r = bulletin_but.BulletinBUT(formsemestre)
return jsonify( return jsonify(
r.bulletin_etud(etud, formsemestre, force_publishing=force_publishing) r.bulletin_etud(
etud,
formsemestre,
force_publishing=force_publishing,
version=version,
)
) )
elif format == "html": elif format == "html":
return render_template( return render_template(
"but/bulletin.html", "but/bulletin.html",
title=f"Bul. {etud.nom} - BUT",
bul_url=url_for( bul_url=url_for(
"notes.formsemestre_bulletinetud", "notes.formsemestre_bulletinetud",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
@ -324,8 +330,21 @@ def formsemestre_bulletinetud(
etudid=etudid, etudid=etudid,
format="json", format="json",
force_publishing=1, # pour ScoDoc lui même force_publishing=1, # pour ScoDoc lui même
version=version,
), ),
sco=ScoData(), etud=etud,
formsemestre=formsemestre,
inscription_courante=etud.inscription_courante(),
inscription_str=etud.inscription_descr()["inscription_str"],
is_apc=formsemestre.formation.is_apc(),
menu_autres_operations=sco_bulletins.make_menu_autres_operations(
formsemestre, etud, "notes.formsemestre_bulletinetud", version
),
sco=ScoData(etud=etud),
scu=scu,
time=time,
title=f"Bul. {etud.nom} - BUT",
version=version,
) )
if not (etudid or code_nip or code_ine): if not (etudid or code_nip or code_ine):
@ -1929,7 +1948,7 @@ def formsemestre_bulletins_mailetuds(
nb_send = 0 nb_send = 0
for etudid in etudids: for etudid in etudids:
h, _ = sco_bulletins.do_formsemestre_bulletinetud( h, _ = sco_bulletins.do_formsemestre_bulletinetud(
formsemestre_id, formsemestre,
etudid, etudid,
version=version, version=version,
prefer_mail_perso=prefer_mail_perso, prefer_mail_perso=prefer_mail_perso,

View File

@ -513,7 +513,7 @@ def etud_info(etudid=None, format="xml"):
sem = etud["cursem"] sem = etud["cursem"]
if sem: if sem:
sco_groups.etud_add_group_infos(etud, sem) sco_groups.etud_add_group_infos(etud, sem["formsemestre_id"] if sem else None)
d["insemestre"] = [ d["insemestre"] = [
{ {
"current": "1", "current": "1",

View File

@ -1,7 +1,7 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
SCOVERSION = "9.1.72" SCOVERSION = "9.2a-72"
SCONAME = "ScoDoc" SCONAME = "ScoDoc"

View File

@ -33,6 +33,7 @@ from app.models.evaluations import Evaluation
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.views import notes, scolar from app.views import notes, scolar
import tools import tools
from tools.fakedatabase import create_test_api_database
from config import RunningConfig from config import RunningConfig
@ -84,6 +85,7 @@ def make_shell_context():
# ctx.push() # ctx.push()
# admin = User.query.filter_by(user_name="admin").first()
# login_user(admin) # login_user(admin)
@ -492,6 +494,19 @@ def clear_cache(sanitize): # clear-cache
formation.sanitize_old_formation() formation.sanitize_old_formation()
@app.cli.command()
def init_test_database():
"""Initialise les objets en base pour les tests API
(à appliquer sur SCODOC_TEST ou SCODOC_DEV)
"""
click.echo("Initialisation base de test API...")
# import app as mapp # le package app
ctx = app.test_request_context()
ctx.push()
create_test_api_database.init_test_database()
def recursive_help(cmd, parent=None): def recursive_help(cmd, parent=None):
ctx = click.core.Context(cmd, info_name=cmd.name, parent=parent) ctx = click.core.Context(cmd, info_name=cmd.name, parent=parent)
print(cmd.get_help(ctx)) print(cmd.get_help(ctx))

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="utf-8"?>
<formation id="220" titre="BUT R&amp;amp;T" version="1" formation_code="V1RET" dept_id="5" acronyme="BUT R&amp;amp;T" titre_officiel="Bachelor technologique réseaux et télécommunications" type_parcours="700" formation_id="220">
<ue acronyme="RT1.1" numero="1" titre="Administrer les réseaux et lInternet" type="0" ue_code="UCOD11" ects="12.0" is_external="0" code_apogee="" coefficient="0.0" semestre_idx="1" color="#B80004" reference="1896">
<matiere titre="Administrer les réseaux et lInternet" numero="1">
<module titre="Initiation aux réseaux informatiques" abbrev="Init aux réseaux informatiques" code="R101" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="10" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="12.0"/>
<coefficients ue_reference="1897" coef="4.0"/>
<coefficients ue_reference="1898" coef="4.0"/>
</module>
<module titre="Se sensibiliser à l&amp;apos;hygiène informatique et à la cybersécurité" abbrev="Hygiène informatique" code="SAE11" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="10" code_apogee="" module_type="3">
<coefficients ue_reference="1896" coef="16.0"/>
</module>
<module titre="Principe et architecture des réseaux" abbrev="" code="R102" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="20" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="12.0"/>
</module>
<module titre="Réseaux locaux et équipements actifs" abbrev="Réseaux locaux" code="R103" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="30" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="8.0"/>
<coefficients ue_reference="1897" coef="4.0"/>
</module>
<module titre="Fondamentaux des systèmes électroniques" abbrev="" code="R104" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="40" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="8.0"/>
<coefficients ue_reference="1897" coef="5.0"/>
</module>
<module titre="Architecture des systèmes numériques et informatiques" abbrev="" code="R106" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="60" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="10.0"/>
</module>
</matiere>
</ue>
<ue acronyme="RT2.1" numero="2" titre="Connecter les entreprises et les usagers" type="0" ue_code="UCOD12" ects="8.0" is_external="0" code_apogee="" coefficient="0.0" semestre_idx="1" color="#F97B3D" reference="1897">
<matiere titre="Connecter les entreprises et les usagers" numero="1">
<module titre="S&amp;apos;initier aux réseaux informatiques" abbrev="" code="SAE12" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="20" code_apogee="" module_type="3">
<coefficients ue_reference="1896" coef="33.0"/>
</module>
<module titre="Découvrir un dispositif de tranmission" abbrev="" code="SAE13" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="30" code_apogee="" module_type="3">
<coefficients ue_reference="1897" coef="33.0"/>
</module>
<module titre="Support de transmission pour les réseaux locaux" abbrev="Support de transmission" code="R105" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="50" code_apogee="" module_type="2">
<coefficients ue_reference="1897" coef="5.0"/>
</module>
<module titre="Anglais général et init vocabulaire technique" abbrev="" code="R110" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="100" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="3.0"/>
<coefficients ue_reference="1897" coef="5.0"/>
<coefficients ue_reference="1898" coef="5.0"/>
</module>
<module titre="Expression-culture-Communication Pro." abbrev="" code="R111" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="110" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="3.0"/>
<coefficients ue_reference="1897" coef="5.0"/>
<coefficients ue_reference="1898" coef="4.0"/>
</module>
<module titre="Mathématiques du signal" abbrev="" code="R113" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="130" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="5.0"/>
<coefficients ue_reference="1897" coef="8.0"/>
</module>
<module titre="Mathématiques des transmissions" abbrev="" code="R114" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="140" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="4.0"/>
<coefficients ue_reference="1897" coef="8.0"/>
</module>
</matiere>
</ue>
<ue acronyme="RT3.1" numero="3" titre="Créer des outils et applications informatiques pour les R&amp;amp;T" type="0" ue_code="UCOD13" ects="10.0" is_external="0" code_apogee="" coefficient="0.0" semestre_idx="1" color="#FEB40B" reference="1898">
<matiere titre="Créer des outils et applications informatiques pour les R&amp;amp;T" numero="1">
<module titre="Se présenter sur Internet" abbrev="" code="SAE14" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="40" code_apogee="" module_type="3">
<coefficients ue_reference="1898" coef="16.0"/>
</module>
<module titre="Traiter des données" abbrev="" code="SAE15" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="50" code_apogee="" module_type="3">
<coefficients ue_reference="1898" coef="26.0"/>
</module>
<module titre="Portofolio" abbrev="" code="SAE16" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="60" code_apogee="" module_type="3"/>
<module titre="Fondamentaux de la programmation" abbrev="" code="R107" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="70" code_apogee="" module_type="2">
<coefficients ue_reference="1898" coef="22.0"/>
</module>
<module titre="Base des systèmes d&amp;apos;exploitation" abbrev="" code="R108" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="80" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="6.0"/>
<coefficients ue_reference="1898" coef="7.0"/>
</module>
<module titre="Introduction aux technologies Web" abbrev="" code="R109" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="90" code_apogee="" module_type="2">
<coefficients ue_reference="1898" coef="4.0"/>
</module>
<module titre="PPP" abbrev="" code="R112" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="120" code_apogee="" module_type="2">
<coefficients ue_reference="1896" coef="2.0"/>
<coefficients ue_reference="1897" coef="3.0"/>
<coefficients ue_reference="1898" coef="4.0"/>
</module>
<module titre="Gestion de projets" abbrev="" code="R115" heures_cours="0.0" heures_td="0.0" heures_tp="0.0" coefficient="1.0" ects="" semestre_id="1" numero="150" code_apogee="" module_type="2">
<coefficients ue_reference="1897" coef="2.0"/>
<coefficients ue_reference="1898" coef="4.0"/>
</module>
</matiere>
</ue>
</formation>

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """XXX OBSOLETE
Scenario: préparation base de données pour tests Selenium Scenario: préparation base de données pour tests Selenium
S'utilise comme un test avec pytest, mais n'est pas un test ! S'utilise comme un test avec pytest, mais n'est pas un test !

View File

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
"""Initialise une base pour les tests de l'API ScoDoc 9
Création des départements, formations, semestres, étudiants, groupes...
utilisation:
1) modifier le .env pour indiquer
SCODOC_DATABASE_URI="postgresql:///SCO_TEST_API"
2) En tant qu'utilisateur scodoc, lancer:
tools/create_database.sh SCO_TEST_API
flask db upgrade
flask sco-db-init --erase
flask init-test-database
3) relancer ScoDoc:
flask run --host 0.0.0.0
4) lancer client de test (ou vérifier dans le navigateur)
"""
import datetime
import random
random.seed(12345678) # tests reproductibles
from flask_login import login_user
from app import auth
from app import models
from app import db
from app.scodoc import sco_formations
from tools.fakeportal.gen_nomprenoms import nomprenom
# La formation à utiliser:
FORMATION_XML_FILENAME = "tests/ressources/formations/scodoc_formation_RT_BUT_RT_v1.xml"
def init_departement(acronym):
"Create dept, and switch context into it."
import app as mapp
dept = models.Departement(acronym=acronym)
db.session.add(dept)
mapp.set_sco_dept(acronym)
db.session.commit()
return dept
def import_formation() -> models.Formation:
"""Import formation from XML.
Returns formation_id
"""
with open(FORMATION_XML_FILENAME) as f:
doc = f.read()
# --- Création de la formation
f = sco_formations.formation_import_xml(doc)
return models.Formation.query.get(f[0])
def create_user(dept):
"""créé les utilisaterurs nécessaires aux tests"""
user = auth.models.User(
user_name="test", nom="Doe", prenom="John", dept=dept.acronym
)
db.session.add(user)
db.session.commit()
return user
def create_fake_etud():
"""Créé un faux étudiant et l'insère dans la base"""
civilite = random.choice(("M", "F", "X"))
nom, prenom = nomprenom(civilite)
etud = models.Identite(civilite=civilite, nom=nom, prenom=prenom)
db.session.add(etud)
db.session.commit()
return etud
def create_etuds(nb=16):
"create nb etuds"
return [create_fake_etud() for _ in range(nb)]
def create_formsemestre(formation, user, semestre_idx=1):
"""Create formsemestre and moduleimpls"""
formsemestre = models.FormSemestre(
dept_id=formation.dept_id,
semestre_id=semestre_idx,
titre="Semestre test",
date_debut=datetime.datetime(2021, 9, 1),
date_fin=datetime.datetime(2022, 1, 31),
modalite="FI",
formation=formation,
)
db.session.add(formsemestre)
db.session.commit()
# Crée un modulimpl par module de ce semestre:
for module in formation.modules.filter_by(semestre_id=semestre_idx):
modimpl = models.ModuleImpl(
module_id=module.id, formsemestre_id=formsemestre.id, responsable_id=user.id
)
db.session.add(modimpl)
db.session.commit()
return formsemestre
def inscrit_etudiants(etuds, formsemestre):
"""Inscrit les etudiants aux semestres et à tous ses modules"""
for etud in etuds:
ins = models.FormSemestreInscription(
etudid=etud.id, formsemestre_id=formsemestre.id, etat="I"
)
db.session.add(ins)
for modimpl in formsemestre.modimpls:
insmod = models.ModuleImplInscription(
etudid=etud.id, moduleimpl_id=modimpl.id
)
db.session.add(insmod)
db.session.commit()
def init_test_database():
dept = init_departement("TAPI")
user = create_user(dept)
login_user(user)
etuds = create_etuds()
formation = import_formation()
formsemestre = create_formsemestre(formation, user)
inscrit_etudiants(etuds, formsemestre)
# à compléter
# - groupes
# - absences
# - notes
# - décisions de jury
# ...