1
0
forked from ScoDoc/ScoDoc

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

This commit is contained in:
Emmanuel Viennet 2022-02-28 15:22:02 +01:00
commit df9ec49568
35 changed files with 413 additions and 160 deletions

View File

@ -228,6 +228,10 @@ class BonusSportAdditif(BonusSport):
else: # necessaire pour éviter bonus négatifs !
bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr)
self.bonus_additif(bonus_moy_arr)
def bonus_additif(self, bonus_moy_arr: np.array):
"Set bonus_ues et bonus_moy_gen"
# en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
if self.formsemestre.formation.is_apc():
# Bonus sur les UE et None sur moyenne générale
@ -306,6 +310,47 @@ class BonusDirect(BonusSportAdditif):
proportion_point = 1.0
class BonusAisneStQuentin(BonusSportAdditif):
"""Calcul bonus modules optionels (sport, culture), règle IUT Aisne St Quentin
<p>Les étudiants de l'IUT peuvent suivre des enseignements optionnels
de l'Université de St Quentin non rattachés à une unité d'enseignement.
</p>
<ul>
<li>Si la note est >= 10 et < 12.1, bonus de 0.1 point</li>
<li>Si la note est >= 12.1 et < 14.1, bonus de 0.2 point</li>
<li>Si la note est >= 14.1 et < 16.1, bonus de 0.3 point</li>
<li>Si la note est >= 16.1 et < 18.1, bonus de 0.4 point</li>
<li>Si la note est >= 18.1, bonus de 0.5 point</li>
</ul>
<p>
Ce bonus s'ajoute à la moyenne générale du semestre déjà obtenue par
l'étudiant (en BUT, s'ajoute à la moyenne de chaque UE).
</p>
"""
name = "bonus_iutstq"
displayed_name = "IUT de Saint-Quentin"
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 18.1] = 0.5
bonus_moy_arr[bonus_moy_arr >= 16.1] = 0.4
bonus_moy_arr[bonus_moy_arr >= 14.1] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.1] = 0.2
bonus_moy_arr[bonus_moy_arr >= 10] = 0.1
self.bonus_additif(bonus_moy_arr)
class BonusAmiens(BonusSportAdditif):
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
@ -705,6 +750,7 @@ class BonusRoanne(BonusSportAdditif):
seuil_moy_gen = 0.0
bonus_max = 0.6 # plafonnement à 0.6 points
classic_use_bonus_ues = True # sur les UE, même en DUT et LP
proportion_point = 1
class BonusStDenis(BonusSportAdditif):
@ -773,21 +819,19 @@ class BonusVilleAvray(BonusSport):
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus"""
if 0 in sem_modimpl_moys_inscrits.shape:
# pas d'étudiants ou pas d'UE ou pas de module...
return
# Calcule moyenne pondérée des notes de sport:
bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(modimpl_coefs_etuds_no_nan, axis=1)
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
bonus_moy_arr[bonus_moy_arr < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
bonus_moy_arr[bonus_moy_arr >= 10.0] = 0.1
# Bonus moyenne générale, et 0 sur les UE
self.bonus_moy_gen = pd.Series(bonus_moy_arr, index=self.etuds_idx, dtype=float)
if self.bonus_max is not None:
# Seuil: bonus (sur moy. gen.) limité à bonus_max points
self.bonus_moy_gen = self.bonus_moy_gen.clip(upper=self.bonus_max)
# Laisse bonus_ues à None, en APC le bonus moy. gen. sera réparti sur les UEs.
self.bonus_additif(bonus_moy_arr)
class BonusIUTV(BonusSportAdditif):

View File

@ -30,8 +30,10 @@
import numpy as np
import pandas as pd
from flask import flash
def compute_sem_moys_apc(
def compute_sem_moys_apc_using_coefs(
etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
@ -48,6 +50,28 @@ def compute_sem_moys_apc(
return moy_gen
def compute_sem_moys_apc_using_ects(
etud_moy_ue_df: pd.DataFrame, ects: list, formation_id=None
) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants
= moyenne des moyennes d'UE, pondérée par leurs ECTS.
etud_moy_ue_df: DataFrame, colonnes ue_id, lignes etudid
ects: liste de floats ou None, 1 par UE
Result: panda Series, index etudid, valeur float (moyenne générale)
"""
try:
moy_gen = (etud_moy_ue_df * ects).sum(axis=1) / sum(ects)
except TypeError:
if None in ects:
flash("""Calcul moyenne générale impossible: ECTS des UE manquants !""")
moy_gen = pd.Series(np.NaN, index=etud_moy_ue_df.index)
else:
raise
return moy_gen
def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
numérique) en tenant compte des ex-aequos.

