This commit is contained in:
leonard_montalbano 2022-02-07 08:37:35 +01:00
commit fa5536ce2c
79 changed files with 1264 additions and 588 deletions

View File

@ -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,

View File

@ -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),

View File

@ -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.

View File

@ -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(

View File

@ -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 ---------

View File

@ -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

View File

@ -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:

View File

@ -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})

View File

@ -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,

178
app/models/config.py Normal file
View File

@ -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()

View File

@ -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)

View File

@ -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):

View File

@ -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):

View File

@ -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}"

View File

@ -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)

View File

@ -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_")
]
)

View File

@ -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"""

View File

@ -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
)
# ----------------------------------------------------------------------------------------

View File

@ -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(

View File

@ -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])

View File

@ -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.

View File

@ -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)

View File

@ -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",

View File

@ -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:

View File

@ -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

View File

@ -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,

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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):

View File

@ -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"])

View File

@ -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})

View File

@ -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)

View File

@ -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"])

View File

@ -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:

View File

@ -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"

View File

@ -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,
},
),

View File

@ -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"

View File

@ -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)

View File

@ -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,

View File

@ -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,

View File

@ -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"',

View File

@ -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(

View File

@ -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)

View File

@ -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,

View File

@ -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:

View File

@ -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"/>
&nbsp;<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>"""

View File

@ -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:

View File

@ -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,

View File

@ -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,

View File

@ -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):

View File

@ -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:

View File

@ -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)",

View File

@ -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 = []

View File

@ -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 (

View File

@ -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

View File

@ -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)

View File

@ -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.",

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 }}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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"},

View File

@ -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

View File

@ -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}")

View File

@ -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")

View File

@ -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")

View File

@ -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 ###

View File

@ -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 ###

View File

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

View File

@ -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):

View File

@ -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

View File

@ -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**} \\

View File

@ -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",