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

This commit is contained in:
Arthur ZHU 2022-02-28 18:58:59 +01:00
commit 4949fbda6b
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 ! else: # necessaire pour éviter bonus négatifs !
bonus_moy_arr = np.clip(bonus_moy_arr, 0.0, 20.0, out=bonus_moy_arr) 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) # en APC, bonus_moy_arr est (nb_etuds, nb_ues_non_bonus)
if self.formsemestre.formation.is_apc(): if self.formsemestre.formation.is_apc():
# Bonus sur les UE et None sur moyenne générale # Bonus sur les UE et None sur moyenne générale
@ -306,6 +310,47 @@ class BonusDirect(BonusSportAdditif):
proportion_point = 1.0 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): class BonusAmiens(BonusSportAdditif):
"""Bonus IUT Amiens pour les modules optionnels (sport, culture, ...). """Bonus IUT Amiens pour les modules optionnels (sport, culture, ...).
@ -705,6 +750,7 @@ class BonusRoanne(BonusSportAdditif):
seuil_moy_gen = 0.0 seuil_moy_gen = 0.0
bonus_max = 0.6 # plafonnement à 0.6 points bonus_max = 0.6 # plafonnement à 0.6 points
classic_use_bonus_ues = True # sur les UE, même en DUT et LP classic_use_bonus_ues = True # sur les UE, même en DUT et LP
proportion_point = 1
class BonusStDenis(BonusSportAdditif): class BonusStDenis(BonusSportAdditif):
@ -773,21 +819,19 @@ class BonusVilleAvray(BonusSport):
def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan): def compute_bonus(self, sem_modimpl_moys_inscrits, modimpl_coefs_etuds_no_nan):
"""calcul du bonus""" """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: # Calcule moyenne pondérée des notes de sport:
bonus_moy_arr = np.sum( bonus_moy_arr = np.sum(
sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1 sem_modimpl_moys_inscrits * modimpl_coefs_etuds_no_nan, axis=1
) / np.sum(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 < 10.0] = 0.0
bonus_moy_arr[bonus_moy_arr >= 12.0] = 0.2
bonus_moy_arr[bonus_moy_arr >= 16.0] = 0.3 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_additif(bonus_moy_arr)
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.
class BonusIUTV(BonusSportAdditif): class BonusIUTV(BonusSportAdditif):

View File

@ -30,8 +30,10 @@
import numpy as np import numpy as np
import pandas as pd 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 etud_moy_ue_df: pd.DataFrame, modimpl_coefs_df: pd.DataFrame
) -> pd.Series: ) -> pd.Series:
"""Calcule les moyennes générales indicatives de tous les étudiants """Calcule les moyennes générales indicatives de tous les étudiants
@ -48,6 +50,28 @@ def compute_sem_moys_apc(
return moy_gen 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): def comp_ranks_series(notes: pd.Series) -> (pd.Series, pd.Series):
"""Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur """Calcul rangs à partir d'une séries ("vecteur") de notes (index etudid, valeur
numérique) en tenant compte des ex-aequos. 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 import moy_ue, moy_sem, inscr_mod
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.comp.bonus_spo import BonusSport 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.models.ues import UniteEns
from app.scodoc.sco_codes_parcours import UE_SPORT 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 # Les coefficients d'UE ne sont pas utilisés en APC
self.etud_coef_ue_df = pd.DataFrame( 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 # --- Modules de MALUS sur les UEs
@ -103,8 +103,13 @@ class ResultatsSemestreBUT(NotesTableCompat):
# Moyenne générale indicative: # Moyenne générale indicative:
# (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte # (note: le bonus sport a déjà été appliqué aux moyennes d'UE, et impacte
# donc la moyenne indicative) # donc la moyenne indicative)
self.etud_moy_gen = moy_sem.compute_sem_moys_apc( # self.etud_moy_gen = moy_sem.compute_sem_moys_apc_using_coefs(
self.etud_moy_ue, self.modimpl_coefs_df # 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 # --- UE capitalisées
self.apply_capitalisation() self.apply_capitalisation()

View File

@ -9,18 +9,22 @@ from functools import cached_property
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from flask import g, flash, url_for
from app import log from app import log
from app.comp.aux_stats import StatsMoyenne from app.comp.aux_stats import StatsMoyenne
from app.comp import moy_sem from app.comp import moy_sem
from app.comp.res_cache import ResultatsCache from app.comp.res_cache import ResultatsCache
from app.comp import res_sem from app.comp import res_sem
from app.comp.moy_mod import ModuleImplResults from app.comp.moy_mod import ModuleImplResults
from app.models import FormSemestre, Identite, ModuleImpl from app.models import FormSemestre, FormSemestreUECoef
from app.models import FormSemestreUECoef from app.models import Identite
from app.models import ModuleImpl, ModuleImplInscription
from app.models.ues import UniteEns from app.models.ues import UniteEns
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_cache import ResultatsSemestreCache from app.scodoc.sco_cache import ResultatsSemestreCache
from app.scodoc.sco_codes_parcours import UE_SPORT, DEF from app.scodoc.sco_codes_parcours import UE_SPORT, DEF
from app.scodoc.sco_exceptions import ScoValueError
# Il faut bien distinguer # Il faut bien distinguer
# - ce qui est caché de façon persistente (via redis): # - ce qui est caché de façon persistente (via redis):
@ -191,7 +195,7 @@ class ResultatsSemestre(ResultatsCache):
if ue_cap["is_capitalized"]: if ue_cap["is_capitalized"]:
recompute_mg = True recompute_mg = True
coef = ue_cap["coef_ue"] 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_notes_ue += ue_cap["moy"] * coef
sum_coefs_ue += coef sum_coefs_ue += coef
@ -206,12 +210,18 @@ class ResultatsSemestre(ResultatsCache):
0.0, min(self.etud_moy_gen[etudid], 20.0) 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] capitalisations = self.validations.ue_capitalisees.loc[etudid]
if isinstance(capitalisations, pd.DataFrame): if isinstance(capitalisations, pd.DataFrame):
ue_cap = capitalisations[capitalisations["ue_code"] == ue.ue_code] 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 # si plusieurs fois capitalisée, prend le max
cap_idx = ue_cap["moy_ue"].values.argmax() cap_idx = ue_cap["moy_ue"].values.argmax()
ue_cap = ue_cap.iloc[cap_idx] ue_cap = ue_cap.iloc[cap_idx]
@ -219,8 +229,9 @@ class ResultatsSemestre(ResultatsCache):
if capitalisations["ue_code"] == ue.ue_code: if capitalisations["ue_code"] == ue.ue_code:
ue_cap = capitalisations ue_cap = capitalisations
else: else:
ue_cap = None return None
return ue_cap # 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: def get_etud_ue_status(self, etudid: int, ue_id: int) -> dict:
"""L'état de l'UE pour cet étudiant. """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] cur_moy_ue = self.etud_moy_ue[ue_id][etudid]
moy_ue = cur_moy_ue moy_ue = cur_moy_ue
is_capitalized = False # si l'UE prise en compte est une UE capitalisée is_capitalized = False # si l'UE prise en compte est une UE capitalisée
was_capitalized = ( # s'il y a precedemment une UE capitalisée (pas forcement meilleure):
False # s'il y a precedemment une UE capitalisée (pas forcement meilleure) was_capitalized = False
)
if etudid in self.validations.ue_capitalisees.index: if etudid in self.validations.ue_capitalisees.index:
ue_cap = self._get_etud_ue_cap(etudid, ue) ue_cap = self._get_etud_ue_cap(etudid, ue)
if ( if ue_cap and not np.isnan(ue_cap["moy_ue"]):
ue_cap is not None
and not ue_cap.empty
and not np.isnan(ue_cap["moy_ue"])
):
was_capitalized = True was_capitalized = True
if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue): if ue_cap["moy_ue"] > cur_moy_ue or np.isnan(cur_moy_ue):
moy_ue = ue_cap["moy_ue"] moy_ue = ue_cap["moy_ue"]
is_capitalized = True 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 { return {
"is_capitalized": is_capitalized, "is_capitalized": is_capitalized,
@ -385,21 +419,31 @@ class NotesTableCompat(ResultatsSemestre):
"""Stats (moy/min/max) sur la moyenne générale""" """Stats (moy/min/max) sur la moyenne générale"""
return StatsMoyenne(self.etud_moy_gen) 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. """Liste des UEs, ordonnée par numero.
Si filter_sport, retire les UE de type SPORT. Si filter_sport, retire les UE de type SPORT.
Résultat: liste de dicts { champs UE U stats moyenne UE } Résultat: liste de dicts { champs UE U stats moyenne UE }
""" """
ues = [] ues = self.formsemestre.query_ues(with_sport=not filter_sport)
for ue in self.formsemestre.query_ues(with_sport=not filter_sport): ues_dict = []
for ue in ues:
d = ue.to_dict() d = ue.to_dict()
if ue.type != UE_SPORT: if ue.type != UE_SPORT:
moys = self.etud_moy_ue[ue.id] moys = self.etud_moy_ue[ue.id]
else: else:
moys = None moys = None
d.update(StatsMoyenne(moys).to_dict()) d.update(StatsMoyenne(moys).to_dict())
ues.append(d) ues_dict.append(d)
return ues 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]: def get_modimpls_dict(self, ue_id=None) -> list[dict]:
"""Liste des modules pour une UE (ou toutes si ue_id==None), """Liste des modules pour une UE (ou toutes si ue_id==None),
@ -518,11 +562,15 @@ class NotesTableCompat(ResultatsSemestre):
return "" return ""
return ins.etat 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)""" """moyenne d'un étudiant dans une matière (ou NA si pas de notes)"""
if not self.moyennes_matieres: if not self.moyennes_matieres:
return "nd" 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: def get_etud_mod_moy(self, moduleimpl_id: int, etudid: int) -> float:
"""La moyenne de l'étudiant dans le moduleimpl """La moyenne de l'étudiant dans le moduleimpl