View File

@ -14,7 +14,7 @@ from app import log
from app.comp import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport
from app.models import ScoDocSiteConfig
from app.models import ScoDocSiteConfig, formsemestre
from app.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT
@ -73,7 +73,7 @@ class ResultatsSemestreBUT(NotesTableCompat):
)
# Les coefficients d'UE ne sont pas utilisés en APC
self.etud_coef_ue_df = pd.DataFrame(
1.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
0.0, index=self.etud_moy_ue.index, columns=self.etud_moy_ue.columns
)
# --- Modules de MALUS sur les UEs
@ -103,8 +103,13 @@ class ResultatsSemestreBUT(NotesTableCompat):
# Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
# donc la moyenne indicative)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc(
self.etud_moy_ue, self.modimpl_coefs_df
# self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
# self.etud_moy_ue, self.modimpl_coefs_df
# )
self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_ects(
self.etud_moy_ue,
[ue.ects for ue in self.ues if ue.type != UE_SPORT],
formation_id=self.formsemestre.formation_id,
)
# --- UE capitalisées
self.apply_capitalisation()

View File

@ -9,18 +9,22 @@ from functools import cached_property
import numpy as np
import pandas as pd
from flask import g, flash, url_for
from app import log
from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache
from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl
from app.models import FormSemestreUECoef
from app.models import FormSemestre, FormSemestreUECoef
from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis):
@ -191,7 +195,7 @@ class ResultatsSemestre(ResultatsCache):
if ue_cap["is_capitalized"]:
recompute_mg = True
coef = ue_cap["coef_ue"]
if not np.isnan(ue_cap["moy"]):
if not np.isnan(ue_cap["moy"]) and coef:
sum_notes_ue += ue_cap["moy"] * coef
sum_coefs_ue += coef
@ -206,12 +210,18 @@ class ResultatsSemestre(ResultatsCache):
0.0, min(self.etud_moy_gen[etudid], 20.0)
)
def _get_etud_ue_cap(self, etudid, ue):
""""""
def _get_etud_ue_cap(self, etudid: int, ue: UniteEns) -> dict:
"""Donne les informations sur la capitalisation de l'UE ue pour cet étudiant.
Résultat:
Si pas capitalisée: None
Si capitalisée: un dict, avec les colonnes de validation.
"""
capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code]
if isinstance(ue_cap, pd.DataFrame) and not ue_cap.empty:
if ue_cap.empty:
return None
if isinstance(ue_cap, pd.DataFrame):
# si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx]
@ -219,8 +229,9 @@ class ResultatsSemestre(ResultatsCache):
if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations
else:
ue_cap = None
return ue_cap
return None
# converti la Series en dict, afin que les np.int64 reviennent en int
return ue_cap.to_dict()
def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant.
@ -248,22 +259,45 @@ class ResultatsSemestre(ResultatsCache):
cur_moy_ue = self.etud_moy_ue[ue_id][etudid]
moy_ue = cur_moy_ue
is_capitalized = False # si l'UE prise en compte est une UE capitalisée
was_capitalized = (
False # s'il y a precedemment une UE capitalisée (pas forcement meilleure)
)
# s'il y a precedemment une UE capitalisée (pas forcement meilleure):
was_capitalized = False
if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue)
if (
ue_cap is not None
and not ue_cap.empty
and not np.isnan(ue_cap["moy_ue"])
):
if ue_cap and not np.isnan(ue_cap["moy_ue"]):
was_capitalized = True
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
moy_ue = ue_cap["moy_ue"]
is_capitalized = True
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
# Coef l'UE dans le semestre courant:
if self.is_apc:
# utilise les ECTS comme coef.
coef_ue = ue.ects
else:
# formations classiques
coef_ue = self.etud_coef_ue_df[ue_id][etudid]
if (not coef_ue) and is_capitalized: # étudiant non inscrit dans l'UE courante
if self.is_apc:
# Coefs de l'UE capitalisée en formation APC: donné par ses ECTS
ue_capitalized = UniteEns.query.get(ue_cap["ue_id"])
coef_ue = ue_capitalized.ects
if coef_ue is None:
orig_sem = FormSemestre.query.get(ue_cap["formsemestre_id"])
raise ScoValueError(
f"""L'UE capitalisée {ue_capitalized.acronyme}
du semestre {orig_sem.titre_annee()}
n'a pas d'indication d'ECTS.
Corrigez ou faite corriger le programme
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=ue_capitalized.formation_id)}">via cette page</a>.
"""
)
else:
# Coefs de l'UE capitalisée en formation classique:
# va chercher le coef dans le semestre d'origine
coef_ue = ModuleImplInscription.sum_coefs_modimpl_ue(
ue_cap["formsemestre_id"], etudid, ue_cap["ue_id"]
)
return {
"is_capitalized": is_capitalized,
@ -385,21 +419,31 @@ class NotesTableCompat(ResultatsSemestre):
"""Stats (moy/min/max) sur la moyenne générale"""
return StatsMoyenne(self.etud_moy_gen)
def get_ues_stat_dict(self, filter_sport=False): # was get_ues()
def get_ues_stat_dict(
self, filter_sport=False, check_apc_ects=True
) -> list[dict]: # was get_ues()
"""Liste des UEs, ordonnée par numero.
Si filter_sport, retire les UE de type SPORT.
Résultat: liste de dicts { champs UE U stats moyenne UE }
"""
ues = []
for ue in self.formsemestre.query_ues(with_sport=not filter_sport):
ues = self.formsemestre.query_ues(with_sport=not filter_sport)
ues_dict = []
for ue in ues:
d = ue.to_dict()
if ue.type != UE_SPORT:
moys = self.etud_moy_ue[ue.id]
else:
moys = None
d.update(StatsMoyenne(moys).to_dict())
ues.append(d)
return ues
ues_dict.append(d)
if check_apc_ects and self.is_apc and not hasattr(g, "checked_apc_ects"):
g.checked_apc_ects = True
if None in [ue.ects for ue in ues if ue.type != UE_SPORT]:
flash(
"""Calcul moyenne générale impossible: ECTS des UE manquants !""",
category="danger",
)
return ues_dict
def get_modimpls_dict(self, ue_id=None) -> list[dict]:
"""Liste des modules pour une UE (ou toutes si ue_id==None),
@ -518,11 +562,15 @@ class NotesTableCompat(ResultatsSemestre):
return ""
return ins.etat
def get_etud_mat_moy(self, matiere_id, etudid):
def get_etud_mat_moy(self, matiere_id: int, etudid: int) -> str:
"""moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
if not self.moyennes_matieres:
return "nd"
return self.moyennes_matieres[matiere_id][etudid]
return (
self.moyennes_matieres[matiere_id].get(etudid, "-")
if matiere_id in self.moyennes_matieres
else "-"
)
def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl

View File

@ -193,7 +193,7 @@ def scodoc7func(func):
# necessary for db ids and boolean values
try:
v = int(v)
except ValueError:
except (ValueError, TypeError):
pass
pos_arg_values.append(v)
# current_app.logger.info("pos_arg_values=%s" % pos_arg_values)

View File

@ -30,17 +30,15 @@ Formulaires configuration logos
Contrib @jmp, dec 21
"""
import re
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
from wtforms import SubmitField, FormField, validators, FieldList
from wtforms import ValidationError
from wtforms.fields.simple import StringField, HiddenField
from app import AccessDenied
from app.models import Departement
from app.models import ScoDocSiteConfig
from app.scodoc import sco_logos, html_sco_header
from app.scodoc import sco_utils as scu
from app.scodoc.sco_config_actions import (
@ -49,10 +47,11 @@ from app.scodoc.sco_config_actions import (
LogoInsert,
)
from flask_login import current_user
from app.scodoc import sco_utils as scu
from app.scodoc.sco_logos import find_logo
JAVASCRIPTS = html_sco_header.BOOTSTRAP_MULTISELECT_JS + []
CSSSTYLES = html_sco_header.BOOTSTRAP_MULTISELECT_CSS
@ -111,6 +110,15 @@ def dept_key_to_id(dept_key):
return dept_key
def logo_name_validator(message=None):
def validate_logo_name(form, field):
name = field.data if field.data else ""
if not scu.is_valid_filename(name):
raise ValidationError(message)
return validate_logo_name
class AddLogoForm(FlaskForm):
"""Formulaire permettant l'ajout d'un logo (dans un département)"""
@ -118,11 +126,7 @@ class AddLogoForm(FlaskForm):
name = StringField(
label="Nom",
validators=[
validators.regexp(
r"^[a-zA-Z0-9-_]*$",
re.IGNORECASE,
"Ne doit comporter que lettres, chiffres, _ ou -",
),
logo_name_validator("Nom de logo invalide (alphanumérique, _)"),
validators.Length(
max=20, message="Un nom ne doit pas dépasser 20 caractères"
),
@ -373,11 +377,11 @@ def config_logos():
if action:
action.execute()
flash(action.message)
return redirect(
url_for(
"scodoc.configure_logos",
)
)
return redirect(url_for("scodoc.configure_logos"))
else:
if not form.validate():
scu.flash_errors(form)
return render_template(
"config_logos.html",
scodoc_dept=None,

View File

@ -2,6 +2,7 @@
"""ScoDoc models: moduleimpls
"""
import pandas as pd
import flask_sqlalchemy
from app import db
from app.comp import df_cache
@ -129,14 +130,36 @@ class ModuleImplInscription(db.Model):
)
@classmethod
def nb_inscriptions_dans_ue(
def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
) -> flask_sqlalchemy.BaseQuery:
"""moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return ModuleImplInscription.query.filter(
ModuleImplInscription.etudid == etudid,
ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
ModuleImpl.formsemestre_id == formsemestre_id,
ModuleImpl.module_id == Module.id,
Module.ue_id == ue_id,
).count()
)
@classmethod
def nb_inscriptions_dans_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id).count()
@classmethod
def sum_coefs_modimpl_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int
) -> float:
"""Somme des coefficients des modules auxquels l'étudiant est inscrit
dans l'UE du semestre indiqué.
N'utilise que les coefficients, donc inadapté aux formations APC.
"""
return sum(
[
inscr.modimpl.module.coefficient
for inscr in cls.etud_modimpls_in_ue(formsemestre_id, etudid, ue_id)
]
)

