Merge branch 'master' of https://scodoc.org/git/ScoDoc/ScoDoc
This commit is contained in:
commit
fa5536ce2c
|
@ -249,7 +249,7 @@ def create_app(config_class=DevConfig):
|
|||
host_name = socket.gethostname()
|
||||
mail_handler = ScoSMTPHandler(
|
||||
mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]),
|
||||
fromaddr="no-reply@" + app.config["MAIL_SERVER"],
|
||||
fromaddr=app.config["SCODOC_MAIL_FROM"],
|
||||
toaddrs=["exception@scodoc.org"],
|
||||
subject="ScoDoc Exception", # unused see ScoSMTPHandler
|
||||
credentials=auth,
|
||||
|
|
|
@ -8,7 +8,7 @@ def send_password_reset_email(user):
|
|||
token = user.get_reset_password_token()
|
||||
send_email(
|
||||
"[ScoDoc] Réinitialisation de votre mot de passe",
|
||||
sender=current_app.config["ADMINS"][0],
|
||||
sender=current_app.config["SCODOC_MAIL_FROM"],
|
||||
recipients=[user.email],
|
||||
text_body=render_template("email/reset_password.txt", user=user, token=token),
|
||||
html_body=render_template("email/reset_password.html", user=user, token=token),
|
||||
|
|
|
@ -112,6 +112,7 @@ class User(UserMixin, db.Model):
|
|||
self.password_hash = generate_password_hash(password)
|
||||
else:
|
||||
self.password_hash = None
|
||||
self.passwd_temp = False
|
||||
|
||||
def check_password(self, password):
|
||||
"""Check given password vs current one.
|
||||
|
|
|
@ -16,6 +16,7 @@ from app.comp import moy_ue, moy_sem, inscr_mod
|
|||
from app.models import ModuleImpl
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_cache import ResultatsSemestreBUTCache
|
||||
from app.scodoc import sco_abs
|
||||
from app.scodoc import sco_bulletins_json
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_utils import jsnan, fmt_note
|
||||
|
@ -107,17 +108,18 @@ class ResultatsSemestreBUT:
|
|||
ue_idx = self.modimpl_coefs_df.index.get_loc(ue.id)
|
||||
etud_moy_module = self.sem_cube[etud_idx] # module x UE
|
||||
for mi in modimpls:
|
||||
coef = self.modimpl_coefs_df[mi.id][ue.id]
|
||||
if coef > 0:
|
||||
d[mi.module.code] = {
|
||||
"id": mi.id,
|
||||
"coef": coef,
|
||||
"moyenne": fmt_note(
|
||||
etud_moy_module[self.modimpl_coefs_df.columns.get_loc(mi.id)][
|
||||
ue_idx
|
||||
]
|
||||
),
|
||||
}
|
||||
if self.modimpl_inscr_df[str(mi.id)][etud.id]: # si inscrit
|
||||
coef = self.modimpl_coefs_df[mi.id][ue.id]
|
||||
if coef > 0:
|
||||
d[mi.module.code] = {
|
||||
"id": mi.id,
|
||||
"coef": coef,
|
||||
"moyenne": fmt_note(
|
||||
etud_moy_module[
|
||||
self.modimpl_coefs_df.columns.get_loc(mi.id)
|
||||
][ue_idx]
|
||||
),
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_ue_results(self, etud, ue):
|
||||
|
@ -163,29 +165,30 @@ class ResultatsSemestreBUT:
|
|||
# moy_indicative_mod = np.nanmean(self.sem_cube[etud_idx, mod_idx])
|
||||
# except RuntimeWarning: # all nans in np.nanmean
|
||||
# pass
|
||||
d[mi.module.code] = {
|
||||
"id": mi.id,
|
||||
"titre": mi.module.titre,
|
||||
"code_apogee": mi.module.code_apogee,
|
||||
"url": url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=mi.id,
|
||||
),
|
||||
"moyenne": {
|
||||
# # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
|
||||
# "value": fmt_note(moy_indicative_mod),
|
||||
# "min": fmt_note(moyennes_etuds.min()),
|
||||
# "max": fmt_note(moyennes_etuds.max()),
|
||||
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||
},
|
||||
"evaluations": [
|
||||
self.etud_eval_results(etud, e)
|
||||
for eidx, e in enumerate(mi.evaluations)
|
||||
if e.visibulletin
|
||||
and self.modimpls_evaluations_complete[mi.id][eidx]
|
||||
],
|
||||
}
|
||||
if self.modimpl_inscr_df[str(mi.id)][etud.id]: # si inscrit
|
||||
d[mi.module.code] = {
|
||||
"id": mi.id,
|
||||
"titre": mi.module.titre,
|
||||
"code_apogee": mi.module.code_apogee,
|
||||
"url": url_for(
|
||||
"notes.moduleimpl_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
moduleimpl_id=mi.id,
|
||||
),
|
||||
"moyenne": {
|
||||
# # moyenne indicative de module: moyenne des UE, ignorant celles sans notes (nan)
|
||||
# "value": fmt_note(moy_indicative_mod),
|
||||
# "min": fmt_note(moyennes_etuds.min()),
|
||||
# "max": fmt_note(moyennes_etuds.max()),
|
||||
# "moy": fmt_note(moyennes_etuds.mean()),
|
||||
},
|
||||
"evaluations": [
|
||||
self.etud_eval_results(etud, e)
|
||||
for eidx, e in enumerate(mi.evaluations)
|
||||
if e.visibulletin
|
||||
and self.modimpls_evaluations_complete[mi.id][eidx]
|
||||
],
|
||||
}
|
||||
return d
|
||||
|
||||
def etud_eval_results(self, etud, e) -> dict:
|
||||
|
@ -217,13 +220,18 @@ class ResultatsSemestreBUT:
|
|||
}
|
||||
return d
|
||||
|
||||
def bulletin_etud(self, etud, formsemestre) -> dict:
|
||||
"""Le bulletin de l'étudiant dans ce semestre"""
|
||||
def bulletin_etud(self, etud, formsemestre, force_publishing=False) -> dict:
|
||||
"""Le bulletin de l'étudiant dans ce semestre.
|
||||
Si force_publishing, rempli le bulletin même si bul_hide_xml est vrai
|
||||
(bulletins non publiés).
|
||||
"""
|
||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
published = (not formsemestre.bul_hide_xml) or force_publishing
|
||||
d = {
|
||||
"version": "0",
|
||||
"type": "BUT",
|
||||
"date": datetime.datetime.utcnow().isoformat() + "Z",
|
||||
"publie": not formsemestre.bul_hide_xml,
|
||||
"etudiant": etud.to_dict_bul(),
|
||||
"formation": {
|
||||
"id": formsemestre.formation.id,
|
||||
|
@ -235,6 +243,10 @@ class ResultatsSemestreBUT:
|
|||
"etat_inscription": etat_inscription,
|
||||
"options": bulletin_option_affichage(formsemestre),
|
||||
}
|
||||
if not published:
|
||||
return d
|
||||
|
||||
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
||||
semestre_infos = {
|
||||
"etapes": [str(x.etape_apo) for x in formsemestre.etapes if x.etape_apo],
|
||||
"date_debut": formsemestre.date_debut.isoformat(),
|
||||
|
@ -243,9 +255,9 @@ class ResultatsSemestreBUT:
|
|||
"inscription": "TODO-MM-JJ", # XXX TODO
|
||||
"numero": formsemestre.semestre_id,
|
||||
"groupes": [], # XXX TODO
|
||||
"absences": { # XXX TODO
|
||||
"injustifie": 1,
|
||||
"total": 33,
|
||||
"absences": {
|
||||
"injustifie": nbabsjust,
|
||||
"total": nbabs,
|
||||
},
|
||||
}
|
||||
semestre_infos.update(
|
||||
|
|
|
@ -68,14 +68,15 @@ def bulletin_but_xml_compat(
|
|||
"bulletin_but_xml_compat( formsemestre_id=%s, etudid=%s )"
|
||||
% (formsemestre_id, etudid)
|
||||
)
|
||||
sem = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
etud = Identite.query.get_or_404(etudid)
|
||||
results = bulletin_but.ResultatsSemestreBUT(sem)
|
||||
results = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||
nb_inscrits = len(results.etuds)
|
||||
if (not sem.bul_hide_xml) or force_publishing:
|
||||
published = "1"
|
||||
etat_inscription = etud.etat_inscription(formsemestre.id)
|
||||
if (not formsemestre.bul_hide_xml) or force_publishing:
|
||||
published = 1
|
||||
else:
|
||||
published = "0"
|
||||
published = 0
|
||||
if xml_nodate:
|
||||
docdate = ""
|
||||
else:
|
||||
|
@ -84,12 +85,12 @@ def bulletin_but_xml_compat(
|
|||
"etudid": str(etudid),
|
||||
"formsemestre_id": str(formsemestre_id),
|
||||
"date": docdate,
|
||||
"publie": published,
|
||||
"publie": str(published),
|
||||
}
|
||||
if sem.etapes:
|
||||
el["etape_apo"] = sem.etapes[0].etape_apo or ""
|
||||
if formsemestre.etapes:
|
||||
el["etape_apo"] = formsemestre.etapes[0].etape_apo or ""
|
||||
n = 2
|
||||
for et in sem.etapes[1:]:
|
||||
for et in formsemestre.etapes[1:]:
|
||||
el["etape_apo" + str(n)] = et.etape_apo or ""
|
||||
n += 1
|
||||
x = Element("bulletinetud", **el)
|
||||
|
@ -117,117 +118,124 @@ def bulletin_but_xml_compat(
|
|||
)
|
||||
# Disponible pour publication ?
|
||||
if not published:
|
||||
return doc # stop !
|
||||
# Moyenne générale:
|
||||
doc.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(results.etud_moy_gen[etud.id]),
|
||||
min=scu.fmt_note(results.etud_moy_gen.min()),
|
||||
max=scu.fmt_note(results.etud_moy_gen.max()),
|
||||
moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen.
|
||||
)
|
||||
)
|
||||
rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative
|
||||
bonus = 0 # XXX TODO valeur du bonus sport
|
||||
doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits)))
|
||||
# XXX TODO: ajouter "rang_group" : rangs dans les partitions
|
||||
doc.append(Element("note_max", value="20")) # notes toujours sur 20
|
||||
doc.append(Element("bonus_sport_culture", value=str(bonus)))
|
||||
# Liste les UE / modules /evals
|
||||
for ue in results.ues:
|
||||
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
|
||||
nb_inscrits_ue = (
|
||||
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
|
||||
)
|
||||
x_ue = Element(
|
||||
"ue",
|
||||
id=str(ue.id),
|
||||
numero=scu.quote_xml_attr(ue.numero),
|
||||
acronyme=scu.quote_xml_attr(ue.acronyme or ""),
|
||||
titre=scu.quote_xml_attr(ue.titre or ""),
|
||||
code_apogee=scu.quote_xml_attr(ue.code_apogee or ""),
|
||||
)
|
||||
doc.append(x_ue)
|
||||
if ue.type != sco_codes_parcours.UE_SPORT:
|
||||
v = results.etud_moy_ue[ue.id][etud.id]
|
||||
else:
|
||||
v = 0 # XXX TODO valeur bonus sport pour cet étudiant
|
||||
x_ue.append(
|
||||
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
|
||||
scu.SCO_ENCODING
|
||||
) # stop !
|
||||
|
||||
if etat_inscription == scu.INSCRIT:
|
||||
# Moyenne générale:
|
||||
doc.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(v),
|
||||
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
|
||||
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
|
||||
value=scu.fmt_note(results.etud_moy_gen[etud.id]),
|
||||
min=scu.fmt_note(results.etud_moy_gen.min()),
|
||||
max=scu.fmt_note(results.etud_moy_gen.max()),
|
||||
moy=scu.fmt_note(results.etud_moy_gen.mean()), # moyenne des moy. gen.
|
||||
)
|
||||
)
|
||||
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
|
||||
x_ue.append(Element("rang", value=str(rang_ue)))
|
||||
x_ue.append(Element("effectif", value=str(nb_inscrits_ue)))
|
||||
# Liste les modules rattachés à cette UE
|
||||
for modimpl in results.modimpls:
|
||||
# Liste ici uniquement les modules rattachés à cette UE
|
||||
if modimpl.module.ue.id == ue.id:
|
||||
mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
|
||||
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
|
||||
x_mod = Element(
|
||||
"module",
|
||||
id=str(modimpl.id),
|
||||
code=str(modimpl.module.code or ""),
|
||||
coefficient=str(coef),
|
||||
numero=str(modimpl.module.numero or 0),
|
||||
titre=scu.quote_xml_attr(modimpl.module.titre or ""),
|
||||
abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""),
|
||||
code_apogee=scu.quote_xml_attr(modimpl.module.code_apogee or ""),
|
||||
rang = 0 # XXX TODO rang de l'étduiant selon la moy gen indicative
|
||||
bonus = 0 # XXX TODO valeur du bonus sport
|
||||
doc.append(Element("rang", value=str(rang), ninscrits=str(nb_inscrits)))
|
||||
# XXX TODO: ajouter "rang_group" : rangs dans les partitions
|
||||
doc.append(Element("note_max", value="20")) # notes toujours sur 20
|
||||
doc.append(Element("bonus_sport_culture", value=str(bonus)))
|
||||
# Liste les UE / modules /evals
|
||||
for ue in results.ues:
|
||||
rang_ue = 0 # XXX TODO rang de l'étudiant dans cette UE
|
||||
nb_inscrits_ue = (
|
||||
nb_inscrits # approx: compliqué de définir le "nb d'inscrit à une UE"
|
||||
)
|
||||
x_ue = Element(
|
||||
"ue",
|
||||
id=str(ue.id),
|
||||
numero=scu.quote_xml_attr(ue.numero),
|
||||
acronyme=scu.quote_xml_attr(ue.acronyme or ""),
|
||||
titre=scu.quote_xml_attr(ue.titre or ""),
|
||||
code_apogee=scu.quote_xml_attr(ue.code_apogee or ""),
|
||||
)
|
||||
doc.append(x_ue)
|
||||
if ue.type != sco_codes_parcours.UE_SPORT:
|
||||
v = results.etud_moy_ue[ue.id][etud.id]
|
||||
else:
|
||||
v = 0 # XXX TODO valeur bonus sport pour cet étudiant
|
||||
x_ue.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(v),
|
||||
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
|
||||
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
|
||||
)
|
||||
x_ue.append(x_mod)
|
||||
x_mod.append(
|
||||
Element(
|
||||
"note",
|
||||
value=mod_moy,
|
||||
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
|
||||
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
|
||||
moy=scu.fmt_note(results.etud_moy_ue[ue.id].mean()),
|
||||
)
|
||||
x_ue.append(Element("ects", value=str(ue.ects if ue.ects else 0)))
|
||||
x_ue.append(Element("rang", value=str(rang_ue)))
|
||||
x_ue.append(Element("effectif", value=str(nb_inscrits_ue)))
|
||||
# Liste les modules rattachés à cette UE
|
||||
for modimpl in results.modimpls:
|
||||
# Liste ici uniquement les modules rattachés à cette UE
|
||||
if modimpl.module.ue.id == ue.id:
|
||||
mod_moy = scu.fmt_note(results.etud_moy_ue[ue.id][etud.id])
|
||||
coef = results.modimpl_coefs_df[modimpl.id][ue.id]
|
||||
x_mod = Element(
|
||||
"module",
|
||||
id=str(modimpl.id),
|
||||
code=str(modimpl.module.code or ""),
|
||||
coefficient=str(coef),
|
||||
numero=str(modimpl.module.numero or 0),
|
||||
titre=scu.quote_xml_attr(modimpl.module.titre or ""),
|
||||
abbrev=scu.quote_xml_attr(modimpl.module.abbrev or ""),
|
||||
code_apogee=scu.quote_xml_attr(
|
||||
modimpl.module.code_apogee or ""
|
||||
),
|
||||
)
|
||||
)
|
||||
# XXX TODO rangs et effectifs
|
||||
# --- notes de chaque eval:
|
||||
if version != "short":
|
||||
for e in modimpl.evaluations:
|
||||
if e.visibulletin or version == "long":
|
||||
x_eval = Element(
|
||||
"evaluation",
|
||||
jour=e.jour.isoformat() if e.jour else "",
|
||||
heure_debut=e.heure_debut.isoformat()
|
||||
if e.heure_debut
|
||||
else "",
|
||||
heure_fin=e.heure_fin.isoformat()
|
||||
if e.heure_debut
|
||||
else "",
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
evaluation_type=str(e.evaluation_type),
|
||||
description=scu.quote_xml_attr(e.description),
|
||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||
note_max_origin=str(e.note_max),
|
||||
)
|
||||
x_mod.append(x_eval)
|
||||
x_eval.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(
|
||||
results.modimpls_evals_notes[e.moduleimpl_id][
|
||||
e.id
|
||||
][etud.id]
|
||||
),
|
||||
x_ue.append(x_mod)
|
||||
x_mod.append(
|
||||
Element(
|
||||
"note",
|
||||
value=mod_moy,
|
||||
min=scu.fmt_note(results.etud_moy_ue[ue.id].min()),
|
||||
max=scu.fmt_note(results.etud_moy_ue[ue.id].max()),
|
||||
moy=scu.fmt_note(results.etud_moy_ue[ue.id].mean()),
|
||||
)
|
||||
)
|
||||
# XXX TODO rangs et effectifs
|
||||
# --- notes de chaque eval:
|
||||
if version != "short":
|
||||
for e in modimpl.evaluations:
|
||||
if e.visibulletin or version == "long":
|
||||
x_eval = Element(
|
||||
"evaluation",
|
||||
jour=e.jour.isoformat() if e.jour else "",
|
||||
heure_debut=e.heure_debut.isoformat()
|
||||
if e.heure_debut
|
||||
else "",
|
||||
heure_fin=e.heure_fin.isoformat()
|
||||
if e.heure_debut
|
||||
else "",
|
||||
coefficient=str(e.coefficient),
|
||||
# pas les poids en XML compat
|
||||
evaluation_type=str(e.evaluation_type),
|
||||
description=scu.quote_xml_attr(e.description),
|
||||
# notes envoyées sur 20, ceci juste pour garder trace:
|
||||
note_max_origin=str(e.note_max),
|
||||
)
|
||||
)
|
||||
# XXX TODO: Evaluations incomplètes ou futures: XXX
|
||||
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
|
||||
x_mod.append(x_eval)
|
||||
x_eval.append(
|
||||
Element(
|
||||
"note",
|
||||
value=scu.fmt_note(
|
||||
results.modimpls_evals_notes[
|
||||
e.moduleimpl_id
|
||||
][e.id][etud.id],
|
||||
note_max=e.note_max,
|
||||
),
|
||||
)
|
||||
)
|
||||
# XXX TODO: Evaluations incomplètes ou futures: XXX
|
||||
# XXX TODO UE capitalisee (listee seulement si meilleure que l'UE courante)
|
||||
|
||||
# --- Absences
|
||||
if sco_preferences.get_preference("bul_show_abs", formsemestre_id):
|
||||
nbabs, nbabsjust = sem.get_abs_count(etud.id)
|
||||
nbabs, nbabsjust = formsemestre.get_abs_count(etud.id)
|
||||
doc.append(Element("absences", nbabs=str(nbabs), nbabsjust=str(nbabsjust)))
|
||||
|
||||
# -------- LA SUITE EST COPIEE SANS MODIF DE sco_bulletins_xml.py ---------
|
||||
|
|
|
@ -40,6 +40,7 @@ from app import log
|
|||
from app import models
|
||||
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def df_load_evaluations_poids(
|
||||
|
@ -60,7 +61,10 @@ def df_load_evaluations_poids(
|
|||
for eval_poids in EvaluationUEPoids.query.join(
|
||||
EvaluationUEPoids.evaluation
|
||||
).filter_by(moduleimpl_id=moduleimpl_id):
|
||||
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
|
||||
try:
|
||||
df[eval_poids.ue_id][eval_poids.evaluation_id] = eval_poids.poids
|
||||
except KeyError as exc:
|
||||
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
|
||||
if default_poids is not None:
|
||||
df.fillna(value=default_poids, inplace=True)
|
||||
return df, ues
|
||||
|
|
|
@ -121,6 +121,7 @@ def notes_sem_assemble_cube(modimpls_notes: list[pd.DataFrame]) -> np.ndarray:
|
|||
(DataFrames rendus par compute_module_moy, (etud x UE))
|
||||
Resultat: ndarray (etud x module x UE)
|
||||
"""
|
||||
assert len(modimpls_notes)
|
||||
modimpls_notes_arr = [df.values for df in modimpls_notes]
|
||||
modimpls_notes = np.stack(modimpls_notes_arr)
|
||||
# passe de (mod x etud x ue) à (etud x mod x UE)
|
||||
|
@ -156,8 +157,13 @@ def notes_sem_load_cube(formsemestre):
|
|||
modimpls_evaluations[modimpl.id] = evaluations
|
||||
modimpls_evaluations_complete[modimpl.id] = evaluations_completes
|
||||
modimpls_notes.append(etuds_moy_module)
|
||||
if len(modimpls_notes):
|
||||
cube = notes_sem_assemble_cube(modimpls_notes)
|
||||
else:
|
||||
nb_etuds = formsemestre.etuds.count()
|
||||
cube = np.zeros((nb_etuds, 0, 0), dtype=float)
|
||||
return (
|
||||
notes_sem_assemble_cube(modimpls_notes),
|
||||
cube,
|
||||
modimpls_evals_poids,
|
||||
modimpls_evals_notes,
|
||||
modimpls_evaluations,
|
||||
|
@ -191,8 +197,12 @@ def compute_ue_moys(
|
|||
Resultat: DataFrame columns UE, rows etudid
|
||||
"""
|
||||
nb_etuds, nb_modules, nb_ues = sem_cube.shape
|
||||
assert len(etuds) == nb_etuds
|
||||
assert len(modimpls) == nb_modules
|
||||
if nb_modules == 0 or nb_etuds == 0:
|
||||
return pd.DataFrame(
|
||||
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||
)
|
||||
assert len(etuds) == nb_etuds
|
||||
assert len(ues) == nb_ues
|
||||
assert modimpl_inscr_df.shape[0] == nb_etuds
|
||||
assert modimpl_inscr_df.shape[1] == nb_modules
|
||||
|
@ -200,10 +210,6 @@ def compute_ue_moys(
|
|||
assert modimpl_coefs_df.shape[1] == nb_modules
|
||||
modimpl_inscr = modimpl_inscr_df.values
|
||||
modimpl_coefs = modimpl_coefs_df.values
|
||||
if nb_etuds == 0:
|
||||
return pd.DataFrame(
|
||||
index=modimpl_inscr_df.index, columns=modimpl_coefs_df.index
|
||||
)
|
||||
# Duplique les inscriptions sur les UEs:
|
||||
modimpl_inscr_stacked = np.stack([modimpl_inscr] * nb_ues, axis=2)
|
||||
# Enlève les NaN du numérateur:
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# ScoDoc
|
||||
#
|
||||
# Copyright (c) 1999 - 2022 Emmanuel Viennet. All rights reserved.
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
#
|
||||
# Emmanuel Viennet emmanuel.viennet@viennet.net
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
"""
|
||||
Formulaires configuration Exports Apogée (codes)
|
||||
"""
|
||||
import re
|
||||
|
||||
from flask import flash, url_for, redirect, render_template
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SubmitField, validators
|
||||
from wtforms.fields.simple import StringField
|
||||
|
||||
from app import models
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.models import SHORT_STR_LEN
|
||||
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_utils as scu
|
||||
|
||||
|
||||
def _build_code_field(code):
|
||||
return StringField(
|
||||
label=code,
|
||||
description=sco_codes_parcours.CODES_EXPL[code],
|
||||
validators=[
|
||||
validators.regexp(
|
||||
r"^[A-Z0-9_]*$",
|
||||
message="Ne doit comporter que majuscules et des chiffres",
|
||||
),
|
||||
validators.Length(
|
||||
max=SHORT_STR_LEN,
|
||||
message=f"L'acronyme ne doit pas dépasser {SHORT_STR_LEN} caractères",
|
||||
),
|
||||
validators.DataRequired("code requis"),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class CodesDecisionsForm(FlaskForm):
|
||||
ADC = _build_code_field("ADC")
|
||||
ADJ = _build_code_field("ADJ")
|
||||
ADM = _build_code_field("ADM")
|
||||
AJ = _build_code_field("AJ")
|
||||
ATB = _build_code_field("ATB")
|
||||
ATJ = _build_code_field("ATJ")
|
||||
ATT = _build_code_field("ATT")
|
||||
CMP = _build_code_field("CMP")
|
||||
DEF = _build_code_field("DEF")
|
||||
DEM = _build_code_field("DEF")
|
||||
NAR = _build_code_field("NAR")
|
||||
RAT = _build_code_field("RAT")
|
||||
submit = SubmitField("Valider")
|
||||
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})
|
|
@ -6,13 +6,13 @@ XXX version préliminaire ScoDoc8 #sco8 sans département
|
|||
|
||||
CODE_STR_LEN = 16 # chaine pour les codes
|
||||
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
|
||||
APO_CODE_STR_LEN = 24 # nb de car max d'un code Apogée
|
||||
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
|
||||
GROUPNAME_STR_LEN = 64
|
||||
|
||||
from app.models.raw_sql_init import create_database_functions
|
||||
|
||||
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
from app.models.departements import Departement
|
||||
|
||||
from app.models.entreprises import (
|
||||
|
@ -63,7 +63,7 @@ from app.models.notes import (
|
|||
NotesNotes,
|
||||
NotesNotesLog,
|
||||
)
|
||||
from app.models.preferences import ScoPreference, ScoDocSiteConfig
|
||||
from app.models.preferences import ScoPreference
|
||||
|
||||
from app.models.but_refcomp import (
|
||||
ApcReferentielCompetences,
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
# -*- coding: UTF-8 -*
|
||||
|
||||
"""Model : site config WORK IN PROGRESS #WIP
|
||||
"""
|
||||
|
||||
from app import db, log
|
||||
from app.scodoc import bonus_sport
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
import functools
|
||||
|
||||
from app.scodoc.sco_codes_parcours import (
|
||||
ADC,
|
||||
ADJ,
|
||||
ADM,
|
||||
AJ,
|
||||
ATB,
|
||||
ATJ,
|
||||
ATT,
|
||||
CMP,
|
||||
DEF,
|
||||
DEM,
|
||||
NAR,
|
||||
RAT,
|
||||
)
|
||||
|
||||
CODES_SCODOC_TO_APO = {
|
||||
ADC: "ADMC",
|
||||
ADJ: "ADM",
|
||||
ADM: "ADM",
|
||||
AJ: "AJ",
|
||||
ATB: "AJAC",
|
||||
ATJ: "AJAC",
|
||||
ATT: "AJAC",
|
||||
CMP: "COMP",
|
||||
DEF: "NAR",
|
||||
DEM: "NAR",
|
||||
NAR: "NAR",
|
||||
RAT: "ATT",
|
||||
}
|
||||
|
||||
|
||||
def code_scodoc_to_apo_default(code):
|
||||
"""Conversion code jury ScoDoc en code Apogée
|
||||
(codes par défaut, c'est configurable via ScoDocSiteConfig.get_code_apo)
|
||||
"""
|
||||
return CODES_SCODOC_TO_APO.get(code, "DEF")
|
||||
|
||||
|
||||
class ScoDocSiteConfig(db.Model):
|
||||
"""Config. d'un site
|
||||
Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions
|
||||
antérieures étaient dans scodoc_config.py
|
||||
"""
|
||||
|
||||
__tablename__ = "scodoc_site_config"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), nullable=False, index=True)
|
||||
value = db.Column(db.Text())
|
||||
|
||||
BONUS_SPORT = "bonus_sport_func_name"
|
||||
NAMES = {
|
||||
BONUS_SPORT: str,
|
||||
"always_require_ine": bool,
|
||||
"SCOLAR_FONT": str,
|
||||
"SCOLAR_FONT_SIZE": str,
|
||||
"SCOLAR_FONT_SIZE_FOOT": str,
|
||||
"INSTITUTION_NAME": str,
|
||||
"INSTITUTION_ADDRESS": str,
|
||||
"INSTITUTION_CITY": str,
|
||||
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
||||
}
|
||||
|
||||
def __init__(self, name, value):
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>"
|
||||
|
||||
@classmethod
|
||||
def get_dict(cls) -> dict:
|
||||
"Returns all data as a dict name = value"
|
||||
return {
|
||||
c.name: cls.NAMES.get(c.name, lambda x: x)(c.value)
|
||||
for c in ScoDocSiteConfig.query.all()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def set_bonus_sport_func(cls, func_name):
|
||||
"""Record bonus_sport config.
|
||||
If func_name not defined, raise NameError
|
||||
"""
|
||||
if func_name not in cls.get_bonus_sport_func_names():
|
||||
raise NameError("invalid function name for bonus_sport")
|
||||
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
|
||||
if c:
|
||||
log("setting to " + func_name)
|
||||
c.value = func_name
|
||||
else:
|
||||
c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name)
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func_name(cls):
|
||||
"""Get configured bonus function name, or None if None."""
|
||||
f = cls.get_bonus_sport_func_from_name()
|
||||
if f is None:
|
||||
return ""
|
||||
else:
|
||||
return f.__name__
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func(cls):
|
||||
"""Get configured bonus function, or None if None."""
|
||||
return cls.get_bonus_sport_func_from_name()
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func_from_name(cls, func_name=None):
|
||||
"""returns bonus func with specified name.
|
||||
If name not specified, return the configured function.
|
||||
None if no bonus function configured.
|
||||
Raises ScoValueError if func_name not found in module bonus_sport.
|
||||
"""
|
||||
if func_name is None:
|
||||
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
|
||||
if c is None:
|
||||
return None
|
||||
func_name = c.value
|
||||
if func_name == "": # pas de bonus défini
|
||||
return None
|
||||
try:
|
||||
return getattr(bonus_sport, func_name)
|
||||
except AttributeError:
|
||||
raise ScoValueError(
|
||||
f"""Fonction de calcul maison inexistante: {func_name}.
|
||||
(contacter votre administrateur local)."""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func_names(cls):
|
||||
"""List available functions names
|
||||
(starting with empty string to represent "no bonus function").
|
||||
"""
|
||||
return [""] + sorted(
|
||||
[
|
||||
getattr(bonus_sport, name).__name__
|
||||
for name in dir(bonus_sport)
|
||||
if name.startswith("bonus_")
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_code_apo(cls, code: str) -> str:
|
||||
"""La représentation d'un code pour les exports Apogée.
|
||||
Par exemple, à l'iUT du H., le code ADM est réprésenté par VAL
|
||||
Les codes par défaut sont donnés dans sco_apogee_csv.
|
||||
|
||||
"""
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name=code).first()
|
||||
if not cfg:
|
||||
code_apo = code_scodoc_to_apo_default(code)
|
||||
else:
|
||||
code_apo = cfg.value
|
||||
return code_apo
|
||||
|
||||
@classmethod
|
||||
def set_code_apo(cls, code: str, code_apo: str):
|
||||
"""Enregistre nouvelle représentation du code"""
|
||||
if code_apo != cls.get_code_apo(code):
|
||||
cfg = ScoDocSiteConfig.query.filter_by(name=code).first()
|
||||
if cfg is None:
|
||||
cfg = ScoDocSiteConfig(code, code_apo)
|
||||
else:
|
||||
cfg.value = code_apo
|
||||
db.session.add(cfg)
|
||||
db.session.commit()
|
|
@ -38,8 +38,8 @@ class Identite(db.Model):
|
|||
boursier = db.Column(db.Boolean()) # True si boursier ('O' en ScoDoc7)
|
||||
photo_filename = db.Column(db.Text())
|
||||
# Codes INE et NIP pas unique car le meme etud peut etre ds plusieurs dept
|
||||
code_nip = db.Column(db.Text())
|
||||
code_ine = db.Column(db.Text())
|
||||
code_nip = db.Column(db.Text(), index=True)
|
||||
code_ine = db.Column(db.Text(), index=True)
|
||||
# Ancien id ScoDoc7 pour les migrations de bases anciennes
|
||||
# ne pas utiliser après migrate_scodoc7_dept_archives
|
||||
scodoc7_id = db.Column(db.Text(), nullable=True)
|
||||
|
|
|
@ -103,7 +103,16 @@ class Evaluation(db.Model):
|
|||
Note: si les poids ne sont pas initialisés (poids par défaut),
|
||||
ils ne sont pas affichés.
|
||||
"""
|
||||
return ", ".join([f"{p.ue.acronyme}: {p.poids}" for p in self.ue_poids])
|
||||
# restreint aux UE du semestre dans lequel est cette évaluation
|
||||
# au cas où le module ait changé de semestre et qu'il reste des poids
|
||||
evaluation_semestre_idx = self.moduleimpl.module.semestre_id
|
||||
return ", ".join(
|
||||
[
|
||||
f"{p.ue.acronyme}: {p.poids}"
|
||||
for p in self.ue_poids
|
||||
if evaluation_semestre_idx == p.ue.semestre_idx
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class EvaluationUEPoids(db.Model):
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
"""ScoDoc 9 models : Formations
|
||||
"""
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.comp import df_cache
|
||||
from app.models import SHORT_STR_LEN
|
||||
from app.models.modules import Module
|
||||
from app.models.ues import UniteEns
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
|
@ -97,19 +100,24 @@ class Formation(db.Model):
|
|||
for sem in self.formsemestres:
|
||||
sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
|
||||
|
||||
def force_semestre_modules_aux_ues(self) -> None:
|
||||
def sanitize_old_formation(self) -> None:
|
||||
"""
|
||||
Affecte à chaque module de cette formation le semestre de son UE de rattachement,
|
||||
Corrige si nécessaire certains champs issus d'anciennes versions de ScoDoc:
|
||||
- affecte à chaque module de cette formation le semestre de son UE de rattachement,
|
||||
si elle en a une.
|
||||
- si le module_type n'est pas renseigné, le met à STANDARD.
|
||||
|
||||
Devrait être appelé lorsqu'on change le type de formation vers le BUT, et aussi
|
||||
lorsqu'on change le semestre d'une UE BUT.
|
||||
Utile pour la migration des anciennes formations vers le BUT.
|
||||
Invalide les caches coefs/poids.
|
||||
|
||||
En cas de changement, invalide les caches coefs/poids.
|
||||
"""
|
||||
if not self.is_apc():
|
||||
return
|
||||
change = False
|
||||
for mod in self.modules:
|
||||
# --- Indices de semestres:
|
||||
if (
|
||||
mod.ue.semestre_idx is not None
|
||||
and mod.ue.semestre_idx > 0
|
||||
|
@ -118,9 +126,23 @@ class Formation(db.Model):
|
|||
mod.semestre_id = mod.ue.semestre_idx
|
||||
db.session.add(mod)
|
||||
change = True
|
||||
# --- Types de modules
|
||||
if mod.module_type is None:
|
||||
mod.module_type = scu.ModuleType.STANDARD
|
||||
db.session.add(mod)
|
||||
change = True
|
||||
# --- Numéros de modules
|
||||
if Module.query.filter_by(formation_id=self.id, numero=None).count() > 0:
|
||||
scu.objects_renumber(db, self.modules.all())
|
||||
# --- Types d'UE (avant de rendre le type non nullable)
|
||||
ues_sans_type = UniteEns.query.filter_by(formation_id=self.id, type=None)
|
||||
if ues_sans_type.count() > 0:
|
||||
for ue in ues_sans_type:
|
||||
ue.type = 0
|
||||
db.session.add(ue)
|
||||
|
||||
db.session.commit()
|
||||
if change:
|
||||
self.invalidate_module_coefs()
|
||||
app.clear_scodoc_cache()
|
||||
|
||||
|
||||
class Matiere(db.Model):
|
||||
|
|
|
@ -224,7 +224,7 @@ class FormSemestre(db.Model):
|
|||
self.date_fin.year})"""
|
||||
|
||||
def titre_num(self) -> str:
|
||||
"""Le titre est le semestre, ex ""DUT Informatique semestre 2"" """
|
||||
"""Le titre et le semestre, ex ""DUT Informatique semestre 2"" """
|
||||
if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
|
||||
return self.titre
|
||||
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"
|
||||
|
|
|
@ -44,6 +44,9 @@ class ModuleImpl(db.Model):
|
|||
def __init__(self, **kwargs):
|
||||
super(ModuleImpl, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__} {self.id} module={repr(self.module)}>"
|
||||
|
||||
def get_evaluations_poids(self) -> pd.DataFrame:
|
||||
"""Les poids des évaluations vers les UE (accès via cache)"""
|
||||
evaluations_poids = df_cache.EvaluationsPoidsCache.get(self.id)
|
||||
|
|
|
@ -2,9 +2,8 @@
|
|||
|
||||
"""Model : preferences
|
||||
"""
|
||||
from app import db, log
|
||||
from app.scodoc import bonus_sport
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
from app import db
|
||||
|
||||
|
||||
class ScoPreference(db.Model):
|
||||
|
@ -19,108 +18,3 @@ class ScoPreference(db.Model):
|
|||
name = db.Column(db.String(128), nullable=False, index=True)
|
||||
value = db.Column(db.Text())
|
||||
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
|
||||
|
||||
|
||||
class ScoDocSiteConfig(db.Model):
|
||||
"""Config. d'un site
|
||||
Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions
|
||||
antérieures étaient dans scodoc_config.py
|
||||
"""
|
||||
|
||||
__tablename__ = "scodoc_site_config"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), nullable=False, index=True)
|
||||
value = db.Column(db.Text())
|
||||
|
||||
BONUS_SPORT = "bonus_sport_func_name"
|
||||
NAMES = {
|
||||
BONUS_SPORT: str,
|
||||
"always_require_ine": bool,
|
||||
"SCOLAR_FONT": str,
|
||||
"SCOLAR_FONT_SIZE": str,
|
||||
"SCOLAR_FONT_SIZE_FOOT": str,
|
||||
"INSTITUTION_NAME": str,
|
||||
"INSTITUTION_ADDRESS": str,
|
||||
"INSTITUTION_CITY": str,
|
||||
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
|
||||
}
|
||||
|
||||
def __init__(self, name, value):
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>"
|
||||
|
||||
def get_dict(self) -> dict:
|
||||
"Returns all data as a dict name = value"
|
||||
return {
|
||||
c.name: self.NAMES.get(c.name, lambda x: x)(c.value)
|
||||
for c in ScoDocSiteConfig.query.all()
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def set_bonus_sport_func(cls, func_name):
|
||||
"""Record bonus_sport config.
|
||||
If func_name not defined, raise NameError
|
||||
"""
|
||||
if func_name not in cls.get_bonus_sport_func_names():
|
||||
raise NameError("invalid function name for bonus_sport")
|
||||
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
|
||||
if c:
|
||||
log("setting to " + func_name)
|
||||
c.value = func_name
|
||||
else:
|
||||
c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name)
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func_name(cls):
|
||||
"""Get configured bonus function name, or None if None."""
|
||||
f = cls.get_bonus_sport_func_from_name()
|
||||
if f is None:
|
||||
return ""
|
||||
else:
|
||||
return f.__name__
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func(cls):
|
||||
"""Get configured bonus function, or None if None."""
|
||||
return cls.get_bonus_sport_func_from_name()
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func_from_name(cls, func_name=None):
|
||||
"""returns bonus func with specified name.
|
||||
If name not specified, return the configured function.
|
||||
None if no bonus function configured.
|
||||
Raises ScoValueError if func_name not found in module bonus_sport.
|
||||
"""
|
||||
if func_name is None:
|
||||
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
|
||||
if c is None:
|
||||
return None
|
||||
func_name = c.value
|
||||
if func_name == "": # pas de bonus défini
|
||||
return None
|
||||
try:
|
||||
return getattr(bonus_sport, func_name)
|
||||
except AttributeError:
|
||||
raise ScoValueError(
|
||||
f"""Fonction de calcul maison inexistante: {func_name}.
|
||||
(contacter votre administrateur local)."""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_bonus_sport_func_names(cls):
|
||||
"""List available functions names
|
||||
(starting with empty string to represent "no bonus function").
|
||||
"""
|
||||
return [""] + sorted(
|
||||
[
|
||||
getattr(bonus_sport, name).__name__
|
||||
for name in dir(bonus_sport)
|
||||
if name.startswith("bonus_")
|
||||
]
|
||||
)
|
||||
|
|
|
@ -46,7 +46,10 @@ class UniteEns(db.Model):
|
|||
modules = db.relationship("Module", lazy="dynamic", backref="ue")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>"
|
||||
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
|
||||
self.formation_id}, acronyme='{self.acronyme}', semestre_idx={
|
||||
self.semestre_idx} {
|
||||
'EXTERNE' if self.is_external else ''})>"""
|
||||
|
||||
def to_dict(self):
|
||||
"""as a dict, with the same conversions as in ScoDoc7"""
|
||||
|
|
|
@ -35,13 +35,15 @@ Created on Thu Sep 8 09:36:33 2016
|
|||
|
||||
@author: barasc
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
import datetime
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
|
||||
from flask import g
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app import log
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
|
@ -54,7 +56,6 @@ if not PE_DEBUG:
|
|||
# kw is ignored. log always add a newline
|
||||
log(" ".join(a))
|
||||
|
||||
|
||||
else:
|
||||
pe_print = print # print function
|
||||
|
||||
|
@ -206,7 +207,9 @@ def add_pe_stuff_to_zip(zipfile, ziproot):
|
|||
for name in logos_names:
|
||||
logo = find_logo(logoname=name, dept_id=g.scodoc_dept_id)
|
||||
if logo is not None:
|
||||
add_local_file_to_zip(zipfile, ziproot, logo, "avis/logos/" + logo.filename)
|
||||
add_local_file_to_zip(
|
||||
zipfile, ziproot, logo.filepath, "avis/logos/" + logo.filename
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------
|
||||
|
|
|
@ -97,7 +97,7 @@ def pe_view_sem_recap(
|
|||
template_latex = ""
|
||||
# template fourni via le formulaire Web
|
||||
if avis_tmpl_file:
|
||||
template_latex = avis_tmpl_file.read()
|
||||
template_latex = avis_tmpl_file.read().decode('utf-8')
|
||||
template_latex = template_latex
|
||||
else:
|
||||
# template indiqué dans préférences ScoDoc ?
|
||||
|
@ -114,7 +114,7 @@ def pe_view_sem_recap(
|
|||
footer_latex = ""
|
||||
# template fourni via le formulaire Web
|
||||
if footer_tmpl_file:
|
||||
footer_latex = footer_tmpl_file.read()
|
||||
footer_latex = footer_tmpl_file.read().decode('utf-8')
|
||||
footer_latex = footer_latex
|
||||
else:
|
||||
footer_latex = pe_avislatex.get_code_latex_from_scodoc_preference(
|
||||
|
|
|
@ -9,6 +9,12 @@
|
|||
v 1.3 (python3)
|
||||
"""
|
||||
import html
|
||||
import re
|
||||
|
||||
# re validant dd/mm/yyyy
|
||||
DMY_REGEXP = re.compile(
|
||||
r"^(?:(?:31(\/|-|\.)(?:0?[13578]|1[02]))\1|(?:(?:29|30)(\/|-|\.)(?:0?[13-9]|1[0-2])\2))(?:(?:1[6-9]|[2-9]\d)?\d{2})$|^(?:29(\/|-|\.)0?2\3(?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))$|^(?:0?[1-9]|1\d|2[0-8])(\/|-|\.)(?:(?:0?[1-9])|(?:1[0-2]))\4(?:(?:1[6-9]|[2-9]\d)?\d{2})$"
|
||||
)
|
||||
|
||||
|
||||
def TrivialFormulator(
|
||||
|
@ -66,8 +72,8 @@ def TrivialFormulator(
|
|||
HTML elements:
|
||||
input_type : 'text', 'textarea', 'password',
|
||||
'radio', 'menu', 'checkbox',
|
||||
'hidden', 'separator', 'file', 'date', 'boolcheckbox',
|
||||
'text_suggest'
|
||||
'hidden', 'separator', 'file', 'date', 'datedmy' (avec validation),
|
||||
'boolcheckbox', 'text_suggest'
|
||||
(default text)
|
||||
size : text field width
|
||||
rows, cols: textarea geometry
|
||||
|
@ -243,6 +249,8 @@ class TF(object):
|
|||
"Le champ '%s' doit être renseigné" % descr.get("title", field)
|
||||
)
|
||||
ok = 0
|
||||
elif val == "" or val == None:
|
||||
continue # allowed empty field, skip
|
||||
# type
|
||||
typ = descr.get("type", "string")
|
||||
if val != "" and val != None:
|
||||
|
@ -300,6 +308,10 @@ class TF(object):
|
|||
if not descr["validator"](val, field):
|
||||
msg.append("valeur invalide (%s) pour le champ '%s'" % (val, field))
|
||||
ok = 0
|
||||
elif descr.get("input_type") == "datedmy":
|
||||
if not DMY_REGEXP.match(val):
|
||||
msg.append("valeur invalide (%s) pour la date '%s'" % (val, field))
|
||||
ok = 0
|
||||
# boolean checkbox
|
||||
if descr.get("input_type", None) == "boolcheckbox":
|
||||
if int(val):
|
||||
|
@ -564,7 +576,9 @@ class TF(object):
|
|||
'<input type="file" name="%s" size="%s" value="%s" %s>'
|
||||
% (field, size, values[field], attribs)
|
||||
)
|
||||
elif input_type == "date": # JavaScript widget for date input
|
||||
elif (
|
||||
input_type == "date" or input_type == "datedmy"
|
||||
): # JavaScript widget for date input
|
||||
lem.append(
|
||||
'<input type="text" name="%s" size="10" value="%s" class="datepicker">'
|
||||
% (field, values[field])
|
||||
|
|
|
@ -416,6 +416,20 @@ def bonus_iutbeziers(notes_sport, coefs, infos=None):
|
|||
return bonus
|
||||
|
||||
|
||||
def bonus_iutlr(notes_sport, coefs, infos=None):
|
||||
"""Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle
|
||||
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point
|
||||
Si la note de sport est comprise entre 10.1 et 20 : ajout de 1% de cette note sur la moyenne générale du semestre
|
||||
"""
|
||||
# les coefs sont ignorés
|
||||
# une seule note
|
||||
note_sport = notes_sport[0]
|
||||
if note_sport <= 10:
|
||||
return 0
|
||||
bonus = note_sport * 0.01 # 1%
|
||||
return bonus
|
||||
|
||||
|
||||
def bonus_demo(notes_sport, coefs, infos=None):
|
||||
"""Fausse fonction "bonus" pour afficher les informations disponibles
|
||||
et aider les développeurs.
|
||||
|
|
|
@ -58,6 +58,7 @@ from app.scodoc import sco_utils as scu
|
|||
from app.scodoc import sco_excel
|
||||
from app.scodoc import sco_pdf
|
||||
from app.scodoc import sco_xml
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError
|
||||
from app.scodoc.sco_pdf import SU
|
||||
from app import log
|
||||
|
||||
|
@ -539,17 +540,18 @@ class GenTable(object):
|
|||
#
|
||||
# titles = ["<para><b>%s</b></para>" % x for x in self.get_titles_list()]
|
||||
pdf_style_list = []
|
||||
Pt = [
|
||||
[Paragraph(SU(str(x)), CellStyle) for x in line]
|
||||
for line in (
|
||||
self.get_data_list(
|
||||
pdf_mode=True,
|
||||
pdf_style_list=pdf_style_list,
|
||||
with_titles=True,
|
||||
omit_hidden_lines=True,
|
||||
)
|
||||
)
|
||||
]
|
||||
data_list = self.get_data_list(
|
||||
pdf_mode=True,
|
||||
pdf_style_list=pdf_style_list,
|
||||
with_titles=True,
|
||||
omit_hidden_lines=True,
|
||||
)
|
||||
try:
|
||||
Pt = [
|
||||
[Paragraph(SU(str(x)), CellStyle) for x in line] for line in data_list
|
||||
]
|
||||
except ValueError as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
pdf_style_list += self.pdf_table_style
|
||||
T = Table(Pt, repeatRows=1, colWidths=self.pdf_col_widths, style=pdf_style_list)
|
||||
|
||||
|
|
|
@ -35,13 +35,14 @@ from flask_login import current_user
|
|||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from sco_version import SCOVERSION
|
||||
|
||||
|
||||
def sidebar_common():
|
||||
"partie commune à toutes les sidebar"
|
||||
home_link = url_for("scodoc.index", scodoc_dept=g.scodoc_dept)
|
||||
H = [
|
||||
f"""<a class="scodoc_title" href="{home_link}">ScoDoc 9.1</a><br>
|
||||
f"""<a class="scodoc_title" href="{home_link}">ScoDoc {SCOVERSION}</a><br>
|
||||
<a href="{home_link}" class="sidebar">Accueil</a> <br>
|
||||
<div id="authuser"><a id="authuserlink" href="{
|
||||
url_for("users.user_info_page",
|
||||
|
|
|
@ -814,7 +814,12 @@ class NotesTable:
|
|||
moy_ue_cap = ue_cap["moy"]
|
||||
mu["was_capitalized"] = True
|
||||
event_date = event_date or ue_cap["event_date"]
|
||||
if (moy_ue_cap != "NA") and (moy_ue_cap > max_moy_ue):
|
||||
if (
|
||||
(moy_ue_cap != "NA")
|
||||
and isinstance(moy_ue_cap, float)
|
||||
and isinstance(max_moy_ue, float)
|
||||
and (moy_ue_cap > max_moy_ue)
|
||||
):
|
||||
# meilleure UE capitalisée
|
||||
event_date = ue_cap["event_date"]
|
||||
max_moy_ue = moy_ue_cap
|
||||
|
@ -1354,7 +1359,11 @@ class NotesTable:
|
|||
t[0] = results.etud_moy_gen[etudid]
|
||||
for i, ue in enumerate(ues, start=1):
|
||||
if ue["type"] != UE_SPORT:
|
||||
t[i] = results.etud_moy_ue[ue["id"]][etudid]
|
||||
# temporaire pour 9.1.29 !
|
||||
if ue["id"] in results.etud_moy_ue:
|
||||
t[i] = results.etud_moy_ue[ue["id"]][etudid]
|
||||
else:
|
||||
t[i] = ""
|
||||
# re-trie selon la nouvelle moyenne générale:
|
||||
self.T.sort(key=self._row_key)
|
||||
# Remplace aussi le rang:
|
||||
|
|
|
@ -108,13 +108,14 @@ def apo_compare_csv(A_file, B_file, autodetect=True):
|
|||
|
||||
def _load_apo_data(csvfile, autodetect=True):
|
||||
"Read data from request variable and build ApoData"
|
||||
data = csvfile.read()
|
||||
data_b = csvfile.read()
|
||||
if autodetect:
|
||||
data, message = sco_apogee_csv.fix_data_encoding(data)
|
||||
data_b, message = sco_apogee_csv.fix_data_encoding(data_b)
|
||||
if message:
|
||||
log("apo_compare_csv: %s" % message)
|
||||
if not data:
|
||||
if not data_b:
|
||||
raise ScoValueError("apo_compare_csv: no data")
|
||||
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING)
|
||||
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
|
||||
return apo_data
|
||||
|
||||
|
|
|
@ -95,30 +95,21 @@ from flask import send_file
|
|||
# Pour la détection auto de l'encodage des fichiers Apogée:
|
||||
from chardet import detect as chardet_detect
|
||||
|
||||
from app.models.config import ScoDocSiteConfig
|
||||
import app.scodoc.sco_utils as scu
|
||||
import app.scodoc.notesdb as ndb
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
from app.scodoc.sco_codes_parcours import code_semestre_validant
|
||||
from app.scodoc.sco_codes_parcours import (
|
||||
ADC,
|
||||
ADJ,
|
||||
ADM,
|
||||
AJ,
|
||||
ATB,
|
||||
ATJ,
|
||||
ATT,
|
||||
CMP,
|
||||
DEF,
|
||||
DEM,
|
||||
NAR,
|
||||
RAT,
|
||||
)
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_formsemestre
|
||||
from app.scodoc import sco_formsemestre_status
|
||||
from app.scodoc import sco_parcours_dut
|
||||
from app.scodoc import sco_etud
|
||||
|
||||
|
@ -132,28 +123,10 @@ APO_SEP = "\t"
|
|||
APO_NEWLINE = "\r\n"
|
||||
|
||||
|
||||
def code_scodoc_to_apo(code):
|
||||
"""Conversion code jury ScoDoc en code Apogée"""
|
||||
return {
|
||||
ATT: "AJAC",
|
||||
ATB: "AJAC",
|
||||
ATJ: "AJAC",
|
||||
ADM: "ADM",
|
||||
ADJ: "ADM",
|
||||
ADC: "ADMC",
|
||||
AJ: "AJ",
|
||||
CMP: "COMP",
|
||||
"DEM": "NAR",
|
||||
DEF: "NAR",
|
||||
NAR: "NAR",
|
||||
RAT: "ATT",
|
||||
}.get(code, "DEF")
|
||||
|
||||
|
||||
def _apo_fmt_note(note):
|
||||
"Formatte une note pour Apogée (séparateur décimal: ',')"
|
||||
if not note and isinstance(note, float):
|
||||
return ""
|
||||
# if not note and isinstance(note, float): changé le 31/1/2022, étrange ?
|
||||
# return ""
|
||||
try:
|
||||
val = float(note)
|
||||
except ValueError:
|
||||
|
@ -173,8 +146,10 @@ def guess_data_encoding(text, threshold=0.6):
|
|||
|
||||
|
||||
def fix_data_encoding(
|
||||
text, default_source_encoding=APO_INPUT_ENCODING, dest_encoding=APO_INPUT_ENCODING
|
||||
):
|
||||
text: bytes,
|
||||
default_source_encoding=APO_INPUT_ENCODING,
|
||||
dest_encoding=APO_INPUT_ENCODING,
|
||||
) -> bytes:
|
||||
"""Try to ensure that text is using dest_encoding
|
||||
returns converted text, and a message describing the conversion.
|
||||
"""
|
||||
|
@ -200,7 +175,7 @@ def fix_data_encoding(
|
|||
|
||||
|
||||
class StringIOFileLineWrapper(object):
|
||||
def __init__(self, data):
|
||||
def __init__(self, data: str):
|
||||
self.f = io.StringIO(data)
|
||||
self.lineno = 0
|
||||
|
||||
|
@ -447,7 +422,7 @@ class ApoEtud(dict):
|
|||
N=_apo_fmt_note(ue_status["moy"]),
|
||||
B=20,
|
||||
J="",
|
||||
R=code_scodoc_to_apo(code_decision_ue),
|
||||
R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
|
||||
M="",
|
||||
)
|
||||
else:
|
||||
|
@ -473,13 +448,9 @@ class ApoEtud(dict):
|
|||
def comp_elt_semestre(self, nt, decision, etudid):
|
||||
"""Calcul résultat apo semestre"""
|
||||
# resultat du semestre
|
||||
decision_apo = code_scodoc_to_apo(decision["code"])
|
||||
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
|
||||
note = nt.get_etud_moy_gen(etudid)
|
||||
if (
|
||||
decision_apo == "DEF"
|
||||
or decision["code"] == "DEM"
|
||||
or decision["code"] == DEF
|
||||
):
|
||||
if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF:
|
||||
note_str = "0,01" # note non nulle pour les démissionnaires
|
||||
else:
|
||||
note_str = _apo_fmt_note(note)
|
||||
|
@ -518,21 +489,21 @@ class ApoEtud(dict):
|
|||
# ou jury intermediaire et etudiant non redoublant...
|
||||
return self.comp_elt_semestre(cur_nt, cur_decision, etudid)
|
||||
|
||||
decision_apo = code_scodoc_to_apo(cur_decision["code"])
|
||||
decision_apo = ScoDocSiteConfig.get_code_apo(cur_decision["code"])
|
||||
|
||||
autre_nt = sco_cache.NotesTableCache.get(autre_sem["formsemestre_id"])
|
||||
autre_decision = autre_nt.get_etud_decision_sem(etudid)
|
||||
if not autre_decision:
|
||||
# pas de decision dans l'autre => pas de résultat annuel
|
||||
return VOID_APO_RES
|
||||
autre_decision_apo = code_scodoc_to_apo(autre_decision["code"])
|
||||
autre_decision_apo = ScoDocSiteConfig.get_code_apo(autre_decision["code"])
|
||||
if (
|
||||
autre_decision_apo == "DEF"
|
||||
or autre_decision["code"] == "DEM"
|
||||
or autre_decision["code"] == DEM
|
||||
or autre_decision["code"] == DEF
|
||||
) or (
|
||||
decision_apo == "DEF"
|
||||
or cur_decision["code"] == "DEM"
|
||||
or cur_decision["code"] == DEM
|
||||
or cur_decision["code"] == DEF
|
||||
):
|
||||
note_str = "0,01" # note non nulle pour les démissionnaires
|
||||
|
@ -655,7 +626,7 @@ class ApoEtud(dict):
|
|||
class ApoData(object):
|
||||
def __init__(
|
||||
self,
|
||||
data,
|
||||
data: str,
|
||||
periode=None,
|
||||
export_res_etape=True,
|
||||
export_res_sem=True,
|
||||
|
@ -693,7 +664,7 @@ class ApoData(object):
|
|||
"<h3>Erreur lecture du fichier Apogée <tt>%s</tt></h3><p>" % filename
|
||||
+ e.args[0]
|
||||
+ "</p>"
|
||||
)
|
||||
) from e
|
||||
self.etape_apogee = self.get_etape_apogee() # 'V1RT'
|
||||
self.vdi_apogee = self.get_vdi_apogee() # '111'
|
||||
self.etape = ApoEtapeVDI(etape=self.etape_apogee, vdi=self.vdi_apogee)
|
||||
|
@ -760,7 +731,6 @@ class ApoData(object):
|
|||
def read_csv(self, data: str):
|
||||
if not data:
|
||||
raise ScoFormatError("Fichier Apogée vide !")
|
||||
|
||||
f = StringIOFileLineWrapper(data) # pour traiter comme un fichier
|
||||
# check that we are at the begining of Apogee CSV
|
||||
line = f.readline().strip()
|
||||
|
@ -768,7 +738,10 @@ class ApoData(object):
|
|||
raise ScoFormatError("format incorrect: pas de XX-APO_TITRES-XX")
|
||||
|
||||
# 1-- En-tête: du début jusqu'à la balise XX-APO_VALEURS-XX
|
||||
idx = data.index("XX-APO_VALEURS-XX")
|
||||
try:
|
||||
idx = data.index("XX-APO_VALEURS-XX")
|
||||
except ValueError as exc:
|
||||
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX") from exc
|
||||
self.header = data[:idx]
|
||||
|
||||
# 2-- Titres:
|
||||
|
@ -1178,7 +1151,7 @@ def nar_etuds_table(apo_data, NAR_Etuds):
|
|||
|
||||
|
||||
def export_csv_to_apogee(
|
||||
apo_csv_data,
|
||||
apo_csv_data: str,
|
||||
periode=None,
|
||||
dest_zip=None,
|
||||
export_res_etape=True,
|
||||
|
|
|
@ -98,9 +98,9 @@ def formsemestre_bulletinetud_published_dict(
|
|||
d = {}
|
||||
|
||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||
published = 1
|
||||
published = True
|
||||
else:
|
||||
published = 0
|
||||
published = False
|
||||
if xml_nodate:
|
||||
docdate = ""
|
||||
else:
|
||||
|
|
|
@ -93,9 +93,9 @@ def make_xml_formsemestre_bulletinetud(
|
|||
)
|
||||
|
||||
if (not sem["bul_hide_xml"]) or force_publishing:
|
||||
published = "1"
|
||||
published = 1
|
||||
else:
|
||||
published = "0"
|
||||
published = 0
|
||||
if xml_nodate:
|
||||
docdate = ""
|
||||
else:
|
||||
|
@ -105,7 +105,7 @@ def make_xml_formsemestre_bulletinetud(
|
|||
"etudid": str(etudid),
|
||||
"formsemestre_id": str(formsemestre_id),
|
||||
"date": docdate,
|
||||
"publie": published,
|
||||
"publie": str(published),
|
||||
}
|
||||
if sem["etapes"]:
|
||||
el["etape_apo"] = str(sem["etapes"][0]) or ""
|
||||
|
@ -141,7 +141,9 @@ def make_xml_formsemestre_bulletinetud(
|
|||
|
||||
# Disponible pour publication ?
|
||||
if not published:
|
||||
return doc # stop !
|
||||
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
|
||||
scu.SCO_ENCODING
|
||||
) # stop !
|
||||
|
||||
# Groupes:
|
||||
partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False)
|
||||
|
|
|
@ -125,6 +125,7 @@ CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis)
|
|||
NAR = "NAR"
|
||||
RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée
|
||||
DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission)
|
||||
DEM = "DEM"
|
||||
|
||||
# codes actions
|
||||
REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1)
|
||||
|
@ -140,22 +141,26 @@ BUG = "BUG"
|
|||
|
||||
ALL = "ALL"
|
||||
|
||||
# Explication des codes (de demestre ou d'UE)
|
||||
CODES_EXPL = {
|
||||
ADM: "Validé",
|
||||
ADC: "Validé par compensation",
|
||||
ADJ: "Validé par le Jury",
|
||||
ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)",
|
||||
ADM: "Validé",
|
||||
AJ: "Ajourné",
|
||||
ATB: "Décision en attente d'un autre semestre (au moins une UE sous la barre)",
|
||||
ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
|
||||
AJ: "Ajourné",
|
||||
NAR: "Echec, non autorisé à redoubler",
|
||||
RAT: "En attente d'un rattrapage",
|
||||
ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)",
|
||||
CMP: "Code UE acquise car semestre acquis",
|
||||
DEF: "Défaillant",
|
||||
NAR: "Échec, non autorisé à redoubler",
|
||||
RAT: "En attente d'un rattrapage",
|
||||
}
|
||||
# Nota: ces explications sont personnalisables via le fichier
|
||||
# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py
|
||||
# variable: CONFIG.CODES_EXP
|
||||
|
||||
# Les codes de semestres:
|
||||
CODES_JURY_SEM = {ADC, ADJ, ADM, AJ, ATB, ATJ, ATT, DEF, NAR, RAT}
|
||||
CODES_SEM_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
|
||||
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente
|
||||
|
||||
|
|
|
@ -191,6 +191,7 @@ def compute_user_formula(
|
|||
return user_moy
|
||||
|
||||
|
||||
# XXX OBSOLETE
|
||||
def compute_moduleimpl_moyennes(nt, modimpl):
|
||||
"""Retourne dict { etudid : note_moyenne } pour tous les etuds inscrits
|
||||
au moduleimpl mod, la liste des evaluations "valides" (toutes notes entrées
|
||||
|
@ -228,22 +229,23 @@ def compute_moduleimpl_moyennes(nt, modimpl):
|
|||
|
||||
user_expr = moduleimpl_has_expression(modimpl)
|
||||
attente = False
|
||||
# recupere les notes de toutes les evaluations
|
||||
# récupere les notes de toutes les evaluations
|
||||
eval_rattr = None
|
||||
for e in evals:
|
||||
e["nb_inscrits"] = e["etat"]["nb_inscrits"]
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
# XXX OBSOLETE
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
e["evaluation_id"]
|
||||
) # toutes, y compris demissions
|
||||
# restreint aux étudiants encore inscrits à ce module
|
||||
notes = [
|
||||
NotesDB[etudid]["value"] for etudid in NotesDB if (etudid in insmod_set)
|
||||
notes_db[etudid]["value"] for etudid in notes_db if (etudid in insmod_set)
|
||||
]
|
||||
e["nb_notes"] = len(notes)
|
||||
e["nb_abs"] = len([x for x in notes if x is None])
|
||||
e["nb_neutre"] = len([x for x in notes if x == NOTES_NEUTRALISE])
|
||||
e["nb_att"] = len([x for x in notes if x == NOTES_ATTENTE])
|
||||
e["notes"] = NotesDB
|
||||
e["notes"] = notes_db
|
||||
|
||||
if e["etat"]["evalattente"]:
|
||||
attente = True
|
||||
|
|
|
@ -62,7 +62,8 @@ def html_edit_formation_apc(
|
|||
else:
|
||||
semestre_ids = [semestre_idx]
|
||||
other_modules = formation.modules.filter(
|
||||
Module.module_type != ModuleType.SAE, Module.module_type != ModuleType.RESSOURCE
|
||||
Module.module_type.is_distinct_from(ModuleType.SAE),
|
||||
Module.module_type.is_distinct_from(ModuleType.RESSOURCE),
|
||||
).order_by(
|
||||
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
|
||||
)
|
||||
|
|
|
@ -304,9 +304,8 @@ def do_formation_edit(args):
|
|||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
sco_formations._formationEditor.edit(cnx, args)
|
||||
formation = Formation.query.get(args["formation_id"])
|
||||
formation: Formation = Formation.query.get(args["formation_id"])
|
||||
formation.invalidate_cached_sems()
|
||||
formation.force_semestre_modules_aux_ues()
|
||||
|
||||
|
||||
def module_move(module_id, after=0, redirect=True):
|
||||
|
|
|
@ -32,15 +32,16 @@ import flask
|
|||
from flask import url_for, render_template
|
||||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import log
|
||||
from app import models
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import Matiere, Module, UniteEns
|
||||
from app.models import Formation, Matiere, Module, UniteEns
|
||||
from app.models import FormSemestre, ModuleImpl
|
||||
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app import log
|
||||
from app import models
|
||||
from app.models import Formation
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import (
|
||||
|
@ -294,6 +295,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
|
|||
"title": "Code Apogée",
|
||||
"size": 25,
|
||||
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
|
||||
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
|
||||
},
|
||||
),
|
||||
(
|
||||
|
@ -472,16 +474,31 @@ def module_edit(module_id=None):
|
|||
formation_id = module["formation_id"]
|
||||
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
|
||||
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
|
||||
is_apc = parcours.APC_SAE
|
||||
ues_matieres = ndb.SimpleDictFetch(
|
||||
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
|
||||
FROM notes_matieres mat, notes_ue ue
|
||||
WHERE mat.ue_id = ue.id
|
||||
AND ue.formation_id = %(formation_id)s
|
||||
ORDER BY ue.numero, mat.numero
|
||||
""",
|
||||
{"formation_id": formation_id},
|
||||
)
|
||||
is_apc = parcours.APC_SAE # BUT
|
||||
in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls
|
||||
if in_use:
|
||||
# matières du même semestre seulement
|
||||
ues_matieres = ndb.SimpleDictFetch(
|
||||
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
|
||||
FROM notes_matieres mat, notes_ue ue
|
||||
WHERE mat.ue_id = ue.id
|
||||
AND ue.formation_id = %(formation_id)s
|
||||
AND ue.semestre_idx = %(semestre_idx)s
|
||||
ORDER BY ue.numero, mat.numero
|
||||
""",
|
||||
{"formation_id": formation_id, "semestre_idx": a_module.ue.semestre_idx},
|
||||
)
|
||||
else:
|
||||
# matières de la formation
|
||||
ues_matieres = ndb.SimpleDictFetch(
|
||||
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
|
||||
FROM notes_matieres mat, notes_ue ue
|
||||
WHERE mat.ue_id = ue.id
|
||||
AND ue.formation_id = %(formation_id)s
|
||||
ORDER BY ue.numero, mat.numero
|
||||
""",
|
||||
{"formation_id": formation_id},
|
||||
)
|
||||
mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres]
|
||||
ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres]
|
||||
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
|
||||
|
@ -500,12 +517,25 @@ def module_edit(module_id=None):
|
|||
),
|
||||
"""<h2>Modification du module %(titre)s""" % module,
|
||||
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
|
||||
render_template("scodoc/help/modules.html", is_apc=is_apc),
|
||||
render_template(
|
||||
"scodoc/help/modules.html",
|
||||
is_apc=is_apc,
|
||||
formsemestres=FormSemestre.query.filter(
|
||||
ModuleImpl.formsemestre_id == FormSemestre.id,
|
||||
ModuleImpl.module_id == module_id,
|
||||
).all(),
|
||||
),
|
||||
]
|
||||
if not unlocked:
|
||||
H.append(
|
||||
"""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>"""
|
||||
)
|
||||
if in_use:
|
||||
H.append(
|
||||
"""<div class="ue_warning"><span>Module déjà utilisé dans des semestres,
|
||||
soyez prudents !
|
||||
</span></div>"""
|
||||
)
|
||||
|
||||
descr = [
|
||||
(
|
||||
|
@ -678,6 +708,13 @@ def module_edit(module_id=None):
|
|||
else:
|
||||
# l'UE peut changer
|
||||
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
|
||||
old_ue_id = a_module.ue.id
|
||||
new_ue_id = int(tf[2]["ue_id"])
|
||||
if (old_ue_id != new_ue_id) and in_use:
|
||||
# pas changer de semestre un module utilisé !
|
||||
raise ScoValueError(
|
||||
"Module utilisé: il ne peut pas être changé de semestre !"
|
||||
)
|
||||
# En APC, force le semestre égal à celui de l'UE
|
||||
if is_apc:
|
||||
selected_ue = UniteEns.query.get(tf[2]["ue_id"])
|
||||
|
|
|
@ -33,13 +33,15 @@ from flask import url_for, render_template
|
|||
from flask import g, request
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app import log
|
||||
from app.models import APO_CODE_STR_LEN
|
||||
from app.models import Formation, UniteEns, ModuleImpl, Module
|
||||
from app.models.formations import Matiere
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_utils import ModuleType
|
||||
from app import log
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
|
||||
from app.scodoc.TrivialFormulator import TrivialFormulator
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.scodoc.sco_exceptions import (
|
||||
|
@ -470,7 +472,13 @@ def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
|
|||
if ue.modules.all():
|
||||
raise ScoValueError(
|
||||
f"""Suppression de l'UE {ue.titre} impossible car
|
||||
des modules (ou SAÉ ou ressources) lui sont rattachés."""
|
||||
des modules (ou SAÉ ou ressources) lui sont rattachés.""",
|
||||
dest_url=url_for(
|
||||
"notes.ue_table",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formation_id=ue.formation.id,
|
||||
semestre_idx=ue.semestre_idx,
|
||||
),
|
||||
)
|
||||
if not can_delete_ue(ue):
|
||||
raise ScoNonEmptyFormationObject(
|
||||
|
@ -504,10 +512,9 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
|||
"""Liste des matières et modules d'une formation, avec liens pour
|
||||
éditer (si non verrouillée).
|
||||
"""
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_formsemestre_validation
|
||||
|
||||
formation = Formation.query.get(formation_id)
|
||||
formation: Formation = Formation.query.get(formation_id)
|
||||
if not formation:
|
||||
raise ScoValueError("invalid formation_id")
|
||||
parcours = formation.get_parcours()
|
||||
|
@ -528,6 +535,12 @@ def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
|
|||
# pour faciliter la transition des anciens programmes non APC
|
||||
for ue in ues_obj:
|
||||
ue.guess_semestre_idx()
|
||||
# vérifie qu'on a bien au moins une matière dans chaque UE
|
||||
for ue in ues_obj:
|
||||
if ue.matieres.count() < 1:
|
||||
mat = Matiere(ue_id=ue.id)
|
||||
db.session.add(mat)
|
||||
db.session.commit()
|
||||
ues = [ue.to_dict() for ue in ues_obj]
|
||||
ues_externes = [ue.to_dict() for ue in ues_externes_obj]
|
||||
|
||||
|
@ -1205,14 +1218,14 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
|
|||
if not dont_invalidate_cache:
|
||||
# Invalide les semestres utilisant cette formation:
|
||||
formation.invalidate_cached_sems()
|
||||
formation.force_semestre_modules_aux_ues()
|
||||
|
||||
|
||||
# essai edition en ligne:
|
||||
def edit_ue_set_code_apogee(id=None, value=None):
|
||||
"set UE code apogee"
|
||||
ue_id = id
|
||||
value = value.strip("-_ \t")
|
||||
value = value.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
|
||||
|
||||
log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))
|
||||
|
||||
ues = ue_list(args={"ue_id": ue_id})
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
apo_csv_get()
|
||||
|
||||
API:
|
||||
apo_csv_store( annee_scolaire, sem_id)
|
||||
# apo_csv_store(csv_data, annee_scolaire, sem_id)
|
||||
store maq file (archive)
|
||||
|
||||
apo_csv_get(etape_apo, annee_scolaire, sem_id, vdi_apo=None)
|
||||
|
@ -101,7 +101,7 @@ ApoCSVArchive = ApoCSVArchiver()
|
|||
|
||||
def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
|
||||
"""
|
||||
csv_data: maquette content (string)
|
||||
csv_data: maquette content (str))
|
||||
annee_scolaire: int (2016)
|
||||
sem_id: 0 (année ?), 1 (premier semestre de l'année) ou 2 (deuxième semestre)
|
||||
:return: etape_apo du fichier CSV stocké
|
||||
|
@ -378,7 +378,7 @@ e.associate_sco( apo_data)
|
|||
print apo_csv_list_stored_archives()
|
||||
|
||||
|
||||
apo_csv_store(csv_data, annee_scolaire, sem_id)
|
||||
# apo_csv_store(csv_data, annee_scolaire, sem_id)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ from app.scodoc import sco_preferences
|
|||
from app.scodoc import sco_semset
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app.scodoc.sco_apogee_csv import APO_PORTAL_ENCODING, APO_INPUT_ENCODING
|
||||
from app.scodoc.sco_apogee_csv import APO_INPUT_ENCODING, APO_OUTPUT_ENCODING
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
|
@ -585,7 +585,7 @@ def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"):
|
|||
return "\n".join(H) + html_sco_header.sco_footer()
|
||||
|
||||
|
||||
def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
|
||||
def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=False):
|
||||
"""Store CSV data
|
||||
Le semset identifie l'annee scolaire et le semestre
|
||||
Si csvfile, lit depuis FILE, sinon utilise data
|
||||
|
@ -593,9 +593,8 @@ def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
|
|||
if not semset_id:
|
||||
raise ValueError("invalid null semset_id")
|
||||
semset = sco_semset.SemSet(semset_id=semset_id)
|
||||
|
||||
if csvfile:
|
||||
data = csvfile.read()
|
||||
data = csvfile.read() # bytes
|
||||
if autodetect:
|
||||
# check encoding (although documentation states that users SHOULD upload LATIN1)
|
||||
data, message = sco_apogee_csv.fix_data_encoding(data)
|
||||
|
@ -605,19 +604,26 @@ def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
|
|||
log("view_apo_csv_store: autodetection of encoding disabled by user")
|
||||
if not data:
|
||||
raise ScoValueError("view_apo_csv_store: no data")
|
||||
|
||||
# data est du bytes, encodé en APO_INPUT_ENCODING
|
||||
data_str = data.decode(APO_INPUT_ENCODING)
|
||||
# check si etape maquette appartient bien au semset
|
||||
apo_data = sco_apogee_csv.ApoData(
|
||||
data, periode=semset["sem_id"]
|
||||
data_str, periode=semset["sem_id"]
|
||||
) # parse le fichier -> exceptions
|
||||
if apo_data.etape not in semset["etapes"]:
|
||||
raise ScoValueError(
|
||||
"Le code étape de ce fichier ne correspond pas à ceux de cet ensemble"
|
||||
)
|
||||
|
||||
sco_etape_apogee.apo_csv_store(data, semset["annee_scolaire"], semset["sem_id"])
|
||||
sco_etape_apogee.apo_csv_store(data_str, semset["annee_scolaire"], semset["sem_id"])
|
||||
|
||||
return flask.redirect("apo_semset_maq_status?semset_id=" + semset_id)
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"notes.apo_semset_maq_status",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
semset_id=semset_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def view_apo_csv_download_and_store(etape_apo="", semset_id=""):
|
||||
|
@ -629,9 +635,9 @@ def view_apo_csv_download_and_store(etape_apo="", semset_id=""):
|
|||
data = sco_portal_apogee.get_maquette_apogee(
|
||||
etape=etape_apo, annee_scolaire=semset["annee_scolaire"]
|
||||
)
|
||||
# here, data is utf8
|
||||
# here, data is str
|
||||
# but we store and generate latin1 files, to ease further import in Apogée
|
||||
data = data.decode(APO_PORTAL_ENCODING).encode(APO_INPUT_ENCODING) # XXX #py3
|
||||
data = data.encode(APO_OUTPUT_ENCODING)
|
||||
return view_apo_csv_store(semset_id, data=data, autodetect=False)
|
||||
|
||||
|
||||
|
@ -669,7 +675,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
|
|||
sem_id = semset["sem_id"]
|
||||
csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id)
|
||||
if format == "raw":
|
||||
scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
|
||||
return scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
|
||||
|
||||
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])
|
||||
|
||||
|
|
|
@ -85,7 +85,7 @@ def evaluation_check_absences(evaluation_id):
|
|||
Justs = set([x["etudid"] for x in Just]) # ensemble des etudiants avec justif
|
||||
|
||||
# Les notes:
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
ValButAbs = [] # une note mais noté absent
|
||||
AbsNonSignalee = [] # note ABS mais pas noté absent
|
||||
ExcNonSignalee = [] # note EXC mais pas noté absent
|
||||
|
@ -94,8 +94,8 @@ def evaluation_check_absences(evaluation_id):
|
|||
for etudid, _ in sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True
|
||||
):
|
||||
if etudid in NotesDB:
|
||||
val = NotesDB[etudid]["value"]
|
||||
if etudid in notes_db:
|
||||
val = notes_db[etudid]["value"]
|
||||
if (
|
||||
val != None and val != scu.NOTES_NEUTRALISE and val != scu.NOTES_ATTENTE
|
||||
) and etudid in As:
|
||||
|
|
|
@ -185,7 +185,8 @@ def _check_evaluation_args(args):
|
|||
if (jour > date_fin) or (jour < date_debut):
|
||||
raise ScoValueError(
|
||||
"La date de l'évaluation (%s/%s/%s) n'est pas dans le semestre !"
|
||||
% (d, m, y)
|
||||
% (d, m, y),
|
||||
dest_url="javascript:history.back();",
|
||||
)
|
||||
heure_debut = args.get("heure_debut", None)
|
||||
args["heure_debut"] = heure_debut
|
||||
|
@ -306,8 +307,8 @@ def do_evaluation_delete(evaluation_id):
|
|||
raise AccessDenied(
|
||||
"Modification évaluation impossible pour %s" % current_user.get_nomplogin()
|
||||
)
|
||||
NotesDB = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
|
||||
notes = [x["value"] for x in NotesDB.values()]
|
||||
notes_db = do_evaluation_get_all_notes(evaluation_id) # { etudid : value }
|
||||
notes = [x["value"] for x in notes_db.values()]
|
||||
if notes:
|
||||
raise ScoValueError(
|
||||
"Impossible de supprimer cette évaluation: il reste des notes"
|
||||
|
|
|
@ -143,6 +143,7 @@ def evaluation_create_form(
|
|||
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
|
||||
vals["visibulletinlist"] = []
|
||||
#
|
||||
ue_coef_dict = {}
|
||||
if is_apc: # BUT: poids vers les UE
|
||||
ue_coef_dict = ModuleImpl.query.get(moduleimpl_id).module.get_ue_coef_dict()
|
||||
for ue in sem_ues:
|
||||
|
@ -170,7 +171,7 @@ def evaluation_create_form(
|
|||
(
|
||||
"jour",
|
||||
{
|
||||
"input_type": "date",
|
||||
"input_type": "datedmy",
|
||||
"title": "Date",
|
||||
"size": 12,
|
||||
"explanation": "date de l'examen, devoir ou contrôle",
|
||||
|
@ -290,7 +291,10 @@ def evaluation_create_form(
|
|||
"title": f"Poids {ue.acronyme}",
|
||||
"size": 2,
|
||||
"type": "float",
|
||||
"explanation": f"{ue.titre}",
|
||||
"explanation": f"""
|
||||
<span class="eval_coef_ue" title="coef. du module dans cette UE">{ue_coef_dict.get(ue.id, 0.)}</span>
|
||||
<span class="eval_coef_ue_titre">{ue.titre}</span>
|
||||
""",
|
||||
"allow_null": False,
|
||||
},
|
||||
),
|
||||
|
|
|
@ -36,8 +36,19 @@ class ScoException(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class NoteProcessError(ScoException):
|
||||
"misc errors in process"
|
||||
class InvalidNoteValue(ScoException):
|
||||
pass
|
||||
|
||||
|
||||
# Exception qui stoque dest_url
|
||||
class ScoValueError(ScoException):
|
||||
def __init__(self, msg, dest_url=None):
|
||||
super().__init__(msg)
|
||||
self.dest_url = dest_url
|
||||
|
||||
|
||||
class NoteProcessError(ScoValueError):
|
||||
"Valeurs notes invalides"
|
||||
pass
|
||||
|
||||
|
||||
|
@ -45,21 +56,25 @@ class InvalidEtudId(NoteProcessError):
|
|||
pass
|
||||
|
||||
|
||||
class InvalidNoteValue(ScoException):
|
||||
pass
|
||||
|
||||
|
||||
# Exception qui stoque dest_url, utilisee dans Zope standard_error_message
|
||||
class ScoValueError(ScoException):
|
||||
def __init__(self, msg, dest_url=None):
|
||||
super().__init__(msg)
|
||||
self.dest_url = dest_url
|
||||
|
||||
|
||||
class ScoFormatError(ScoValueError):
|
||||
pass
|
||||
|
||||
|
||||
class ScoPDFFormatError(ScoValueError):
|
||||
"erreur génération PDF (templates platypus, ...)"
|
||||
|
||||
def __init__(self, msg, dest_url=None):
|
||||
super().__init__(
|
||||
f"""Erreur dans un format pdf:
|
||||
<p>{msg}</p>
|
||||
<p>Vérifiez les paramètres (polices de caractères, balisage)
|
||||
dans les paramètres ou préférences.
|
||||
</p>
|
||||
""",
|
||||
dest_url=dest_url,
|
||||
)
|
||||
|
||||
|
||||
class ScoInvalidDept(ScoValueError):
|
||||
"""departement invalide"""
|
||||
|
||||
|
@ -96,6 +111,28 @@ class ScoNonEmptyFormationObject(ScoValueError):
|
|||
super().__init__(msg=msg, dest_url=dest_url)
|
||||
|
||||
|
||||
class ScoInvalidIdType(ScoValueError):
|
||||
"""Pour les clients qui s'obstinnent à utiliser des bookmarks ou
|
||||
historiques anciens avec des ID ScoDoc7"""
|
||||
|
||||
def __init__(self, msg=""):
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
||||
msg = f"""<h3>Adresse de page invalide</h3>
|
||||
<p class="help">
|
||||
Vous utilisez un lien invalide, qui correspond probablement
|
||||
à une ancienne version du logiciel. <br>
|
||||
Au besoin, mettre à jour vos marque-pages.
|
||||
</p>
|
||||
<p> Si le problème persiste, merci de contacter l'assistance
|
||||
via la liste de diffusion <a href="{scu.SCO_USERS_LIST}">Notes</a>
|
||||
ou le salon Discord.
|
||||
</p>
|
||||
<p>Message serveur: <tt>{msg}</tt></p>
|
||||
"""
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class ScoGenError(ScoException):
|
||||
"exception avec affichage d'une page explicative ad-hoc"
|
||||
|
||||
|
|
|
@ -27,21 +27,22 @@
|
|||
|
||||
"""Operations de base sur les formsemestres
|
||||
"""
|
||||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
import time
|
||||
from operator import itemgetter
|
||||
import time
|
||||
|
||||
from flask import g, request
|
||||
|
||||
import app
|
||||
from app import log
|
||||
from app.models import Departement
|
||||
|
||||
from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_cache
|
||||
from app.scodoc import sco_formations
|
||||
from app.scodoc import sco_preferences
|
||||
from app.scodoc.gen_tables import GenTable
|
||||
from app import log
|
||||
from app.scodoc.sco_codes_parcours import NO_SEMESTRE_ID
|
||||
from app.scodoc.sco_exceptions import ScoValueError, ScoInvalidIdType
|
||||
from app.scodoc.sco_vdi import ApoEtapeVDI
|
||||
import app.scodoc.notesdb as ndb
|
||||
import app.scodoc.sco_utils as scu
|
||||
|
@ -97,7 +98,7 @@ def get_formsemestre(formsemestre_id, raise_soft_exc=False):
|
|||
if formsemestre_id in g.stored_get_formsemestre:
|
||||
return g.stored_get_formsemestre[formsemestre_id]
|
||||
if not isinstance(formsemestre_id, int):
|
||||
raise ValueError("formsemestre_id must be an integer !")
|
||||
raise ScoInvalidIdType("formsemestre_id must be an integer !")
|
||||
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
|
||||
if not sems:
|
||||
log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id)
|
||||
|
|
|
@ -254,7 +254,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||
"date_debut",
|
||||
{
|
||||
"title": "Date de début", # j/m/a
|
||||
"input_type": "date",
|
||||
"input_type": "datedmy",
|
||||
"explanation": "j/m/a",
|
||||
"size": 9,
|
||||
"allow_null": False,
|
||||
|
@ -264,7 +264,7 @@ def do_formsemestre_createwithmodules(edit=False):
|
|||
"date_fin",
|
||||
{
|
||||
"title": "Date de fin", # j/m/a
|
||||
"input_type": "date",
|
||||
"input_type": "datedmy",
|
||||
"explanation": "j/m/a",
|
||||
"size": 9,
|
||||
"allow_null": False,
|
||||
|
@ -914,7 +914,7 @@ def formsemestre_clone(formsemestre_id):
|
|||
"date_debut",
|
||||
{
|
||||
"title": "Date de début", # j/m/a
|
||||
"input_type": "date",
|
||||
"input_type": "datedmy",
|
||||
"explanation": "j/m/a",
|
||||
"size": 9,
|
||||
"allow_null": False,
|
||||
|
@ -924,7 +924,7 @@ def formsemestre_clone(formsemestre_id):
|
|||
"date_fin",
|
||||
{
|
||||
"title": "Date de fin", # j/m/a
|
||||
"input_type": "date",
|
||||
"input_type": "datedmy",
|
||||
"explanation": "j/m/a",
|
||||
"size": 9,
|
||||
"allow_null": False,
|
||||
|
|
|
@ -154,7 +154,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
|
|||
"date_debut",
|
||||
{
|
||||
"title": "Date de début", # j/m/a
|
||||
"input_type": "date",
|
||||
"input_type": "datedmy",
|
||||
"explanation": "j/m/a (peut être approximatif)",
|
||||
"size": 9,
|
||||
"allow_null": False,
|
||||
|
@ -164,7 +164,7 @@ def formsemestre_ext_create_form(etudid, formsemestre_id):
|
|||
"date_fin",
|
||||
{
|
||||
"title": "Date de fin", # j/m/a
|
||||
"input_type": "date",
|
||||
"input_type": "datedmy",
|
||||
"explanation": "j/m/a (peut être approximatif)",
|
||||
"size": 9,
|
||||
"allow_null": False,
|
||||
|
|
|
@ -105,10 +105,10 @@ def _build_menu_stats(formsemestre_id):
|
|||
"enabled": True,
|
||||
},
|
||||
{
|
||||
"title": "Documents Avis Poursuite Etudes",
|
||||
"title": "Documents Avis Poursuite Etudes (xp)",
|
||||
"endpoint": "notes.pe_view_sem_recap",
|
||||
"args": {"formsemestre_id": formsemestre_id},
|
||||
"enabled": current_app.config["TESTING"] or current_app.config["DEBUG"],
|
||||
"enabled": True, # current_app.config["TESTING"] or current_app.config["DEBUG"],
|
||||
},
|
||||
{
|
||||
"title": 'Table "débouchés"',
|
||||
|
|
|
@ -738,7 +738,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
|
|||
)
|
||||
|
||||
# Choix code semestre:
|
||||
codes = list(sco_codes_parcours.CODES_EXPL.keys())
|
||||
codes = list(sco_codes_parcours.CODES_JURY_SEM)
|
||||
codes.sort() # fortuitement, cet ordre convient bien !
|
||||
|
||||
H.append(
|
||||
|
|
|
@ -87,7 +87,7 @@ groupEditor = ndb.EditableTable(
|
|||
group_list = groupEditor.list
|
||||
|
||||
|
||||
def get_group(group_id):
|
||||
def get_group(group_id: int):
|
||||
"""Returns group object, with partition"""
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
|
||||
|
@ -284,7 +284,9 @@ def get_group_infos(group_id, etat=None): # was _getlisteetud
|
|||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
group = get_group(group_id)
|
||||
sem = sco_formsemestre.get_formsemestre(group["formsemestre_id"])
|
||||
sem = sco_formsemestre.get_formsemestre(
|
||||
group["formsemestre_id"], raise_soft_exc=True
|
||||
)
|
||||
|
||||
members = get_group_members(group_id, etat=etat)
|
||||
# add human readable description of state:
|
||||
|
@ -685,6 +687,11 @@ def setGroups(
|
|||
group_id = fs[0].strip()
|
||||
if not group_id:
|
||||
continue
|
||||
try:
|
||||
group_id = int(group_id)
|
||||
except ValueError as exc:
|
||||
log("setGroups: ignoring invalid group_id={group_id}")
|
||||
continue
|
||||
group = get_group(group_id)
|
||||
# Anciens membres du groupe:
|
||||
old_members = get_group_members(group_id)
|
||||
|
|
|
@ -815,7 +815,7 @@ def tab_absences_html(groups_infos, etat=None):
|
|||
% (groups_infos.base_url, groups_infos.groups_titles),
|
||||
"""<li><a class="stdlink" href="trombino?%s&format=pdf">Trombinoscope en PDF</a></li>"""
|
||||
% groups_infos.groups_query_args,
|
||||
"""<li><a class="stdlink" href="pdf_trombino_tours?%s&format=pdf">Trombinoscope en PDF (format "IUT de Tours", beta)</a></li>"""
|
||||
"""<li><a class="stdlink" href="pdf_trombino_tours?%s&format=pdf">Trombinoscope en PDF (format "IUT de Tours")</a></li>"""
|
||||
% groups_infos.groups_query_args,
|
||||
"""<li><a class="stdlink" href="pdf_feuille_releve_absences?%s&format=pdf">Feuille relevé absences hebdomadaire (beta)</a></li>"""
|
||||
% groups_infos.groups_query_args,
|
||||
|
|
|
@ -550,9 +550,9 @@ def _import_one_student(
|
|||
formsemestre_id = values["codesemestre"]
|
||||
try:
|
||||
formsemestre_id = int(formsemestre_id)
|
||||
except ValueError as exc:
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise ScoValueError(
|
||||
f"valeur invalide dans la colonne codesemestre, ligne {linenum+1}"
|
||||
f"valeur invalide ou manquante dans la colonne codesemestre, ligne {linenum+1}"
|
||||
) from exc
|
||||
# recupere liste des groupes:
|
||||
if formsemestre_id not in GroupIdInferers:
|
||||
|
|
|
@ -49,9 +49,11 @@ from app.scodoc import sco_etud
|
|||
from app.scodoc.sco_exceptions import ScoValueError
|
||||
|
||||
|
||||
def list_authorized_etuds_by_sem(sem, delai=274):
|
||||
def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
|
||||
"""Liste des etudiants autorisés à s'inscrire dans sem.
|
||||
delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible.
|
||||
ignore_jury: si vrai, considère tous les étudiants comem autorisés, même
|
||||
s'ils n'ont pas de décision de jury.
|
||||
"""
|
||||
src_sems = list_source_sems(sem, delai=delai)
|
||||
inscrits = list_inscrits(sem["formsemestre_id"])
|
||||
|
@ -59,7 +61,12 @@ def list_authorized_etuds_by_sem(sem, delai=274):
|
|||
candidats = {} # etudid : etud (tous les etudiants candidats)
|
||||
nb = 0 # debug
|
||||
for src in src_sems:
|
||||
liste = list_etuds_from_sem(src, sem)
|
||||
if ignore_jury:
|
||||
# liste de tous les inscrits au semestre (sans dems)
|
||||
liste = list_inscrits(src["formsemestre_id"]).values()
|
||||
else:
|
||||
# liste des étudiants autorisés par le jury à s'inscrire ici
|
||||
liste = list_etuds_from_sem(src, sem)
|
||||
liste_filtree = []
|
||||
for e in liste:
|
||||
# Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src
|
||||
|
@ -125,7 +132,7 @@ def list_inscrits(formsemestre_id, with_dems=False):
|
|||
return inscr
|
||||
|
||||
|
||||
def list_etuds_from_sem(src, dst):
|
||||
def list_etuds_from_sem(src, dst) -> list[dict]:
|
||||
"""Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
|
||||
target = dst["semestre_id"]
|
||||
dpv = sco_pvjury.dict_pvjury(src["formsemestre_id"])
|
||||
|
@ -224,7 +231,7 @@ def do_desinscrit(sem, etudids):
|
|||
)
|
||||
|
||||
|
||||
def list_source_sems(sem, delai=None):
|
||||
def list_source_sems(sem, delai=None) -> list[dict]:
|
||||
"""Liste des semestres sources
|
||||
sem est le semestre destination
|
||||
"""
|
||||
|
@ -265,6 +272,7 @@ def formsemestre_inscr_passage(
|
|||
inscrit_groupes=False,
|
||||
submitted=False,
|
||||
dialog_confirmed=False,
|
||||
ignore_jury=False,
|
||||
):
|
||||
"""Form. pour inscription des etudiants d'un semestre dans un autre
|
||||
(donné par formsemestre_id).
|
||||
|
@ -280,6 +288,7 @@ def formsemestre_inscr_passage(
|
|||
|
||||
"""
|
||||
inscrit_groupes = int(inscrit_groupes)
|
||||
ignore_jury = int(ignore_jury)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
# -- check lock
|
||||
if not sem["etat"]:
|
||||
|
@ -292,8 +301,12 @@ def formsemestre_inscr_passage(
|
|||
etuds = [int(x) for x in etuds.split(",") if x]
|
||||
elif isinstance(etuds, int):
|
||||
etuds = [etuds]
|
||||
elif etuds and isinstance(etuds[0], str):
|
||||
etuds = [int(x) for x in etuds]
|
||||
|
||||
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem)
|
||||
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(
|
||||
sem, ignore_jury=ignore_jury
|
||||
)
|
||||
etuds_set = set(etuds)
|
||||
candidats_set = set(candidats)
|
||||
inscrits_set = set(inscrits)
|
||||
|
@ -321,6 +334,7 @@ def formsemestre_inscr_passage(
|
|||
candidats_non_inscrits,
|
||||
inscrits_ailleurs,
|
||||
inscrit_groupes=inscrit_groupes,
|
||||
ignore_jury=ignore_jury,
|
||||
)
|
||||
else:
|
||||
if not dialog_confirmed:
|
||||
|
@ -361,6 +375,7 @@ def formsemestre_inscr_passage(
|
|||
"formsemestre_id": formsemestre_id,
|
||||
"etuds": ",".join([str(x) for x in etuds]),
|
||||
"inscrit_groupes": inscrit_groupes,
|
||||
"ignore_jury": ignore_jury,
|
||||
"submitted": 1,
|
||||
},
|
||||
)
|
||||
|
@ -409,18 +424,23 @@ def build_page(
|
|||
candidats_non_inscrits,
|
||||
inscrits_ailleurs,
|
||||
inscrit_groupes=False,
|
||||
ignore_jury=False,
|
||||
):
|
||||
inscrit_groupes = int(inscrit_groupes)
|
||||
ignore_jury = int(ignore_jury)
|
||||
if inscrit_groupes:
|
||||
inscrit_groupes_checked = " checked"
|
||||
else:
|
||||
inscrit_groupes_checked = ""
|
||||
|
||||
if ignore_jury:
|
||||
ignore_jury_checked = " checked"
|
||||
else:
|
||||
ignore_jury_checked = ""
|
||||
H = [
|
||||
html_sco_header.html_sem_header(
|
||||
"Passages dans le semestre", sem, with_page_header=False
|
||||
),
|
||||
"""<form method="post" action="%s">""" % request.base_url,
|
||||
"""<form name="f" method="post" action="%s">""" % request.base_url,
|
||||
"""<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>
|
||||
<input type="submit" name="submitted" value="Appliquer les modifications"/>
|
||||
<a href="#help">aide</a>
|
||||
|
@ -428,6 +448,8 @@ def build_page(
|
|||
% sem, # "
|
||||
"""<input name="inscrit_groupes" type="checkbox" value="1" %s>inscrire aux mêmes groupes</input>"""
|
||||
% inscrit_groupes_checked,
|
||||
"""<input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()" %s>inclure tous les étudiants (même sans décision de jury)</input>"""
|
||||
% ignore_jury_checked,
|
||||
"""<div class="pas_recap">Actuellement <span id="nbinscrits">%s</span> inscrits
|
||||
et %d candidats supplémentaires
|
||||
</div>"""
|
||||
|
|
|
@ -201,6 +201,7 @@ def do_evaluation_listenotes(
|
|||
note_sur_20 = tf[2]["note_sur_20"]
|
||||
hide_groups = tf[2]["hide_groups"]
|
||||
with_emails = tf[2]["with_emails"]
|
||||
group_ids = [x for x in tf[2]["group_ids"] if x != ""]
|
||||
return (
|
||||
_make_table_notes(
|
||||
tf[1],
|
||||
|
@ -208,7 +209,7 @@ def do_evaluation_listenotes(
|
|||
format=format,
|
||||
note_sur_20=note_sur_20,
|
||||
anonymous_listing=anonymous_listing,
|
||||
group_ids=tf[2]["group_ids"],
|
||||
group_ids=group_ids,
|
||||
hide_groups=hide_groups,
|
||||
with_emails=with_emails,
|
||||
mode=mode,
|
||||
|
@ -652,11 +653,11 @@ def _add_eval_columns(
|
|||
notes = [] # liste des notes numeriques, pour calcul histogramme uniquement
|
||||
evaluation_id = e["evaluation_id"]
|
||||
e_o = Evaluation.query.get(evaluation_id) # XXX en attendant ré-écriture
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
for row in rows:
|
||||
etudid = row["etudid"]
|
||||
if etudid in NotesDB:
|
||||
val = NotesDB[etudid]["value"]
|
||||
if etudid in notes_db:
|
||||
val = notes_db[etudid]["value"]
|
||||
if val is None:
|
||||
nb_abs += 1
|
||||
if val == scu.NOTES_ATTENTE:
|
||||
|
@ -673,12 +674,12 @@ def _add_eval_columns(
|
|||
nb_notes = nb_notes + 1
|
||||
sum_notes += val
|
||||
val_fmt = scu.fmt_note(val, keep_numeric=keep_numeric)
|
||||
comment = NotesDB[etudid]["comment"]
|
||||
comment = notes_db[etudid]["comment"]
|
||||
if comment is None:
|
||||
comment = ""
|
||||
explanation = "%s (%s) %s" % (
|
||||
NotesDB[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
|
||||
sco_users.user_info(NotesDB[etudid]["uid"])["nomcomplet"],
|
||||
notes_db[etudid]["date"].strftime("%d/%m/%y %Hh%M"),
|
||||
sco_users.user_info(notes_db[etudid]["uid"])["nomcomplet"],
|
||||
comment,
|
||||
)
|
||||
else:
|
||||
|
|
|
@ -565,17 +565,17 @@ def do_etud_inscrit_ue(etudid, formsemestre_id, ue_id):
|
|||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""SELECT mi.moduleimpl_id
|
||||
"""SELECT mi.id
|
||||
FROM notes_moduleimpl mi, notes_modules mod, notes_formsemestre sem
|
||||
WHERE sem.formsemestre_id = %(formsemestre_id)s
|
||||
AND mi.formsemestre_id = sem.formsemestre_id
|
||||
AND mod.module_id = mi.module_id
|
||||
WHERE sem.id = %(formsemestre_id)s
|
||||
AND mi.formsemestre_id = sem.id
|
||||
AND mod.id = mi.module_id
|
||||
AND mod.ue_id = %(ue_id)s
|
||||
""",
|
||||
{"formsemestre_id": formsemestre_id, "ue_id": ue_id},
|
||||
)
|
||||
res = cursor.dictfetchall()
|
||||
for moduleimpl_id in [x["moduleimpl_id"] for x in res]:
|
||||
for moduleimpl_id in [x["id"] for x in res]:
|
||||
sco_moduleimpl.do_moduleimpl_inscription_create(
|
||||
{"moduleimpl_id": moduleimpl_id, "etudid": etudid},
|
||||
formsemestre_id=formsemestre_id,
|
||||
|
|
|
@ -36,6 +36,7 @@ from app.auth.models import User
|
|||
from app.models import ModuleImpl
|
||||
from app.models.evaluations import Evaluation
|
||||
import app.scodoc.sco_utils as scu
|
||||
from app.scodoc.sco_exceptions import ScoInvalidIdType
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
|
@ -183,6 +184,8 @@ def _ue_coefs_html(coefs_descr) -> str:
|
|||
|
||||
def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
||||
"""Tableau de bord module (liste des evaluations etc)"""
|
||||
if not isinstance(moduleimpl_id, int):
|
||||
raise ScoInvalidIdType("moduleimpl_id must be an integer !")
|
||||
modimpl = ModuleImpl.query.get_or_404(moduleimpl_id)
|
||||
M = modimpl.to_dict()
|
||||
formsemestre_id = M["formsemestre_id"]
|
||||
|
@ -394,7 +397,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
|
|||
eval_index = len(mod_evals) - 1
|
||||
first_eval = True
|
||||
for eval in mod_evals:
|
||||
evaluation = Evaluation.query.get(eval["evaluation_id"]) # TODO unifier
|
||||
evaluation: Evaluation = Evaluation.query.get(
|
||||
eval["evaluation_id"]
|
||||
) # TODO unifier
|
||||
etat = sco_evaluations.do_evaluation_etat(
|
||||
eval["evaluation_id"],
|
||||
partition_id=partition_id,
|
||||
|
|
|
@ -168,7 +168,7 @@ def can_change_groups(formsemestre_id):
|
|||
"Vrai si l'utilisateur peut changer les groupes dans ce semestre"
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||
if not sem["etat"]:
|
||||
return False # semestre verrouillé
|
||||
if current_user.has_permission(Permission.ScoEtudChangeGroups):
|
||||
|
|
|
@ -169,7 +169,9 @@ def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2):
|
|||
if doc:
|
||||
break
|
||||
if not doc:
|
||||
raise ScoValueError("pas de réponse du portail ! (timeout=%s)" % portal_timeout)
|
||||
raise ScoValueError(
|
||||
f"pas de réponse du portail ! <br>(timeout={portal_timeout}, requête: <tt>{req}</tt>)"
|
||||
)
|
||||
etuds = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req))
|
||||
|
||||
# Filtre sur annee inscription Apogee:
|
||||
|
@ -544,7 +546,7 @@ def check_paiement_etuds(etuds):
|
|||
etud["paiementinscription_str"] = "(pb cnx Apogée)"
|
||||
|
||||
|
||||
def get_maquette_apogee(etape="", annee_scolaire=""):
|
||||
def get_maquette_apogee(etape="", annee_scolaire="") -> str:
|
||||
"""Maquette CSV Apogee pour une étape et une annee scolaire"""
|
||||
maquette_url = get_maquette_url()
|
||||
if not maquette_url:
|
||||
|
|
|
@ -111,8 +111,9 @@ get_base_preferences(formsemestre_id)
|
|||
|
||||
"""
|
||||
import flask
|
||||
from flask import g, url_for, request
|
||||
from flask_login import current_user
|
||||
from flask import g, request, current_app
|
||||
|
||||
# from flask_login import current_user
|
||||
|
||||
from app.models import Departement
|
||||
from app.scodoc import sco_cache
|
||||
|
@ -762,7 +763,7 @@ class BasePreferences(object):
|
|||
{
|
||||
"initvalue": "Helvetica",
|
||||
"title": "Police de caractère principale",
|
||||
"explanation": "pour les pdf",
|
||||
"explanation": "pour les pdf (Helvetica est recommandée)",
|
||||
"size": 25,
|
||||
"category": "pdf",
|
||||
},
|
||||
|
@ -1537,7 +1538,7 @@ class BasePreferences(object):
|
|||
(
|
||||
"email_from_addr",
|
||||
{
|
||||
"initvalue": "noreply@scodoc.example.com",
|
||||
"initvalue": current_app.config["SCODOC_MAIL_FROM"],
|
||||
"title": "adresse mail origine",
|
||||
"size": 40,
|
||||
"explanation": "adresse expéditeur pour les envois par mails (bulletins)",
|
||||
|
|
|
@ -567,7 +567,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
|
|||
if "prev_decision" in row and row["prev_decision"]:
|
||||
counts[row["prev_decision"]] += 0
|
||||
# Légende des codes
|
||||
codes = list(counts.keys()) # sco_codes_parcours.CODES_EXPL.keys()
|
||||
codes = list(counts.keys())
|
||||
codes.sort()
|
||||
H.append("<h3>Explication des codes</h3>")
|
||||
lines = []
|
||||
|
|
|
@ -153,7 +153,10 @@ def _check_notes(notes, evaluation, mod):
|
|||
|
||||
for (etudid, note) in notes:
|
||||
note = str(note).strip().upper()
|
||||
etudid = int(etudid) #
|
||||
try:
|
||||
etudid = int(etudid) #
|
||||
except ValueError as exc:
|
||||
raise ScoValueError(f"Code étudiant ({etudid}) invalide")
|
||||
if note[:3] == "DEM":
|
||||
continue # skip !
|
||||
if note:
|
||||
|
@ -308,13 +311,13 @@ def do_evaluation_set_missing(evaluation_id, value, dialog_confirmed=False):
|
|||
# XXX imaginer un redirect + msg erreur
|
||||
raise AccessDenied("Modification des notes impossible pour %s" % current_user)
|
||||
#
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
etudid_etats = sco_groups.do_evaluation_listeetuds_groups(
|
||||
evaluation_id, getallstudents=True, include_dems=False
|
||||
)
|
||||
notes = []
|
||||
for etudid, _ in etudid_etats: # pour tous les inscrits
|
||||
if etudid not in NotesDB: # pas de note
|
||||
if etudid not in notes_db: # pas de note
|
||||
notes.append((etudid, value))
|
||||
# Check value
|
||||
L, invalids, _, _, _ = _check_notes(notes, E, M["module"])
|
||||
|
@ -393,18 +396,18 @@ def evaluation_suppress_alln(evaluation_id, dialog_confirmed=False):
|
|||
):
|
||||
# On a le droit de modifier toutes les notes
|
||||
# recupere les etuds ayant une note
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
elif sco_permissions_check.can_edit_notes(
|
||||
current_user, E["moduleimpl_id"], allow_ens=True
|
||||
):
|
||||
# Enseignant associé au module: ne peut supprimer que les notes qu'il a saisi
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(
|
||||
evaluation_id, by_uid=current_user.id
|
||||
)
|
||||
else:
|
||||
raise AccessDenied("Modification des notes impossible pour %s" % current_user)
|
||||
|
||||
notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in NotesDB.keys()]
|
||||
notes = [(etudid, scu.NOTES_SUPPRESS) for etudid in notes_db.keys()]
|
||||
|
||||
if not dialog_confirmed:
|
||||
nb_changed, nb_suppress, existing_decisions = notes_add(
|
||||
|
@ -487,13 +490,13 @@ def notes_add(
|
|||
}
|
||||
for (etudid, value) in notes:
|
||||
if check_inscription and (etudid not in inscrits):
|
||||
raise NoteProcessError("etudiant non inscrit dans ce module")
|
||||
if not ((value is None) or (type(value) == type(1.0))):
|
||||
raise NoteProcessError(f"etudiant {etudid} non inscrit dans ce module")
|
||||
if (value is not None) and not isinstance(value, float):
|
||||
raise NoteProcessError(
|
||||
"etudiant %s: valeur de note invalide (%s)" % (etudid, value)
|
||||
f"etudiant {etudid}: valeur de note invalide ({value})"
|
||||
)
|
||||
# Recherche notes existantes
|
||||
NotesDB = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)
|
||||
# Met a jour la base
|
||||
cnx = ndb.GetDBConnexion()
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
|
@ -507,7 +510,7 @@ def notes_add(
|
|||
try:
|
||||
for (etudid, value) in notes:
|
||||
changed = False
|
||||
if etudid not in NotesDB:
|
||||
if etudid not in notes_db:
|
||||
# nouvelle note
|
||||
if value != scu.NOTES_SUPPRESS:
|
||||
if do_it:
|
||||
|
@ -530,7 +533,7 @@ def notes_add(
|
|||
changed = True
|
||||
else:
|
||||
# il y a deja une note
|
||||
oldval = NotesDB[etudid]["value"]
|
||||
oldval = notes_db[etudid]["value"]
|
||||
if type(value) != type(oldval):
|
||||
changed = True
|
||||
elif type(value) == type(1.0) and (
|
||||
|
|
|
@ -395,6 +395,8 @@ def do_semset_add_sem(semset_id, formsemestre_id):
|
|||
"""Add a sem to a semset"""
|
||||
if not semset_id:
|
||||
raise ScoValueError("empty semset_id")
|
||||
if formsemestre_id == "":
|
||||
raise ScoValueError("pas de semestre choisi !")
|
||||
s = SemSet(semset_id=semset_id)
|
||||
# check for valid formsemestre_id
|
||||
_ = sco_formsemestre.get_formsemestre(formsemestre_id) # raise exc
|
||||
|
|
|
@ -44,6 +44,7 @@ from app.scodoc import sco_groups_view
|
|||
from app.scodoc import sco_preferences
|
||||
from app.scodoc import sco_trombino
|
||||
from app.scodoc import sco_etud
|
||||
from app.scodoc.sco_exceptions import ScoPDFFormatError
|
||||
from app.scodoc.sco_pdf import *
|
||||
|
||||
|
||||
|
@ -268,7 +269,10 @@ def pdf_trombino_tours(
|
|||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
)
|
||||
document.build(objects)
|
||||
try:
|
||||
document.build(objects)
|
||||
except (ValueError, KeyError) as exc:
|
||||
raise ScoPDFFormatError(str(exc)) from exc
|
||||
data = report.getvalue()
|
||||
|
||||
return scu.sendPDFFile(data, filename)
|
||||
|
|
|
@ -181,7 +181,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||
"""
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||
r = ndb.SimpleDictFetch(
|
||||
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name
|
||||
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name, e.id as evaluation_id
|
||||
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
|
||||
notes_modules mod, identite i, "user" u
|
||||
WHERE mi.id = e.moduleimpl_id
|
||||
|
@ -202,6 +202,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||
"value",
|
||||
"user_name",
|
||||
"titre",
|
||||
"evaluation_id",
|
||||
"description",
|
||||
"jour",
|
||||
"comment",
|
||||
|
@ -214,6 +215,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
|
|||
"value": "Note",
|
||||
"comment": "Remarque",
|
||||
"user_name": "Enseignant",
|
||||
"evaluation_id": "evaluation_id",
|
||||
"titre": "Module",
|
||||
"description": "Evaluation",
|
||||
"jour": "Date éval.",
|
||||
|
|
|
@ -77,6 +77,17 @@ section>div:nth-child(1){
|
|||
display: flex !important;
|
||||
}
|
||||
|
||||
.listeOff .ue::before,
|
||||
.listeOff .module::before,
|
||||
.moduleOnOff .ue::before,
|
||||
.moduleOnOff .module::before{
|
||||
transform: rotate(0);
|
||||
}
|
||||
.listeOff .moduleOnOff .ue::before,
|
||||
.listeOff .moduleOnOff .module::before{
|
||||
transform: rotate(180deg) !important;
|
||||
}
|
||||
|
||||
/***********************/
|
||||
/* Options d'affichage */
|
||||
/***********************/
|
||||
|
@ -118,11 +129,16 @@ section>div:nth-child(1){
|
|||
/************/
|
||||
/* Semestre */
|
||||
/************/
|
||||
.flex{
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.infoSemestre{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
flex: none;
|
||||
}
|
||||
.infoSemestre>div{
|
||||
border: 1px solid var(--couleurIntense);
|
||||
|
@ -141,7 +157,12 @@ section>div:nth-child(1){
|
|||
.rang{
|
||||
text-decoration: underline var(--couleurIntense);
|
||||
}
|
||||
|
||||
.decision{
|
||||
margin: 5px 0;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
text-decoration: underline var(--couleurIntense);
|
||||
}
|
||||
.enteteSemestre{
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
|
@ -174,8 +195,21 @@ section>div:nth-child(1){
|
|||
display: flex;
|
||||
gap: 16px;
|
||||
margin: 4px 0 2px 0;
|
||||
overflow: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.module::before, .ue::before {
|
||||
content:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26px' height='26px' fill='black'><path d='M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z' /></svg>");
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
margin-left: -13px;
|
||||
transform: rotate(180deg);
|
||||
transition: 0.2s;
|
||||
}
|
||||
h3{
|
||||
display: flex;
|
||||
|
|
|
@ -1491,6 +1491,16 @@ table.moduleimpl_evaluations td.eval_poids {
|
|||
color:rgb(0, 0, 255);
|
||||
}
|
||||
|
||||
span.eval_coef_ue {
|
||||
color:rgb(6, 73, 6);
|
||||
font-style: normal;
|
||||
font-size: 80%;
|
||||
margin-right: 2em;
|
||||
}
|
||||
span.eval_coef_ue_titre {
|
||||
|
||||
}
|
||||
|
||||
/* Formulaire edition des partitions */
|
||||
form#editpart table {
|
||||
border: 1px solid gray;
|
||||
|
|
|
@ -1,42 +1,49 @@
|
|||
/* Module par Seb. L. */
|
||||
class releveBUT extends HTMLElement {
|
||||
constructor(){
|
||||
constructor() {
|
||||
super();
|
||||
this.shadow = this.attachShadow({mode: 'open'});
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
|
||||
/* Config par defaut */
|
||||
this.config = {
|
||||
showURL: true
|
||||
};
|
||||
|
||||
|
||||
/* Template du module */
|
||||
this.shadow.innerHTML = this.template();
|
||||
|
||||
|
||||
/* Style du module */
|
||||
const styles = document.createElement('link');
|
||||
styles.setAttribute('rel', 'stylesheet');
|
||||
styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css');
|
||||
this.shadow.appendChild(styles);
|
||||
/* variante "ScoDoc" ou "Passerelle" (ENT) ? */
|
||||
if (location.href.split("/")[3] == "ScoDoc") { /* un peu osé... */
|
||||
styles.setAttribute('href', '/ScoDoc/static/css/releve-but.css');
|
||||
} else {
|
||||
// Passerelle
|
||||
styles.setAttribute('href', '/assets/styles/releve-but.css');
|
||||
}
|
||||
this.shadow.appendChild(styles);
|
||||
}
|
||||
listeOnOff() {
|
||||
this.parentElement.parentElement.classList.toggle("listeOff");
|
||||
this.parentElement.parentElement.querySelectorAll(".moduleOnOff").forEach(e=>{
|
||||
this.parentElement.parentElement.querySelectorAll(".moduleOnOff").forEach(e => {
|
||||
e.classList.remove("moduleOnOff")
|
||||
})
|
||||
}
|
||||
moduleOnOff(){
|
||||
moduleOnOff() {
|
||||
this.parentElement.classList.toggle("moduleOnOff");
|
||||
}
|
||||
goTo(){
|
||||
goTo() {
|
||||
let module = this.dataset.module;
|
||||
this.parentElement.parentElement.parentElement.parentElement.querySelector("#Module_" + module).scrollIntoView();
|
||||
}
|
||||
|
||||
set setConfig(config){
|
||||
set setConfig(config) {
|
||||
this.config.showURL = config.showURL ?? this.config.showURL;
|
||||
}
|
||||
|
||||
set showData(data) {
|
||||
set showData(data) {
|
||||
this.showInformations(data);
|
||||
this.showSemestre(data);
|
||||
this.showSynthese(data);
|
||||
|
@ -46,7 +53,7 @@ class releveBUT extends HTMLElement {
|
|||
|
||||
this.shadow.querySelectorAll(".CTA_Liste").forEach(e => {
|
||||
e.addEventListener("click", this.listeOnOff)
|
||||
})
|
||||
})
|
||||
this.shadow.querySelectorAll(".ue, .module").forEach(e => {
|
||||
e.addEventListener("click", this.moduleOnOff)
|
||||
})
|
||||
|
@ -57,7 +64,7 @@ class releveBUT extends HTMLElement {
|
|||
this.shadow.children[0].classList.add("ready");
|
||||
}
|
||||
|
||||
template(){
|
||||
template() {
|
||||
return `
|
||||
<div>
|
||||
<div class="wait"></div>
|
||||
|
@ -75,10 +82,15 @@ class releveBUT extends HTMLElement {
|
|||
<!--------------------------->
|
||||
<section>
|
||||
<h2>Semestre </h2>
|
||||
<div class=dateInscription>Inscrit le </div>
|
||||
<em>Les moyennes servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de
|
||||
compétences ou d'UE.</em>
|
||||
<div class=infoSemestre></div>
|
||||
<div class=flex>
|
||||
<div class=infoSemestre></div>
|
||||
<div>
|
||||
<div class=decision></div>
|
||||
<div class=dateInscription>Inscrit le </div>
|
||||
<em>Les moyennes servent à situer l'étudiant dans la promotion et ne correspondent pas à des validations de compétences ou d'UE.</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!--------------------------->
|
||||
|
@ -91,8 +103,7 @@ class releveBUT extends HTMLElement {
|
|||
<em>La moyenne des ressources dans une UE dépend des poids donnés aux évaluations.</em>
|
||||
</div>
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -107,8 +118,7 @@ class releveBUT extends HTMLElement {
|
|||
<div>
|
||||
<h2>Ressources</h2>
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -120,8 +130,7 @@ class releveBUT extends HTMLElement {
|
|||
<div>
|
||||
<h2>SAÉ</h2>
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
|
@ -140,8 +149,8 @@ class releveBUT extends HTMLElement {
|
|||
this.shadow.querySelector(".studentPic").src = data.etudiant.photo_url || "default_Student.svg";
|
||||
|
||||
let output = '';
|
||||
|
||||
if(this.config.showURL){
|
||||
|
||||
if (this.config.showURL) {
|
||||
output += `<a href="${data.etudiant.fiche_url}" class=info_etudiant>`;
|
||||
} else {
|
||||
output += `<div class=info_etudiant>`;
|
||||
|
@ -165,7 +174,7 @@ class releveBUT extends HTMLElement {
|
|||
</div>
|
||||
<div>${data.formation.titre}</div>
|
||||
`;
|
||||
if(this.config.showURL){
|
||||
if (this.config.showURL) {
|
||||
output += `</a>`;
|
||||
} else {
|
||||
output += `</div>`;
|
||||
|
@ -187,21 +196,21 @@ class releveBUT extends HTMLElement {
|
|||
<div>Max. promo. :</div><div>${data.semestre.notes.max}</div>
|
||||
<div>Moy. promo. :</div><div>${data.semestre.notes.moy}</div>
|
||||
<div>Min. promo. :</div><div>${data.semestre.notes.min}</div>
|
||||
</div>
|
||||
${data.semestre.groupes.map(groupe => {
|
||||
</div>`;
|
||||
/*${data.semestre.groupes.map(groupe => {
|
||||
return `
|
||||
<div>
|
||||
<div class=enteteSemestre>Groupe</div><div class=enteteSemestre>${groupe.nom}</div>
|
||||
<div class=rang>Rang :</div><div class=rang>${groupe.rang.value} / ${groupe.rang.total}</div>
|
||||
<div>Max. groupe :</div><div>${groupe.notes.max}</div>
|
||||
<div>Moy. groupe :</div><div>${groupe.notes.min}</div>
|
||||
<div>Min. groupe :</div><div>${groupe.notes.min}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("")
|
||||
}
|
||||
`;
|
||||
<div>
|
||||
<div class=enteteSemestre>Groupe</div><div class=enteteSemestre>${groupe.nom}</div>
|
||||
<div class=rang>Rang :</div><div class=rang>${groupe.rang.value} / ${groupe.rang.total}</div>
|
||||
<div>Max. groupe :</div><div>${groupe.notes.max}</div>
|
||||
<div>Moy. groupe :</div><div>${groupe.notes.min}</div>
|
||||
<div>Min. groupe :</div><div>${groupe.notes.min}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("")
|
||||
}*/
|
||||
this.shadow.querySelector(".infoSemestre").innerHTML = output;
|
||||
/*this.shadow.querySelector(".decision").innerHTML = data.semestre.decision.code;*/
|
||||
}
|
||||
|
||||
/*******************************/
|
||||
|
@ -211,6 +220,7 @@ class releveBUT extends HTMLElement {
|
|||
let output = ``;
|
||||
Object.entries(data.ues).forEach(([ue, dataUE]) => {
|
||||
output += `
|
||||
|
||||
<div>
|
||||
<div class=ue>
|
||||
<h3>
|
||||
|
@ -255,7 +265,7 @@ class releveBUT extends HTMLElement {
|
|||
})
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
/*******************************/
|
||||
/* Evaluations */
|
||||
/*******************************/
|
||||
|
@ -333,8 +343,8 @@ class releveBUT extends HTMLElement {
|
|||
/********************/
|
||||
/* Fonctions d'aide */
|
||||
/********************/
|
||||
URL(href, content){
|
||||
if(this.config.showURL){
|
||||
URL(href, content) {
|
||||
if (this.config.showURL) {
|
||||
return `<a href=${href}>${content}</a>`;
|
||||
} else {
|
||||
return content;
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
{% block app_content %}
|
||||
<h1>Modification du compte ScoDoc <tt>{{form.user_name.data}}</tt></h1>
|
||||
<div class="help">
|
||||
<p>Identifiez-vous avez votre mot de passe actuel</p>
|
||||
<p>Identifiez-vous avec votre mot de passe actuel</p>
|
||||
</div>
|
||||
<form method=post>
|
||||
{{ form.user_name }}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
{% import 'bootstrap/wtf.html' as wtf %}
|
||||
|
||||
{% block app_content %}
|
||||
<h1>Configuration des codes de décision exportés vers Apogée</h1>
|
||||
|
||||
|
||||
<div class="help">
|
||||
<p>Ces codes (ADM, AJ, ...) sont utilisés pour représenter les décisions de jury
|
||||
et les validations de semestres ou d'UE. les valeurs indiquées ici sont utilisées
|
||||
dans les exports Apogée.
|
||||
<p>
|
||||
<p>Ne les modifier que si vous savez ce que vous faites !
|
||||
</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ wtf.quick_form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
|
@ -92,6 +92,8 @@
|
|||
<div class="sco_help">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):</div>
|
||||
{{ render_field(form.bonus_sport_func_name, onChange="submit_form()")}}
|
||||
|
||||
<h1>Exports Apogée</h1>
|
||||
<p><a href="{{url_for('scodoc.config_codes_decisions')}}">configuration des codes de décision</a></p>
|
||||
<h1>Bibliothèque de logos</h1>
|
||||
{% for dept_entry in form.depts.entries %}
|
||||
{% set dept_form = dept_entry.form %}
|
||||
|
|
|
@ -24,4 +24,24 @@
|
|||
<a href="https://scodoc.org/BUT" target="_blank">la documentation</a>.
|
||||
</p>
|
||||
{%endif%}
|
||||
|
||||
{% if formsemestres %}
|
||||
<p class="help">
|
||||
Ce module est utilisé dans des semestres déjà mis en place, il faut prêter attention
|
||||
aux conséquences des changements effectués ici: par exemple les coefficients vont modifier
|
||||
les notes moyennes calculées. Les modules déjà utilisés ne peuvent pas être changés de semestre, ni détruits.
|
||||
Si vous souhaitez faire cela, allez d'abord modifier les semestres concernés pour déselectionner le module.
|
||||
</p>
|
||||
<h4>Semestres utilisant ce module:</h4>
|
||||
<ul>
|
||||
{%for formsemestre in formsemestres %}
|
||||
<li><a class="stdlink" href="{{
|
||||
url_for('notes.formsemestre_status',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
|
||||
}}">{{formsemestre.titre_mois()}}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{%endif%}
|
||||
|
||||
</div>
|
|
@ -1151,8 +1151,8 @@ def AddBilletAbsenceForm(etudid):
|
|||
scu.get_request_args(),
|
||||
(
|
||||
("etudid", {"input_type": "hidden"}),
|
||||
("begin", {"input_type": "date"}),
|
||||
("end", {"input_type": "date"}),
|
||||
("begin", {"input_type": "datedmy"}),
|
||||
("end", {"input_type": "datedmy"}),
|
||||
(
|
||||
"justified",
|
||||
{"input_type": "boolcheckbox", "default": 0, "title": "Justifiée"},
|
||||
|
|
|
@ -72,12 +72,7 @@ from app import log, send_scodoc_alarm
|
|||
from app.scodoc import scolog
|
||||
from app.scodoc.scolog import logdb
|
||||
|
||||
from app.scodoc.sco_exceptions import (
|
||||
ScoValueError,
|
||||
ScoLockedFormError,
|
||||
ScoGenError,
|
||||
AccessDenied,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoInvalidIdType
|
||||
from app.scodoc import html_sco_header
|
||||
from app.pe import pe_view
|
||||
from app.scodoc import sco_abs
|
||||
|
@ -284,20 +279,37 @@ def formsemestre_bulletinetud(
|
|||
force_publishing=False,
|
||||
prefer_mail_perso=False,
|
||||
code_nip=None,
|
||||
code_ine=None,
|
||||
):
|
||||
if not formsemestre_id:
|
||||
flask.abort(404, "argument manquant: formsemestre_id")
|
||||
if not isinstance(formsemestre_id, int):
|
||||
raise ScoInvalidIdType("formsemestre_id must be an integer !")
|
||||
formsemestre = FormSemestre.query.get_or_404(formsemestre_id)
|
||||
if formsemestre.formation.is_apc() and format != "oldjson":
|
||||
if etudid:
|
||||
etud = models.Identite.query.get_or_404(etudid)
|
||||
elif code_nip:
|
||||
etud = models.Identite.query.filter_by(
|
||||
code_nip=str(code_nip)
|
||||
).first_or_404()
|
||||
etud = (
|
||||
models.Identite.query.filter_by(code_nip=str(code_nip))
|
||||
.filter_by(dept_id=formsemestre.dept_id)
|
||||
.first_or_404()
|
||||
)
|
||||
elif code_ine:
|
||||
etud = (
|
||||
models.Identite.query.filter_by(code_ine=str(code_ine))
|
||||
.filter_by(dept_id=formsemestre.dept_id)
|
||||
.first_or_404()
|
||||
)
|
||||
else:
|
||||
raise ScoValueError(
|
||||
"Paramètre manquant: spécifier code_nip ou etudid ou code_ine"
|
||||
)
|
||||
if format == "json":
|
||||
r = bulletin_but.ResultatsSemestreBUT(formsemestre)
|
||||
return jsonify(r.bulletin_etud(etud, formsemestre))
|
||||
return jsonify(
|
||||
r.bulletin_etud(etud, formsemestre, force_publishing=force_publishing)
|
||||
)
|
||||
elif format == "html":
|
||||
return render_template(
|
||||
"but/bulletin.html",
|
||||
|
@ -308,12 +320,15 @@ def formsemestre_bulletinetud(
|
|||
formsemestre_id=formsemestre_id,
|
||||
etudid=etudid,
|
||||
format="json",
|
||||
force_publishing=1, # pour ScoDoc lui même
|
||||
),
|
||||
sco=ScoData(),
|
||||
)
|
||||
|
||||
if not (etudid or code_nip):
|
||||
raise ScoValueError("Paramètre manquant: spécifier code_nip ou etudid")
|
||||
if not (etudid or code_nip or code_ine):
|
||||
raise ScoValueError(
|
||||
"Paramètre manquant: spécifier code_nip ou etudid ou code_ine"
|
||||
)
|
||||
if format == "oldjson":
|
||||
format = "json"
|
||||
return sco_bulletins.formsemestre_bulletinetud(
|
||||
|
@ -744,6 +759,10 @@ def XMLgetFormsemestres(etape_apo=None, formsemestre_id=None):
|
|||
DEPRECATED: use formsemestre_list()
|
||||
"""
|
||||
current_app.logger.debug("Warning: calling deprecated XMLgetFormsemestres")
|
||||
if not formsemestre_id:
|
||||
return flask.abort(404, "argument manquant: formsemestre_id")
|
||||
if not isinstance(formsemestre_id, int):
|
||||
return flask.abort(404, "formsemestre_id must be an integer !")
|
||||
args = {}
|
||||
if etape_apo:
|
||||
args["etape_apo"] = etape_apo
|
||||
|
|
|
@ -33,49 +33,38 @@ Emmanuel Viennet, 2021
|
|||
import datetime
|
||||
import io
|
||||
|
||||
import wtforms.validators
|
||||
|
||||
from app.auth.models import User
|
||||
import os
|
||||
|
||||
import flask
|
||||
from flask import abort, flash, url_for, redirect, render_template, send_file
|
||||
from flask import request
|
||||
from flask.app import Flask
|
||||
import flask_login
|
||||
from flask_login.utils import login_required, current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
from wtforms import SelectField, SubmitField, FormField, validators, Form, FieldList
|
||||
from wtforms.fields import IntegerField
|
||||
from wtforms.fields.simple import BooleanField, StringField, TextAreaField, HiddenField
|
||||
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from werkzeug.exceptions import BadRequest, NotFound
|
||||
|
||||
|
||||
import app
|
||||
from app import db
|
||||
from app.auth.models import User
|
||||
from app.forms.main import config_forms
|
||||
from app.forms.main.create_dept import CreateDeptForm
|
||||
from app.forms.main.config_apo import CodesDecisionsForm
|
||||
from app import models
|
||||
from app.models import Departement, Identite
|
||||
from app.models import departements
|
||||
from app.models import FormSemestre, FormSemestreInscription
|
||||
import sco_version
|
||||
from app.scodoc import sco_logos
|
||||
from app.models import ScoDocSiteConfig
|
||||
from app.scodoc import sco_codes_parcours, sco_logos
|
||||
from app.scodoc import sco_find_etud
|
||||
from app.scodoc import sco_utils as scu
|
||||
from app.decorators import (
|
||||
admin_required,
|
||||
scodoc7func,
|
||||
scodoc,
|
||||
permission_required_compat_scodoc7,
|
||||
permission_required,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import AccessDenied
|
||||
from app.scodoc.sco_logos import find_logo
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
from app.views import scodoc_bp as bp
|
||||
|
||||
from PIL import Image as PILImage
|
||||
import sco_version
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
|
@ -132,6 +121,28 @@ def toggle_dept_vis(dept_id):
|
|||
return redirect(url_for("scodoc.index"))
|
||||
|
||||
|
||||
@bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"])
|
||||
@admin_required
|
||||
def config_codes_decisions():
|
||||
"""Form config codes decisions"""
|
||||
form = CodesDecisionsForm()
|
||||
if request.method == "POST" and form.cancel.data: # cancel button
|
||||
return redirect(url_for("scodoc.index"))
|
||||
if form.validate_on_submit():
|
||||
for code in models.config.CODES_SCODOC_TO_APO:
|
||||
ScoDocSiteConfig.set_code_apo(code, getattr(form, code).data)
|
||||
flash(f"Codes décisions enregistrés.")
|
||||
return redirect(url_for("scodoc.index"))
|
||||
elif request.method == "GET":
|
||||
for code in models.config.CODES_SCODOC_TO_APO:
|
||||
getattr(form, code).data = ScoDocSiteConfig.get_code_apo(code)
|
||||
return render_template(
|
||||
"config_codes_decisions.html",
|
||||
form=form,
|
||||
title="Configuration des codes de décisions",
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
|
||||
@login_required
|
||||
def table_etud_in_accessible_depts():
|
||||
|
@ -255,14 +266,16 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True):
|
|||
suffix = logo.suffix
|
||||
if small:
|
||||
with PILImage.open(logo.filepath) as im:
|
||||
im.thumbnail(SMALL_SIZE)
|
||||
stream = io.BytesIO()
|
||||
# on garde le même format (on pourrait plus simplement générer systématiquement du JPEG)
|
||||
fmt = { # adapt suffix to be compliant with PIL save format
|
||||
"PNG": "PNG",
|
||||
"JPG": "JPEG",
|
||||
"JPEG": "JPEG",
|
||||
}[suffix.upper()]
|
||||
if fmt == "JPEG":
|
||||
im = im.convert("RGB")
|
||||
im.thumbnail(SMALL_SIZE)
|
||||
stream = io.BytesIO()
|
||||
im.save(stream, fmt)
|
||||
stream.seek(0)
|
||||
return send_file(stream, mimetype=f"image/{fmt}")
|
||||
|
|
|
@ -81,7 +81,7 @@ _l = _
|
|||
class ChangePasswordForm(FlaskForm):
|
||||
user_name = HiddenField()
|
||||
old_password = PasswordField(_l("Identifiez-vous"))
|
||||
new_password = PasswordField(_l("Nouveau mot de passe"))
|
||||
new_password = PasswordField(_l("Nouveau mot de passe de l'utilisateur"))
|
||||
bis_password = PasswordField(
|
||||
_l("Répéter"),
|
||||
validators=[
|
||||
|
@ -151,8 +151,10 @@ def user_info(user_name, format="json"):
|
|||
@scodoc7func
|
||||
def create_user_form(user_name=None, edit=0, all_roles=1):
|
||||
"form. création ou edition utilisateur"
|
||||
if user_name is not None: # scodoc7func converti en int !
|
||||
user_name = str(user_name)
|
||||
auth_dept = current_user.dept
|
||||
from_mail = current_user.email
|
||||
from_mail = current_app.config["SCODOC_MAIL_FROM"] # current_user.email
|
||||
initvalues = {}
|
||||
edit = int(edit)
|
||||
all_roles = int(all_roles)
|
||||
|
@ -424,7 +426,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
|
|||
"date_expiration",
|
||||
{
|
||||
"title": "Date d'expiration", # j/m/a
|
||||
"input_type": "date",
|
||||
"input_type": "datedmy",
|
||||
"explanation": "j/m/a, laisser vide si pas de limite",
|
||||
"size": 9,
|
||||
"allow_null": True,
|
||||
|
@ -575,8 +577,8 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
|
|||
# A: envoi de welcome + procedure de reset
|
||||
# B: envoi de welcome seulement (mot de passe saisie dans le formulaire)
|
||||
# C: Aucun envoi (mot de passe saisi dans le formulaire)
|
||||
if vals["welcome"] == "1":
|
||||
if vals["reset_password:list"] == "1":
|
||||
if vals["welcome"] != "1":
|
||||
if vals["reset_password"] != "1":
|
||||
mode = Mode.WELCOME_AND_CHANGE_PASSWORD
|
||||
else:
|
||||
mode = Mode.WELCOME_ONLY
|
||||
|
@ -745,6 +747,8 @@ def user_info_page(user_name=None):
|
|||
"""
|
||||
from app.scodoc.sco_permissions_check import can_handle_passwd
|
||||
|
||||
if user_name is not None: # scodoc7func converti en int !
|
||||
user_name = str(user_name)
|
||||
# peut on divulguer ces infos ?
|
||||
if not can_handle_passwd(current_user, allow_admindepts=True):
|
||||
raise AccessDenied("Vous n'avez pas la permission de voir cette page")
|
||||
|
@ -802,6 +806,8 @@ def form_change_password(user_name=None):
|
|||
"""Formulaire de changement mot de passe de l'utilisateur user_name.
|
||||
Un utilisateur peut toujours changer son propre mot de passe.
|
||||
"""
|
||||
if user_name is not None: # scodoc7func converti en int !
|
||||
user_name = str(user_name)
|
||||
if not user_name:
|
||||
user = current_user
|
||||
else:
|
||||
|
@ -850,6 +856,8 @@ def form_change_password(user_name=None):
|
|||
@scodoc7func
|
||||
def change_password(user_name, password, password2):
|
||||
"Change the password for user given by user_name"
|
||||
if user_name is not None: # scodoc7func converti en int !
|
||||
user_name = str(user_name)
|
||||
u = User.query.filter_by(user_name=user_name).first()
|
||||
# Check access permission
|
||||
if not can_handle_passwd(u):
|
||||
|
@ -909,6 +917,8 @@ def change_password(user_name, password, password2):
|
|||
@permission_required(Permission.ScoUsersAdmin)
|
||||
def toggle_active_user(user_name: str = None):
|
||||
"""Change active status of a user account"""
|
||||
if user_name is not None: # scodoc7func converti en int !
|
||||
user_name = str(user_name)
|
||||
u = User.query.filter_by(user_name=user_name).first()
|
||||
if not u:
|
||||
raise ScoValueError("invalid user_name")
|
||||
|
|
|
@ -26,6 +26,9 @@ class Config:
|
|||
SCODOC_ADMIN_LOGIN = os.environ.get("SCODOC_ADMIN_LOGIN") or "admin"
|
||||
ADMINS = [SCODOC_ADMIN_MAIL]
|
||||
SCODOC_ERR_MAIL = os.environ.get("SCODOC_ERR_MAIL")
|
||||
# Le "from" des mails émis. Attention: peut être remplacée par la préférence email_from_addr:
|
||||
SCODOC_MAIL_FROM = os.environ.get("SCODOC_MAIL_FROM") or ("no-reply@" + MAIL_SERVER)
|
||||
|
||||
BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL")
|
||||
SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc")
|
||||
SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "/opt/scodoc-data")
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
"""augmente taille codes Apogée
|
||||
|
||||
Revision ID: 28874ed6af64
|
||||
Revises: f40fbaf5831c
|
||||
Create Date: 2022-01-19 22:57:59.678313
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "28874ed6af64"
|
||||
down_revision = "f40fbaf5831c"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
op.alter_column(
|
||||
"notes_formsemestre_etapes",
|
||||
"etape_apo",
|
||||
existing_type=sa.VARCHAR(length=24),
|
||||
type_=sa.String(length=512),
|
||||
existing_nullable=True,
|
||||
)
|
||||
op.alter_column(
|
||||
"notes_formsemestre_inscription",
|
||||
"etape",
|
||||
existing_type=sa.VARCHAR(length=24),
|
||||
type_=sa.String(length=512),
|
||||
existing_nullable=True,
|
||||
)
|
||||
op.alter_column(
|
||||
"notes_modules",
|
||||
"code_apogee",
|
||||
existing_type=sa.VARCHAR(length=24),
|
||||
type_=sa.String(length=512),
|
||||
existing_nullable=True,
|
||||
)
|
||||
op.alter_column(
|
||||
"notes_ue",
|
||||
"code_apogee",
|
||||
existing_type=sa.VARCHAR(length=24),
|
||||
type_=sa.String(length=512),
|
||||
existing_nullable=True,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column(
|
||||
"notes_ue",
|
||||
"code_apogee",
|
||||
existing_type=sa.String(length=512),
|
||||
type_=sa.VARCHAR(length=24),
|
||||
existing_nullable=True,
|
||||
)
|
||||
op.alter_column(
|
||||
"notes_modules",
|
||||
"code_apogee",
|
||||
existing_type=sa.String(length=512),
|
||||
type_=sa.VARCHAR(length=24),
|
||||
existing_nullable=True,
|
||||
)
|
||||
op.alter_column(
|
||||
"notes_formsemestre_inscription",
|
||||
"etape",
|
||||
existing_type=sa.String(length=512),
|
||||
type_=sa.VARCHAR(length=24),
|
||||
existing_nullable=True,
|
||||
)
|
||||
op.alter_column(
|
||||
"notes_formsemestre_etapes",
|
||||
"etape_apo",
|
||||
existing_type=sa.String(length=512),
|
||||
type_=sa.VARCHAR(length=24),
|
||||
existing_nullable=True,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,34 @@
|
|||
"""index ine et nip
|
||||
|
||||
Revision ID: f40fbaf5831c
|
||||
Revises: 91be8a06d423
|
||||
Create Date: 2022-01-10 15:13:06.867903
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "f40fbaf5831c"
|
||||
down_revision = "91be8a06d423"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index(
|
||||
op.f("ix_identite_code_ine"), "identite", ["code_ine"], unique=False
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_identite_code_nip"), "identite", ["code_nip"], unique=False
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f("ix_identite_code_nip"), table_name="identite")
|
||||
op.drop_index(op.f("ix_identite_code_ine"), table_name="identite")
|
||||
# ### end Alembic commands ###
|
|
@ -1,7 +1,7 @@
|
|||
# -*- mode: python -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
SCOVERSION = "9.1.18"
|
||||
SCOVERSION = "9.1.36"
|
||||
|
||||
SCONAME = "ScoDoc"
|
||||
|
||||
|
|
35
scodoc.py
35
scodoc.py
|
@ -278,20 +278,28 @@ def user_role(username, dept_acronym=None, add_role_name=None, remove_role_name=
|
|||
db.session.commit()
|
||||
|
||||
|
||||
def abort_if_false(ctx, param, value):
|
||||
if not value:
|
||||
ctx.abort()
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
@click.option(
|
||||
"--yes",
|
||||
is_flag=True,
|
||||
callback=abort_if_false,
|
||||
expose_value=False,
|
||||
prompt=f"""Attention: Cela va effacer toutes les données du département
|
||||
(étudiants, notes, formations, etc)
|
||||
Voulez-vous vraiment continuer ?
|
||||
""",
|
||||
)
|
||||
@click.argument("dept")
|
||||
def delete_dept(dept): # delete-dept
|
||||
"""Delete existing departement"""
|
||||
from app.scodoc import notesdb as ndb
|
||||
from app.scodoc import sco_dept
|
||||
|
||||
click.confirm(
|
||||
f"""Attention: Cela va effacer toutes les données du département {dept}
|
||||
(étudiants, notes, formations, etc)
|
||||
Voulez-vous vraiment continuer ?
|
||||
""",
|
||||
abort=True,
|
||||
)
|
||||
db.reflect()
|
||||
ndb.open_db_connection()
|
||||
d = models.Departement.query.filter_by(acronym=dept).first()
|
||||
|
@ -438,14 +446,21 @@ def photos_import_files(formsemestre_id: int, xlsfile: str, zipfile: str):
|
|||
|
||||
|
||||
@app.cli.command()
|
||||
@click.option("--sanitize/--no-sanitize", default=False)
|
||||
@with_appcontext
|
||||
def clear_cache(): # clear-cache
|
||||
def clear_cache(sanitize): # clear-cache
|
||||
"""Clear ScoDoc cache
|
||||
This cache (currently Redis) is persistent between invocation
|
||||
and it may be necessary to clear it during development or tests.
|
||||
and it may be necessary to clear it during upgrades,
|
||||
development or tests.
|
||||
"""
|
||||
click.echo("Flushing Redis cache...")
|
||||
clear_scodoc_cache()
|
||||
click.echo("Redis caches flushed.")
|
||||
if sanitize:
|
||||
# sanitizes all formations:
|
||||
click.echo("Checking formations...")
|
||||
for formation in Formation.query:
|
||||
formation.sanitize_old_formation()
|
||||
|
||||
|
||||
def recursive_help(cmd, parent=None):
|
||||
|
|
|
@ -107,7 +107,7 @@ then
|
|||
# utilise les scripts dans migrations/version/
|
||||
# pour mettre à jour notre base (en tant qu'utilisateur scodoc)
|
||||
export FLASK_ENV="production"
|
||||
su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade && flask clear-cache)" "$SCODOC_USER"
|
||||
su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade && flask clear-cache --sanitize)" "$SCODOC_USER"
|
||||
fi
|
||||
|
||||
# ------------ LOGROTATE
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
% ************************************************************
|
||||
% En-tête de l'avis
|
||||
% ************************************************************
|
||||
\begin{entete}{logos/logo_header}
|
||||
\begin{entete}{logos/header}
|
||||
\textbf{\Huge{Avis de Poursuites d'Etudes}} \\
|
||||
\ligne \\
|
||||
\normalsize{Département **DeptFullName**} \\
|
||||
|
|
|
@ -170,6 +170,11 @@ def import_scodoc7_dept(dept_id: str, dept_db_uri=None):
|
|||
logging.info(f"connecting to database {dept_db_uri}")
|
||||
cnx = psycopg2.connect(dept_db_uri)
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
# FIX : des dates aberrantes (dans le futur) peuvent tenir en SQL mais pas en Python
|
||||
cursor.execute(
|
||||
"""UPDATE scolar_events SET event_date='2021-09-30' WHERE event_date > '2200-01-01'"""
|
||||
)
|
||||
cnx.commit()
|
||||
# Create dept:
|
||||
dept = models.Departement(acronym=dept_id, description="migré de ScoDoc7")
|
||||
db.session.add(dept)
|
||||
|
@ -374,6 +379,8 @@ def convert_object(
|
|||
new_ref = id_from_scodoc7[old_ref]
|
||||
elif (not is_table) and table_name in {
|
||||
"scolog",
|
||||
"entreprise_correspondant",
|
||||
"entreprise_contact",
|
||||
"etud_annotations",
|
||||
"notes_notes_log",
|
||||
"scolar_news",
|
||||
|
|
Loading…
Reference in New Issue