View File

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

View File

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

View File

@ -2,6 +2,7 @@
"""ScoDoc models: moduleimpls """ScoDoc models: moduleimpls
""" """
import pandas as pd import pandas as pd
import flask_sqlalchemy
from app import db from app import db
from app.comp import df_cache from app.comp import df_cache
@ -129,14 +130,36 @@ class ModuleImplInscription(db.Model):
) )
@classmethod @classmethod
def nb_inscriptions_dans_ue( def etud_modimpls_in_ue(
cls, formsemestre_id: int, etudid: int, ue_id: int cls, formsemestre_id: int, etudid: int, ue_id: int
) -> int: ) -> flask_sqlalchemy.BaseQuery:
"""Nombre de moduleimpls de l'UE auxquels l'étudiant est inscrit""" """moduleimpls de l'UE auxquels l'étudiant est inscrit"""
return ModuleImplInscription.query.filter( return ModuleImplInscription.query.filter(
ModuleImplInscription.etudid == etudid, ModuleImplInscription.etudid == etudid,
ModuleImplInscription.moduleimpl_id == ModuleImpl.id, ModuleImplInscription.moduleimpl_id == ModuleImpl.id,
ModuleImpl.formsemestre_id == formsemestre_id, ModuleImpl.formsemestre_id == formsemestre_id,
ModuleImpl.module_id == Module.id, ModuleImpl.module_id == Module.id,
Module.ue_id == ue_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 ''})>""" 'EXTERNE' if self.is_external else ''})>"""
def to_dict(self): 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 = dict(self.__dict__)
e.pop("_sa_instance_state", None) e.pop("_sa_instance_state", None)
# ScoDoc7 output_formators # ScoDoc7 output_formators
e["ue_id"] = self.id e["ue_id"] = self.id
e["numero"] = e["numero"] if e["numero"] else 0 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["coefficient"] = e["coefficient"] if e["coefficient"] else 0.0
e["code_apogee"] = e["code_apogee"] or "" # pas de None e["code_apogee"] = e["code_apogee"] or "" # pas de None
return e return e