View File

@ -54,13 +54,15 @@ class UniteEns(db.Model):
'EXTERNE' if self.is_external else ''})>"""
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""
"""as a dict, with the same conversions as in ScoDoc7
(except ECTS: keep None)
"""
e = dict(self.__dict__)
e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators
e["ue_id"] = self.id
e["numero"] = e["numero"] if e["numero"] else 0
e["ects"] = e["ects"] if e["ects"] else 0.0
e["ects"] = e["ects"]
e["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None
return e

View File

@ -30,7 +30,7 @@
import html
from flask import g
from flask import render_template
from flask import request
from flask_login import current_user
@ -280,6 +280,9 @@ def sco_header(
if not no_side_bar:
H.append(html_sidebar.sidebar())
H.append("""<div id="gtrcontent">""")
# En attendant le replacement complet de cette fonction,
# inclusion ici des messages flask
H.append(render_template("flashed_messages.html"))
#
# Barre menu semestre:
H.append(formsemestre_page_title())

View File

@ -98,7 +98,7 @@ from chardet import detect as chardet_detect
from app import log
from app.comp import res_sem
from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre
from app.models import FormSemestre, Identite
from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
@ -111,7 +111,6 @@ from app.scodoc.sco_codes_parcours import (
NAR,
RAT,
)
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_etud
@ -454,6 +453,12 @@ class ApoEtud(dict):
def comp_elt_semestre(self, nt, decision, etudid):
"""Calcul résultat apo semestre"""
if decision is None:
etud = Identite.query.get(etudid)
nomprenom = etud.nomprenom if etud else "(inconnu)"
raise ScoValueError(
f"decision absente pour l'étudiant {nomprenom} ({etudid})"
)
# resultat du semestre
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
note = nt.get_etud_moy_gen(etudid)

View File

@ -268,15 +268,17 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
I["matieres_modules"] = {}
I["matieres_modules_capitalized"] = {}
for ue in ues:
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if (
ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"]
)
== 0
):
) and not ue_status["is_capitalized"]:
# saute les UE où l'on est pas inscrit et n'avons pas de capitalisation
continue
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
if ue["type"] != sco_codes_parcours.UE_SPORT:
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"])
@ -1018,11 +1020,16 @@ def mail_bulletin(formsemestre_id, I, pdfdata, filename, recipient_addr):
intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id)
if intro_mail:
hea = intro_mail % {
"nomprenom": etud["nomprenom"],
"dept": dept,
"webmaster": webmaster,
}
try:
hea = intro_mail % {
"nomprenom": etud["nomprenom"],
"dept": dept,
"webmaster": webmaster,
}
except KeyError as e:
raise ScoValueError(
"format 'Message d'accompagnement' (bul_intro_mail) invalide, revoir les réglages dans les préférences"
)
else:
hea = ""

View File

@ -285,7 +285,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
)
with_col_moypromo = prefs["bul_show_moypromo"]
with_col_rang = prefs["bul_show_rangs"]
with_col_coef = prefs["bul_show_coef"]
with_col_coef = prefs["bul_show_coef"] or prefs["bul_show_ue_coef"]
with_col_ects = prefs["bul_show_ects"]
col_keys = ["titre", "module"] # noms des colonnes à afficher
@ -410,7 +410,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
# Chaque UE:
for ue in I["ues"]:
ue_type = None
coef_ue = ue["coef_ue_txt"]
coef_ue = ue["coef_ue_txt"] if prefs["bul_show_ue_coef"] else ""
ue_descr = ue["ue_descr_txt"]
rowstyle = ""
plusminus = minuslink #
@ -593,7 +593,7 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
"_titre_colspan": 2,
"rang": mod["mod_rang_txt"], # vide si pas option rang
"note": mod["mod_moy_txt"],
"coef": mod["mod_coef_txt"],
"coef": mod["mod_coef_txt"] if prefs["bul_show_coef"] else "",
"abs": mod.get(
"mod_abs_txt", ""
), # absent si pas option show abs module
@ -657,7 +657,9 @@ class BulletinGeneratorStandard(sco_bulletins_generator.BulletinGenerator):
eval_style = ""
t = {
"module": '<bullet indent="2mm">&bull;</bullet>&nbsp;' + e["name"],
"coef": "<i>" + e["coef_txt"] + "</i>",
"coef": ("<i>" + e["coef_txt"] + "</i>")
if prefs["bul_show_coef"]
else "",
"_hidden": hidden,
"_module_target": e["target_html"],
# '_module_help' : ,

View File

@ -282,7 +282,7 @@ class TypeParcours(object):
return [
ue_status
for ue_status in ues_status
if ue_status["coef_ue"] > 0
if ue_status["coef_ue"]
and isinstance(ue_status["moy"], float)
and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
]
@ -587,7 +587,7 @@ class ParcoursILEPS(TypeParcours):
# SESSION_ABBRV = 'A' # A1, A2, ...
COMPENSATION_UE = False
UNUSED_CODES = set((ADC, ATT, ATB, ATJ))
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE]
ALLOWED_UE_TYPES = [UE_STANDARD, UE_OPTIONNELLE, UE_SPORT]
# Barre moy gen. pour validation semestre:
BARRE_MOY = 10.0
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales")

View File

@ -186,18 +186,28 @@ def _send_db(ano_db_name):
log("uploading anonymized dump...")
files = {"file": (ano_db_name + ".dump", dump)}
r = requests.post(
scu.SCO_DUMP_UP_URL,
files=files,
data={
"dept_name": sco_preferences.get_preference("DeptName"),
"serial": _get_scodoc_serial(),
"sco_user": str(current_user),
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
"sco_version": sco_version.SCOVERSION,
"sco_fullversion": scu.get_scodoc_version(),
},
)
try:
r = requests.post(
scu.SCO_DUMP_UP_URL,
files=files,
data={
"dept_name": sco_preferences.get_preference("DeptName"),
"serial": _get_scodoc_serial(),
"sco_user": str(current_user),
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
"sco_version": sco_version.SCOVERSION,
"sco_fullversion": scu.get_scodoc_version(),
},
)
except requests.exceptions.ConnectionError as exc:
raise ScoValueError(
"""
Impossible de joindre le serveur d'assistance (scodoc.org).
Veuillez contacter le service informatique de votre établissement pour
corriger la configuration de ScoDoc. Dans la plupart des cas, il
s'agit d'un proxy mal configuré.
"""
) from exc
return r

View File

@ -551,7 +551,11 @@ def module_edit(module_id=None):
# ne propose pas SAE et Ressources, sauf si déjà de ce type...
module_types = (
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE}
) | {a_module.module_type or scu.ModuleType.STANDARD}
) | {
scu.ModuleType(a_module.module_type)
if a_module.module_type
else scu.ModuleType.STANDARD
}
descr = [
(

View File

@ -29,7 +29,7 @@
"""
import flask
from flask import url_for, render_template
from flask import flash, render_template, url_for
from flask import g, request
from flask_login import current_user
@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable(
input_formators={
"type": ndb.int_null_is_zero,
"is_external": ndb.bool_or_str,
"ects": ndb.float_null_is_null,
},
output_formators={
"numero": ndb.int_null_is_zero,
@ -107,8 +108,6 @@ def ue_list(*args, **kw):
def do_ue_create(args):
"create an ue"
from app.scodoc import sco_formations
cnx = ndb.GetDBConnexion()
# check duplicates
ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]})
@ -117,6 +116,14 @@ def do_ue_create(args):
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)"""
)
if not "ue_code" in args:
# évite les conflits de code
while True:
cursor = db.session.execute("select notes_newid_ucod();")
code = cursor.fetchone()[0]
if UniteEns.query.filter_by(ue_code=code).count() == 0:
break
args["ue_code"] = code
# create
ue_id = _ueEditor.create(cnx, args)
@ -128,6 +135,8 @@ def do_ue_create(args):
formation = Formation.query.get(args["formation_id"])
formation.invalidate_module_coefs()
# news
ue = UniteEns.query.get(ue_id)
flash(f"UE créée (code {ue.ue_code})")
formation = Formation.query.get(args["formation_id"])
sco_news.add(
typ=sco_news.NEWS_FORM,
@ -339,6 +348,7 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"type": "float",
"title": "ECTS",
"explanation": "nombre de crédits ECTS",
"allow_null": not is_apc, # ects requis en APC
},
),
(
@ -462,8 +472,10 @@ def ue_edit(ue_id=None, create=False, formation_id=None, default_semestre_idx=No
"semestre_id": tf[2]["semestre_idx"],
},
)
flash("UE créée")
else:
do_ue_edit(tf[2])
flash("UE modifiée")
return flask.redirect(
url_for(
"notes.ue_table",
@ -746,6 +758,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
)
)
else:
H.append('<div class="formation_classic_infos">')
H.append(
_ue_table_ues(
parcours,
@ -775,7 +788,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</ul>
"""
)
H.append("</div>")
H.append("</div>") # formation_ue_list
if ues_externes:
@ -924,10 +937,10 @@ def _ue_table_ues(
cur_ue_semestre_id = None
iue = 0
for ue in ues:
if ue["ects"]:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
else:
if ue["ects"] is None:
ue["ects_str"] = ""
else:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
if editable:
klass = "span_apo_edit"
else:
@ -1288,7 +1301,6 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)"""
)
# On ne peut pas supprimer le code UE:
if "ue_code" in args and not args["ue_code"]:
del args["ue_code"]

View File

@ -39,6 +39,7 @@ from app.scodoc.gen_tables import GenTable
from app.scodoc import html_sco_header
from app.scodoc import sco_etud
from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences
@ -221,7 +222,10 @@ def search_etuds_infos(expnom=None, code_nip=None):
cnx = ndb.GetDBConnexion()
if expnom and not may_be_nip:
expnom = expnom.upper() # les noms dans la BD sont en uppercase
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
try:
etuds = sco_etud.etudident_list(cnx, args={"nom": expnom}, test="~")
except ScoException:
etuds = []
else:
code_nip = code_nip or expnom
if code_nip:

View File

@ -1078,7 +1078,7 @@ def formsemestre_status(formsemestre_id=None):
"</p>",
]
if use_ue_coefs:
if use_ue_coefs and not formsemestre.formation.is_apc():
H.append(
"""
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p>

View File

@ -585,15 +585,17 @@ def formsemestre_recap_parcours_table(
else:
H.append('<td colspan="%d"><em>en cours</em></td>')
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
# acronymes UEs auxquelles l'étudiant est inscrit:
# XXX il est probable que l'on doive ici ajouter les
# XXX UE capitalisées
# acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
ues = nt.get_ues_stat_dict(filter_sport=True)
cnx = ndb.GetDBConnexion()
etud_ue_status = {
ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues
}
ues = [
ue
for ue in ues
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"])
or etud_ue_status[ue["ue_id"]]["is_capitalized"]
]
for ue in ues:
@ -644,7 +646,7 @@ def formsemestre_recap_parcours_table(
code = decisions_ue[ue["ue_id"]]["code"]
else:
code = ""
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
ue_status = etud_ue_status[ue["ue_id"]]
moy_ue = ue_status["moy"] if ue_status else ""
explanation_ue = [] # list of strings
if code == ADM:

View File

@ -151,6 +151,8 @@ class Logo:
Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet
"""
self.logoname = secure_filename(logoname)
if not self.logoname:
self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
self.scodoc_dept_id = dept_id
self.prefix = prefix or ""
if self.scodoc_dept_id:
@ -276,7 +278,7 @@ class Logo:
if self.mm is None:
return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">'
else:
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm"">'
return f'<logo name="{self.logoname}" width="{self.mm[0]}mm">'
def last_modified(self):
path = Path(self.filepath)

View File

@ -139,9 +139,7 @@ class SituationEtudParcoursGeneric(object):
# pour le DUT, le dernier est toujours S4.
# Ici: terminal si semestre == NB_SEM ou bien semestre_id==-1
# (licences et autres formations en 1 seule session))
self.semestre_non_terminal = (
self.sem["semestre_id"] != self.parcours.NB_SEM
) # True | False
self.semestre_non_terminal = self.sem["semestre_id"] != self.parcours.NB_SEM
if self.sem["semestre_id"] == NO_SEMESTRE_ID:
self.semestre_non_terminal = False
# Liste des semestres du parcours de cet étudiant:

View File

@ -220,12 +220,16 @@ class ScolarsPageTemplate(PageTemplate):
PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
self.logo = None
logo = find_logo(
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=None
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id
) or find_logo(
logoname="bul_pdf_background", dept_id=g.scodoc_dept_id, prefix=""
)
if logo is None:
# Also try to use PV background
logo = find_logo(
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=None
logoname="letter_background", dept_id=g.scodoc_dept_id
) or find_logo(
logoname="letter_background", dept_id=g.scodoc_dept_id, prefix=""
)
if logo is not None:
self.background_image_filename = logo.filepath

View File

@ -1296,11 +1296,21 @@ class BasePreferences(object):
"labels": ["non", "oui"],
},
),
(
"bul_show_ue_coef",
{
"initvalue": 1,
"title": "Afficher coefficient des UE sur les bulletins",
"input_type": "boolcheckbox",
"category": "bul",
"labels": ["non", "oui"],
},
),
(
"bul_show_coef",
{
"initvalue": 1,
"title": "Afficher coefficient des ue/modules sur les bulletins",
"title": "Afficher coefficient des modules sur les bulletins",
"input_type": "boolcheckbox",
"category": "bul",
"labels": ["non", "oui"],

View File

@ -206,12 +206,18 @@ class CourrierIndividuelTemplate(PageTemplate):
background = find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
else:
background = find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
prefix="",
)
if not self.background_image_filename and background is not None:

View File

@ -854,23 +854,27 @@ def formsemestre_import_etud_admission(
apo_emailperso = etud.get("mailperso", "")
if info["emailperso"] and not apo_emailperso:
apo_emailperso = info["emailperso"]
if (
import_email
and info["email"] != etud["mail"]
or info["emailperso"] != apo_emailperso
):
sco_etud.adresse_edit(
cnx,
args={
"etudid": etudid,
"adresse_id": info["adresse_id"],
"email": etud["mail"],
"emailperso": apo_emailperso,
},
)
# notifie seulement les changements d'adresse mail institutionnelle
if info["email"] != etud["mail"]:
changed_mails.append((info, etud["mail"]))
if import_email:
if not "mail" in etud:
raise ScoValueError(
"la réponse portail n'a pas le champs requis 'mail'"
)
if (
info["email"] != etud["mail"]
or info["emailperso"] != apo_emailperso
):
sco_etud.adresse_edit(
cnx,
args={
"etudid": etudid,
"adresse_id": info["adresse_id"],
"email": etud["mail"],
"emailperso": apo_emailperso,
},
)
# notifie seulement les changements d'adresse mail institutionnelle
if info["email"] != etud["mail"]:
changed_mails.append((info, etud["mail"]))
else:
unknowns.append(code_nip)
sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"])

View File

@ -50,7 +50,7 @@ import pydot
import requests
from flask import g, request
from flask import url_for, make_response, jsonify
from flask import flash, url_for, make_response, jsonify
from config import Config
from app import log
@ -616,6 +616,16 @@ def bul_filename(sem, etud, format):
return filename
def flash_errors(form):
"""Flashes form errors (version sommaire)"""
for field, errors in form.errors.items():
flash(
"Erreur: voir le champs %s" % (getattr(form, field).label.text,),
"warning",
)
# see https://getbootstrap.com/docs/4.0/components/alerts/
def sendCSVFile(data, filename): # DEPRECATED utiliser send_file
"""publication fichier CSV."""
return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True)

View File

@ -138,7 +138,7 @@ div.head_message {
border-radius: 8px;
font-family : arial, verdana, sans-serif ;
font-weight: bold;
width: 40%;
width: 70%;
text-align: center;
}
@ -287,15 +287,15 @@ div.logo-insidebar {
width: 75px; /* la marge fait 130px */
}
div.logo-logo {
margin-left: -5px;
text-align: center ;
}
div.logo-logo img {
box-sizing: content-box;
margin-top: -10px;
width: 128px;
margin-top: 10px; /* -10px */
width: 135px; /* 128px */
padding-right: 5px;
margin-left: -75px;
}
div.sidebar-bottom {
margin-top: 10px;
@ -1671,7 +1671,10 @@ div.formation_list_modules ul.notes_module_list {
padding-top: 5px;
padding-bottom: 5px;
}
span.missing_ue_ects {
color: red;
font-weight: bold;
}
li.module_malus span.formation_module_tit {
color: red;
font-weight: bold;
@ -1703,8 +1706,11 @@ ul.notes_ue_list {
padding-bottom: 1em;
font-weight: bold;
}
.formation_classic_infos ul.notes_ue_list {
padding-top: 0px;
}
li.notes_ue_list {
.formation_classic_infos li.notes_ue_list {
margin-top: 9px;
list-style-type: none;
border: 1px solid maroon;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -57,12 +57,10 @@
{% block content %}
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-info" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endwith %}
{# application content needs to be provided in the app_content block #}

View File

@ -0,0 +1,9 @@
{# Message flask : utilisé uniquement par les anciennes pages ScoDoc #}
{# -*- mode: jinja-html -*- #}
<div class="head_message_container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="head_message alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endwith %}
</div>

View File

@ -38,7 +38,8 @@
{% set virg = joiner(", ") %}
<span class="ue_code">(
{%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%}
{{ virg() }}{{ue.ects or 0}} ECTS)
{{ virg() }}{{ue.ects if ue.ects is not none
else '<span class="missing_ue_ects">aucun</span>'|safe}} ECTS)
</span>
</span>

View File

@ -23,12 +23,10 @@
<div id="gtrcontent" class="gtrcontent">
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-info" role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
{% endfor %}
{% endwith %}
</div>
{% if sco.sem %}

View File

@ -35,7 +35,7 @@ from operator import itemgetter
from xml.etree import ElementTree
import flask
from flask import flash, jsonify, render_template, url_for
from flask import abort, flash, jsonify, render_template, url_for
from flask import current_app, g, request
from flask_login import current_user
from werkzeug.utils import redirect
@ -68,10 +68,14 @@ from app.scodoc import sco_utils as scu
from app.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm
from app.scodoc import scolog
from app.scodoc.scolog import logdb
from app.scodoc.sco_exceptions import AccessDenied, ScoValueError, ScoInvalidIdType
from app.scodoc.sco_exceptions import (
AccessDenied,
ScoException,
ScoValueError,
ScoInvalidIdType,
)
from app.scodoc import html_sco_header
from app.pe import pe_view
from app.scodoc import sco_abs
@ -2672,12 +2676,15 @@ def check_integrity_all():
def moduleimpl_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
):
data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=moduleimpl_id,
formsemestre_id=formsemestre_id,
module_id=module_id,
)
return scu.sendResult(data, format=format)
try:
data = sco_moduleimpl.moduleimpl_list(
moduleimpl_id=moduleimpl_id,
formsemestre_id=formsemestre_id,
module_id=module_id,
)
return scu.sendResult(data, format=format)
except ScoException:
abort(404)
@bp.route("/do_moduleimpl_withmodule_list") # ancien nom
@ -2686,7 +2693,7 @@ def moduleimpl_list(
@permission_required(Permission.ScoView)
@scodoc7func
def moduleimpl_withmodule_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None
moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
):
"""API ScoDoc 7"""
data = sco_moduleimpl.moduleimpl_withmodule_list(

View File

@ -304,8 +304,9 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True):
# stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici
# from app.scodoc.sco_photos import _http_jpeg_file
logo = sco_logos.find_logo(name, dept_id, strict).select()
logo = sco_logos.find_logo(name, dept_id, strict)
if logo is not None:
logo.select()
suffix = logo.suffix
if small:
with PILImage.open(logo.filepath) as im:

View File

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