View File

@ -30,7 +30,7 @@
import html import html
from flask import g from flask import render_template
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
@ -280,6 +280,9 @@ def sco_header(
if not no_side_bar: if not no_side_bar:
H.append(html_sidebar.sidebar()) H.append(html_sidebar.sidebar())
H.append("""<div id="gtrcontent">""") 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: # Barre menu semestre:
H.append(formsemestre_page_title()) H.append(formsemestre_page_title())

View File

@ -98,7 +98,7 @@ from chardet import detect as chardet_detect
from app import log from app import log
from app.comp import res_sem from app.comp import res_sem
from app.comp.res_common import NotesTableCompat from app.comp.res_common import NotesTableCompat
from app.models import FormSemestre from app.models import FormSemestre, Identite
from app.models.config import ScoDocSiteConfig from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
@ -111,7 +111,6 @@ from app.scodoc.sco_codes_parcours import (
NAR, NAR,
RAT, RAT,
) )
from app.scodoc import sco_cache
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
from app.scodoc import sco_etud from app.scodoc import sco_etud
@ -454,6 +453,12 @@ class ApoEtud(dict):
def comp_elt_semestre(self, nt, decision, etudid): def comp_elt_semestre(self, nt, decision, etudid):
"""Calcul résultat apo semestre""" """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 # resultat du semestre
decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"]) decision_apo = ScoDocSiteConfig.get_code_apo(decision["code"])
note = nt.get_etud_moy_gen(etudid) 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"] = {}
I["matieres_modules_capitalized"] = {} I["matieres_modules_capitalized"] = {}
for ue in ues: for ue in ues:
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
if ( if (
ModuleImplInscription.nb_inscriptions_dans_ue( ModuleImplInscription.nb_inscriptions_dans_ue(
formsemestre_id, etudid, ue["ue_id"] formsemestre_id, etudid, ue["ue_id"]
) )
== 0 == 0
): ) and not ue_status["is_capitalized"]:
# saute les UE où l'on est pas inscrit et n'avons pas de capitalisation
continue continue
u = ue.copy()
ue_status = nt.get_etud_ue_status(etudid, ue["ue_id"])
u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...} u["ue_status"] = ue_status # { 'moy', 'coef_ue', ...}
if ue["type"] != sco_codes_parcours.UE_SPORT: if ue["type"] != sco_codes_parcours.UE_SPORT:
u["cur_moy_ue_txt"] = scu.fmt_note(ue_status["cur_moy_ue"]) 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) intro_mail = sco_preferences.get_preference("bul_intro_mail", formsemestre_id)
if intro_mail: if intro_mail:
hea = intro_mail % { try:
"nomprenom": etud["nomprenom"], hea = intro_mail % {
"dept": dept, "nomprenom": etud["nomprenom"],
"webmaster": webmaster, "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: else:
hea = "" hea = ""

View File

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

View File

@ -282,7 +282,7 @@ class TypeParcours(object):
return [ return [
ue_status ue_status
for ue_status in ues_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 isinstance(ue_status["moy"], float)
and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"]) and ue_status["moy"] < self.get_barre_ue(ue_status["ue"]["type"])
] ]
@ -587,7 +587,7 @@ class ParcoursILEPS(TypeParcours):
# SESSION_ABBRV = 'A' # A1, A2, ... # SESSION_ABBRV = 'A' # A1, A2, ...
COMPENSATION_UE = False COMPENSATION_UE = False
UNUSED_CODES = set((ADC, ATT, ATB, ATJ)) 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 gen. pour validation semestre:
BARRE_MOY = 10.0 BARRE_MOY = 10.0
# Barre pour UE ILEPS: 8/20 pour UE standards ("fondamentales") # 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...") log("uploading anonymized dump...")
files = {"file": (ano_db_name + ".dump", dump)} files = {"file": (ano_db_name + ".dump", dump)}
r = requests.post( try:
scu.SCO_DUMP_UP_URL, r = requests.post(
files=files, scu.SCO_DUMP_UP_URL,
data={ files=files,
"dept_name": sco_preferences.get_preference("DeptName"), data={
"serial": _get_scodoc_serial(), "dept_name": sco_preferences.get_preference("DeptName"),
"sco_user": str(current_user), "serial": _get_scodoc_serial(),
"sent_by": sco_users.user_info(str(current_user))["nomcomplet"], "sco_user": str(current_user),
"sco_version": sco_version.SCOVERSION, "sent_by": sco_users.user_info(str(current_user))["nomcomplet"],
"sco_fullversion": scu.get_scodoc_version(), "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 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... # ne propose pas SAE et Ressources, sauf si déjà de ce type...
module_types = ( module_types = (
set(scu.ModuleType) - {scu.ModuleType.RESSOURCE, scu.ModuleType.SAE} 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 = [ descr = [
( (

View File

@ -29,7 +29,7 @@
""" """
import flask import flask
from flask import url_for, render_template from flask import flash, render_template, url_for
from flask import g, request from flask import g, request
from flask_login import current_user from flask_login import current_user
@ -89,6 +89,7 @@ _ueEditor = ndb.EditableTable(
input_formators={ input_formators={
"type": ndb.int_null_is_zero, "type": ndb.int_null_is_zero,
"is_external": ndb.bool_or_str, "is_external": ndb.bool_or_str,
"ects": ndb.float_null_is_null,
}, },
output_formators={ output_formators={
"numero": ndb.int_null_is_zero, "numero": ndb.int_null_is_zero,
@ -107,8 +108,6 @@ def ue_list(*args, **kw):
def do_ue_create(args): def do_ue_create(args):
"create an ue" "create an ue"
from app.scodoc import sco_formations
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
# check duplicates # check duplicates
ues = ue_list({"formation_id": args["formation_id"], "acronyme": args["acronyme"]}) 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é ! f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)""" (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 # create
ue_id = _ueEditor.create(cnx, args) ue_id = _ueEditor.create(cnx, args)
@ -128,6 +135,8 @@ def do_ue_create(args):
formation = Formation.query.get(args["formation_id"]) formation = Formation.query.get(args["formation_id"])
formation.invalidate_module_coefs() formation.invalidate_module_coefs()
# news # news
ue = UniteEns.query.get(ue_id)
flash(f"UE créée (code {ue.ue_code})")
formation = Formation.query.get(args["formation_id"]) formation = Formation.query.get(args["formation_id"])
sco_news.add( sco_news.add(
typ=sco_news.NEWS_FORM, 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", "type": "float",
"title": "ECTS", "title": "ECTS",
"explanation": "nombre de crédits 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"], "semestre_id": tf[2]["semestre_idx"],
}, },
) )
flash("UE créée")
else: else:
do_ue_edit(tf[2]) do_ue_edit(tf[2])
flash("UE modifiée")
return flask.redirect( return flask.redirect(
url_for( url_for(
"notes.ue_table", "notes.ue_table",
@ -746,6 +758,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
) )
) )
else: else:
H.append('<div class="formation_classic_infos">')
H.append( H.append(
_ue_table_ues( _ue_table_ues(
parcours, parcours,
@ -775,7 +788,7 @@ du programme" (menu "Semestre") si vous avez un semestre en cours);
</ul> </ul>
""" """
) )
H.append("</div>")
H.append("</div>") # formation_ue_list H.append("</div>") # formation_ue_list
if ues_externes: if ues_externes:
@ -924,10 +937,10 @@ def _ue_table_ues(
cur_ue_semestre_id = None cur_ue_semestre_id = None
iue = 0 iue = 0
for ue in ues: for ue in ues:
if ue["ects"]: if ue["ects"] is None:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
else:
ue["ects_str"] = "" ue["ects_str"] = ""
else:
ue["ects_str"] = ", %g ECTS" % ue["ects"]
if editable: if editable:
klass = "span_apo_edit" klass = "span_apo_edit"
else: 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é ! f"""Acronyme d'UE "{args['acronyme']}" déjà utilisé !
(chaque UE doit avoir un acronyme unique dans la formation)""" (chaque UE doit avoir un acronyme unique dans la formation)"""
) )
# On ne peut pas supprimer le code UE: # On ne peut pas supprimer le code UE:
if "ue_code" in args and not args["ue_code"]: if "ue_code" in args and not args["ue_code"]:
del 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 html_sco_header
from app.scodoc import sco_etud from app.scodoc import sco_etud
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import ScoException
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc import sco_preferences from app.scodoc import sco_preferences
@ -221,7 +222,10 @@ def search_etuds_infos(expnom=None, code_nip=None):
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
if expnom and not may_be_nip: if expnom and not may_be_nip:
expnom = expnom.upper() # les noms dans la BD sont en uppercase 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: else:
code_nip = code_nip or expnom code_nip = code_nip or expnom
if code_nip: if code_nip:

View File

@ -1078,7 +1078,7 @@ def formsemestre_status(formsemestre_id=None):
"</p>", "</p>",
] ]
if use_ue_coefs: if use_ue_coefs and not formsemestre.formation.is_apc():
H.append( H.append(
""" """
<p class="infop">utilise les coefficients d'UE pour calculer la moyenne générale.</p> <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: else:
H.append('<td colspan="%d"><em>en cours</em></td>') H.append('<td colspan="%d"><em>en cours</em></td>')
H.append('<td class="rcp_nonass">%s</td>' % ass) # abs H.append('<td class="rcp_nonass">%s</td>' % ass) # abs
# acronymes UEs auxquelles l'étudiant est inscrit: # acronymes UEs auxquelles l'étudiant est inscrit (ou capitalisé)
# XXX il est probable que l'on doive ici ajouter les
# XXX UE capitalisées
ues = nt.get_ues_stat_dict(filter_sport=True) ues = nt.get_ues_stat_dict(filter_sport=True)
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
etud_ue_status = {
ue["ue_id"]: nt.get_etud_ue_status(etudid, ue["ue_id"]) for ue in ues
}
ues = [ ues = [
ue ue
for ue in ues for ue in ues
if etud_est_inscrit_ue(cnx, etudid, sem["formsemestre_id"], ue["ue_id"]) 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: for ue in ues:
@ -644,7 +646,7 @@ def formsemestre_recap_parcours_table(
code = decisions_ue[ue["ue_id"]]["code"] code = decisions_ue[ue["ue_id"]]["code"]
else: else:
code = "" 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 "" moy_ue = ue_status["moy"] if ue_status else ""
explanation_ue = [] # list of strings explanation_ue = [] # list of strings
if code == ADM: 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 Le format est renseigné au moment de la lecture (select) ou de la création (create) de l'objet
""" """
self.logoname = secure_filename(logoname) self.logoname = secure_filename(logoname)
if not self.logoname:
self.logoname = "*** *** nom de logo invalide *** à changer ! *** ***"
self.scodoc_dept_id = dept_id self.scodoc_dept_id = dept_id
self.prefix = prefix or "" self.prefix = prefix or ""
if self.scodoc_dept_id: if self.scodoc_dept_id:
@ -276,7 +278,7 @@ class Logo:
if self.mm is None: if self.mm is None:
return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">' return f'<logo name="{self.logoname}" width="?? mm" height="?? mm">'
else: 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): def last_modified(self):
path = Path(self.filepath) path = Path(self.filepath)

View File

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

View File

@ -220,12 +220,16 @@ class ScolarsPageTemplate(PageTemplate):
PageTemplate.__init__(self, "ScolarsPageTemplate", [content]) PageTemplate.__init__(self, "ScolarsPageTemplate", [content])
self.logo = None self.logo = None
logo = find_logo( 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: if logo is None:
# Also try to use PV background # Also try to use PV background
logo = find_logo( 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: if logo is not None:
self.background_image_filename = logo.filepath self.background_image_filename = logo.filepath

View File

@ -1296,11 +1296,21 @@ class BasePreferences(object):
"labels": ["non", "oui"], "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", "bul_show_coef",
{ {
"initvalue": 1, "initvalue": 1,
"title": "Afficher coefficient des ue/modules sur les bulletins", "title": "Afficher coefficient des modules sur les bulletins",
"input_type": "boolcheckbox", "input_type": "boolcheckbox",
"category": "bul", "category": "bul",
"labels": ["non", "oui"], "labels": ["non", "oui"],

View File

@ -206,12 +206,18 @@ class CourrierIndividuelTemplate(PageTemplate):
background = find_logo( background = find_logo(
logoname="pvjury_background", logoname="pvjury_background",
dept_id=g.scodoc_dept_id, dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="pvjury_background",
dept_id=g.scodoc_dept_id,
prefix="", prefix="",
) )
else: else:
background = find_logo( background = find_logo(
logoname="letter_background", logoname="letter_background",
dept_id=g.scodoc_dept_id, dept_id=g.scodoc_dept_id,
) or find_logo(
logoname="letter_background",
dept_id=g.scodoc_dept_id,
prefix="", prefix="",
) )
if not self.background_image_filename and background is not None: 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", "") apo_emailperso = etud.get("mailperso", "")
if info["emailperso"] and not apo_emailperso: if info["emailperso"] and not apo_emailperso:
apo_emailperso = info["emailperso"] apo_emailperso = info["emailperso"]
if ( if import_email:
import_email if not "mail" in etud:
and info["email"] != etud["mail"] raise ScoValueError(
or info["emailperso"] != apo_emailperso "la réponse portail n'a pas le champs requis 'mail'"
): )
sco_etud.adresse_edit( if (
cnx, info["email"] != etud["mail"]
args={ or info["emailperso"] != apo_emailperso
"etudid": etudid, ):
"adresse_id": info["adresse_id"], sco_etud.adresse_edit(
"email": etud["mail"], cnx,
"emailperso": apo_emailperso, args={
}, "etudid": etudid,
) "adresse_id": info["adresse_id"],
# notifie seulement les changements d'adresse mail institutionnelle "email": etud["mail"],
if info["email"] != etud["mail"]: "emailperso": apo_emailperso,
changed_mails.append((info, etud["mail"])) },
)
# notifie seulement les changements d'adresse mail institutionnelle
if info["email"] != etud["mail"]:
changed_mails.append((info, etud["mail"]))
else: else:
unknowns.append(code_nip) unknowns.append(code_nip)
sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"]) sco_cache.invalidate_formsemestre(formsemestre_id=sem["formsemestre_id"])

View File

@ -50,7 +50,7 @@ import pydot
import requests import requests
from flask import g, request 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 config import Config
from app import log from app import log
@ -616,6 +616,16 @@ def bul_filename(sem, etud, format):
return filename 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 def sendCSVFile(data, filename): # DEPRECATED utiliser send_file
"""publication fichier CSV.""" """publication fichier CSV."""
return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True) return send_file(data, filename=filename, mime=CSV_MIMETYPE, attached=True)

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -61,12 +61,10 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
{% with messages = get_flashed_messages() %} {% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% for category, message in messages %}
{% for message in messages %} <div class="alert alert-info alert-{{ category }}" role="alert">{{ message }}</div>
<div class="alert alert-info" role="alert">{{ message }}</div> {% endfor %}
{% endfor %}
{% endif %}
{% endwith %} {% endwith %}
{# application content needs to be provided in the app_content block #} {# 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(", ") %} {% set virg = joiner(", ") %}
<span class="ue_code">( <span class="ue_code">(
{%- if ue.ue_code -%}{{ virg() }}code {{ue.ue_code}} {%- endif -%} {%- 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>
</span> </span>

View File

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

View File

@ -35,7 +35,7 @@ from operator import itemgetter
from xml.etree import ElementTree from xml.etree import ElementTree
import flask 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 import current_app, g, request
from flask_login import current_user from flask_login import current_user
from werkzeug.utils import redirect 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.scodoc import notesdb as ndb
from app import log, send_scodoc_alarm from app import log, send_scodoc_alarm
from app.scodoc import scolog
from app.scodoc.scolog import logdb 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.scodoc import html_sco_header
from app.pe import pe_view from app.pe import pe_view
from app.scodoc import sco_abs from app.scodoc import sco_abs
@ -2672,12 +2676,15 @@ def check_integrity_all():
def moduleimpl_list( def moduleimpl_list(
moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json" moduleimpl_id=None, formsemestre_id=None, module_id=None, format="json"
): ):
data = sco_moduleimpl.moduleimpl_list( try:
moduleimpl_id=moduleimpl_id, data = sco_moduleimpl.moduleimpl_list(
formsemestre_id=formsemestre_id, moduleimpl_id=moduleimpl_id,
module_id=module_id, formsemestre_id=formsemestre_id,
) module_id=module_id,
return scu.sendResult(data, format=format) )
return scu.sendResult(data, format=format)
except ScoException:
abort(404)
@bp.route("/do_moduleimpl_withmodule_list") # ancien nom @bp.route("/do_moduleimpl_withmodule_list") # ancien nom
@ -2686,7 +2693,7 @@ def moduleimpl_list(
@permission_required(Permission.ScoView) @permission_required(Permission.ScoView)
@scodoc7func @scodoc7func
def moduleimpl_withmodule_list( 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""" """API ScoDoc 7"""
data = sco_moduleimpl.moduleimpl_withmodule_list( 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 # stockée dans /opt/scodoc-data/config/logos donc servie manuellement ici
# from app.scodoc.sco_photos import _http_jpeg_file # 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: if logo is not None:
logo.select()
suffix = logo.suffix suffix = logo.suffix
if small: if small:
with PILImage.open(logo.filepath) as im: with PILImage.open(logo.filepath) as im:

View File

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