1
0
forked from ScoDoc/ScoDoc

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

This commit is contained in:
Emmanuel Viennet 2022-01-25 11:11:03 +01:00
commit f14a14ee85
37 changed files with 660 additions and 269 deletions

View File

@ -253,7 +253,7 @@ def create_app(config_class=DevConfig):
host_name = socket.gethostname() host_name = socket.gethostname()
mail_handler = ScoSMTPHandler( mail_handler = ScoSMTPHandler(
mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]), 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"], toaddrs=["exception@scodoc.org"],
subject="ScoDoc Exception", # unused see ScoSMTPHandler subject="ScoDoc Exception", # unused see ScoSMTPHandler
credentials=auth, credentials=auth,

View File

@ -8,7 +8,7 @@ def send_password_reset_email(user):
token = user.get_reset_password_token() token = user.get_reset_password_token()
send_email( send_email(
"[ScoDoc] Réinitialisation de votre mot de passe", "[ScoDoc] Réinitialisation de votre mot de passe",
sender=current_app.config["ADMINS"][0], sender=current_app.config["SCODOC_MAIL_FROM"],
recipients=[user.email], recipients=[user.email],
text_body=render_template("email/reset_password.txt", user=user, token=token), text_body=render_template("email/reset_password.txt", user=user, token=token),
html_body=render_template("email/reset_password.html", user=user, token=token), html_body=render_template("email/reset_password.html", user=user, token=token),

View File

@ -40,6 +40,7 @@ import pandas as pd
from app import db from app import db
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError
@dataclass @dataclass
@ -280,7 +281,11 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
for ue_poids in EvaluationUEPoids.query.join( for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation EvaluationUEPoids.evaluation
).filter_by(moduleimpl_id=moduleimpl_id): ).filter_by(moduleimpl_id=moduleimpl_id):
try:
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
except KeyError as exc:
pass # poids vers des UE qui n'existent plus ou sont dans un autre semestre...
# Initialise poids non enregistrés: # Initialise poids non enregistrés:
if np.isnan(evals_poids.values.flat).any(): if np.isnan(evals_poids.values.flat).any():
ue_coefs = modimpl.module.get_ue_coef_dict() ue_coefs = modimpl.module.get_ue_coef_dict()

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 CODE_STR_LEN = 16 # chaine pour les codes
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes 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 GROUPNAME_STR_LEN = 64
from app.models.raw_sql_init import create_database_functions from app.models.raw_sql_init import create_database_functions
from app.models.absences import Absence, AbsenceNotification, BilletAbsence from app.models.absences import Absence, AbsenceNotification, BilletAbsence
from app.models.config import ScoDocSiteConfig
from app.models.departements import Departement from app.models.departements import Departement
from app.models.etudiants import ( from app.models.etudiants import (
Identite, Identite,
@ -57,7 +57,7 @@ from app.models.notes import (
NotesNotes, NotesNotes,
NotesNotesLog, NotesNotesLog,
) )
from app.models.preferences import ScoPreference, ScoDocSiteConfig from app.models.preferences import ScoPreference
from app.models.but_refcomp import ( from app.models.but_refcomp import (
ApcReferentielCompetences, ApcReferentielCompetences,

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

@ -0,0 +1,192 @@
# -*- coding: UTF-8 -*
"""Model : site config WORK IN PROGRESS #WIP
"""
from app import db, log
from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError
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_class(cls, class_name):
"""Record bonus_sport config.
If class_name not defined, raise NameError
"""
if class_name not in cls.get_bonus_sport_class_names():
raise NameError("invalid class name for bonus_sport")
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c:
log("setting to " + class_name)
c.value = class_name
else:
c = ScoDocSiteConfig(cls.BONUS_SPORT, class_name)
db.session.add(c)
db.session.commit()
@classmethod
def get_bonus_sport_class_name(cls):
"""Get configured bonus function name, or None if None."""
klass = cls.get_bonus_sport_class_from_name()
if klass is None:
return ""
else:
return klass.name
@classmethod
def get_bonus_sport_class_from_name(cls, class_name=None):
"""returns bonus class with specified name.
If name not specified, return the configured function.
None if no bonus function configured.
Raises ScoValueError if class_name not found in module bonus_sport.
"""
if class_name is None:
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c is None:
return None
class_name = c.value
if class_name == "": # pas de bonus défini
return None
klass = bonus_spo.get_bonus_class_dict().get(class_name)
if klass is None:
raise ScoValueError(
f"""Fonction de calcul bonus sport inexistante: {class_name}.
(contacter votre administrateur local)."""
)
return klass
@classmethod
def get_bonus_sport_class_names(cls):
"""List available bonus class names
(starting with empty string to represent "no bonus function").
"""
return [""] + sorted(bonus_spo.get_bonus_class_dict().keys())
@classmethod
def get_bonus_sport_func(cls):
"""Fonction bonus_sport ScoDoc 7 XXX
Transitoire pour les tests durant la transition #sco92
"""
"""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.
"""
from app.scodoc import bonus_sport
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_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

@ -103,7 +103,16 @@ class Evaluation(db.Model):
Note: si les poids ne sont pas initialisés (poids par défaut), Note: si les poids ne sont pas initialisés (poids par défaut),
ils ne sont pas affichés. 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): class EvaluationUEPoids(db.Model):

View File

@ -1,6 +1,7 @@
"""ScoDoc 9 models : Formations """ScoDoc 9 models : Formations
""" """
import app
from app import db from app import db
from app.comp import df_cache from app.comp import df_cache
from app.models import SHORT_STR_LEN from app.models import SHORT_STR_LEN
@ -141,8 +142,7 @@ class Formation(db.Model):
db.session.add(ue) db.session.add(ue)
db.session.commit() db.session.commit()
if change: app.clear_scodoc_cache()
self.invalidate_module_coefs()
class Matiere(db.Model): class Matiere(db.Model):

View File

@ -287,7 +287,7 @@ class FormSemestre(db.Model):
self.date_fin.year})""" self.date_fin.year})"""
def titre_num(self) -> str: 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: if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
return self.titre return self.titre
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}" return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"

View File

@ -2,9 +2,8 @@
"""Model : preferences """Model : preferences
""" """
from app import db, log
from app.comp import bonus_spo from app import db
from app.scodoc.sco_exceptions import ScoValueError
class ScoPreference(db.Model): class ScoPreference(db.Model):
@ -19,128 +18,3 @@ class ScoPreference(db.Model):
name = db.Column(db.String(128), nullable=False, index=True) name = db.Column(db.String(128), nullable=False, index=True)
value = db.Column(db.Text()) value = db.Column(db.Text())
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id")) 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_class(cls, class_name):
"""Record bonus_sport config.
If class_name not defined, raise NameError
"""
if class_name not in cls.get_bonus_sport_class_names():
raise NameError("invalid class name for bonus_sport")
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c:
log("setting to " + class_name)
c.value = class_name
else:
c = ScoDocSiteConfig(cls.BONUS_SPORT, class_name)
db.session.add(c)
db.session.commit()
@classmethod
def get_bonus_sport_class_name(cls):
"""Get configured bonus function name, or None if None."""
klass = cls.get_bonus_sport_class_from_name()
if klass is None:
return ""
else:
return klass.name
@classmethod
def get_bonus_sport_class(cls):
"""Get configured bonus function, or None if None."""
return cls.get_bonus_sport_class_from_name()
@classmethod
def get_bonus_sport_class_from_name(cls, class_name=None):
"""returns bonus class with specified name.
If name not specified, return the configured function.
None if no bonus function configured.
Raises ScoValueError if class_name not found in module bonus_sport.
"""
if class_name is None:
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c is None:
return None
class_name = c.value
if class_name == "": # pas de bonus défini
return None
klass = bonus_spo.get_bonus_class_dict().get(class_name)
if klass is None:
raise ScoValueError(
f"""Fonction de calcul bonus sport inexistante: {class_name}.
(contacter votre administrateur local)."""
)
return klass
@classmethod
def get_bonus_sport_class_names(cls):
"""List available functions names
(starting with empty string to represent "no bonus function").
"""
return [""] + sorted(bonus_spo.get_bonus_class_dict().keys())
@classmethod
def get_bonus_sport_func(cls):
"""Fonction bonus_sport ScoDoc 7 XXX
Transitoire pour les tests durant la transition #sco92
"""
"""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.
"""
from app.scodoc import bonus_sport
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)."""
)

View File

@ -788,7 +788,12 @@ class NotesTable:
moy_ue_cap = ue_cap["moy"] moy_ue_cap = ue_cap["moy"]
mu["was_capitalized"] = True mu["was_capitalized"] = True
event_date = event_date or ue_cap["event_date"] 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 # meilleure UE capitalisée
event_date = ue_cap["event_date"] event_date = ue_cap["event_date"]
max_moy_ue = moy_ue_cap max_moy_ue = moy_ue_cap
@ -1329,7 +1334,11 @@ class NotesTable:
t[0] = results.etud_moy_gen[etudid] t[0] = results.etud_moy_gen[etudid]
for i, ue in enumerate(ues, start=1): for i, ue in enumerate(ues, start=1):
if ue["type"] != UE_SPORT: if ue["type"] != UE_SPORT:
# temporaire pour 9.1.29 !
if ue["id"] in results.etud_moy_ue:
t[i] = results.etud_moy_ue[ue["id"]][etudid] t[i] = results.etud_moy_ue[ue["id"]][etudid]
else:
t[i] = ""
# re-trie selon la nouvelle moyenne générale: # re-trie selon la nouvelle moyenne générale:
self.T.sort(key=self._row_key) self.T.sort(key=self._row_key)
# Remplace aussi le rang: # Remplace aussi le rang:

View File

@ -95,30 +95,21 @@ from flask import send_file
# Pour la détection auto de l'encodage des fichiers Apogée: # Pour la détection auto de l'encodage des fichiers Apogée:
from chardet import detect as chardet_detect from chardet import detect as chardet_detect
from app.models.config import ScoDocSiteConfig
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log from app import log
from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError from app.scodoc.sco_exceptions import ScoValueError, ScoFormatError
from app.scodoc.gen_tables import GenTable from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_vdi import ApoEtapeVDI from app.scodoc.sco_vdi import ApoEtapeVDI
from app.scodoc.sco_codes_parcours import code_semestre_validant from app.scodoc.sco_codes_parcours import code_semestre_validant
from app.scodoc.sco_codes_parcours import ( from app.scodoc.sco_codes_parcours import (
ADC,
ADJ,
ADM,
AJ,
ATB,
ATJ,
ATT,
CMP,
DEF, DEF,
DEM,
NAR, NAR,
RAT, RAT,
) )
from app.scodoc import sco_cache 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
from app.scodoc import sco_formsemestre_status
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
@ -132,24 +123,6 @@ APO_SEP = "\t"
APO_NEWLINE = "\r\n" 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): def _apo_fmt_note(note):
"Formatte une note pour Apogée (séparateur décimal: ',')" "Formatte une note pour Apogée (séparateur décimal: ',')"
if not note and isinstance(note, float): if not note and isinstance(note, float):
@ -449,7 +422,7 @@ class ApoEtud(dict):
N=_apo_fmt_note(ue_status["moy"]), N=_apo_fmt_note(ue_status["moy"]),
B=20, B=20,
J="", J="",
R=code_scodoc_to_apo(code_decision_ue), R=ScoDocSiteConfig.get_code_apo(code_decision_ue),
M="", M="",
) )
else: else:
@ -475,13 +448,9 @@ 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"""
# resultat du 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) note = nt.get_etud_moy_gen(etudid)
if ( if decision_apo == "DEF" or decision["code"] == DEM or decision["code"] == DEF:
decision_apo == "DEF"
or decision["code"] == "DEM"
or decision["code"] == DEF
):
note_str = "0,01" # note non nulle pour les démissionnaires note_str = "0,01" # note non nulle pour les démissionnaires
else: else:
note_str = _apo_fmt_note(note) note_str = _apo_fmt_note(note)
@ -520,21 +489,21 @@ class ApoEtud(dict):
# ou jury intermediaire et etudiant non redoublant... # ou jury intermediaire et etudiant non redoublant...
return self.comp_elt_semestre(cur_nt, cur_decision, etudid) 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_nt = sco_cache.NotesTableCache.get(autre_sem["formsemestre_id"])
autre_decision = autre_nt.get_etud_decision_sem(etudid) autre_decision = autre_nt.get_etud_decision_sem(etudid)
if not autre_decision: if not autre_decision:
# pas de decision dans l'autre => pas de résultat annuel # pas de decision dans l'autre => pas de résultat annuel
return VOID_APO_RES 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 ( if (
autre_decision_apo == "DEF" autre_decision_apo == "DEF"
or autre_decision["code"] == "DEM" or autre_decision["code"] == DEM
or autre_decision["code"] == DEF or autre_decision["code"] == DEF
) or ( ) or (
decision_apo == "DEF" decision_apo == "DEF"
or cur_decision["code"] == "DEM" or cur_decision["code"] == DEM
or cur_decision["code"] == DEF or cur_decision["code"] == DEF
): ):
note_str = "0,01" # note non nulle pour les démissionnaires note_str = "0,01" # note non nulle pour les démissionnaires

View File

@ -125,6 +125,7 @@ CMP = "CMP" # utile pour UE seulement (indique UE acquise car semestre acquis)
NAR = "NAR" NAR = "NAR"
RAT = "RAT" # en attente rattrapage, sera ATT dans Apogée 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) DEF = "DEF" # défaillance (n'est pas un code jury dans scodoc mais un état, comme inscrit ou demission)
DEM = "DEM"
# codes actions # codes actions
REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1) REDOANNEE = "REDOANNEE" # redouble annee (va en Sn-1)
@ -140,22 +141,26 @@ BUG = "BUG"
ALL = "ALL" ALL = "ALL"
# Explication des codes (de demestre ou d'UE)
CODES_EXPL = { CODES_EXPL = {
ADM: "Validé",
ADC: "Validé par compensation", ADC: "Validé par compensation",
ADJ: "Validé par le Jury", 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)", 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)", ATJ: "Décision en attente d'un autre semestre (assiduité insuffisante)",
AJ: "Ajourné", ATT: "Décision en attente d'un autre semestre (faute d'atteindre la moyenne)",
NAR: "Echec, non autorisé à redoubler", CMP: "Code UE acquise car semestre acquis",
RAT: "En attente d'un rattrapage",
DEF: "Défaillant", DEF: "Défaillant",
NAR: "Échec, non autorisé à redoubler",
RAT: "En attente d'un rattrapage",
} }
# Nota: ces explications sont personnalisables via le fichier # Nota: ces explications sont personnalisables via le fichier
# de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py # de config locale /opt/scodoc/var/scodoc/config/scodoc_local.py
# variable: CONFIG.CODES_EXP # 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_VALIDES = {ADM: True, ADC: True, ADJ: True} # semestre validé
CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente CODES_SEM_ATTENTES = {ATT: True, ATB: True, ATJ: True} # semestre en attente

View File

@ -32,15 +32,16 @@ import flask
from flask import url_for, render_template from flask import url_for, render_template
from flask import g, request from flask import g, request
from flask_login import current_user 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 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.notesdb as ndb
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType 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.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ( 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", "title": "Code Apogée",
"size": 25, "size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules", "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,7 +474,22 @@ def module_edit(module_id=None):
formation_id = module["formation_id"] formation_id = module["formation_id"]
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0] formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"]) parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
is_apc = parcours.APC_SAE 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( ues_matieres = ndb.SimpleDictFetch(
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id """SELECT ue.acronyme, mat.*, mat.id AS matiere_id
FROM notes_matieres mat, notes_ue ue FROM notes_matieres mat, notes_ue ue
@ -500,12 +517,25 @@ def module_edit(module_id=None):
), ),
"""<h2>Modification du module %(titre)s""" % module, """<h2>Modification du module %(titre)s""" % module,
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation, """ (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: if not unlocked:
H.append( H.append(
"""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>""" """<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 = [ descr = [
( (
@ -680,6 +710,13 @@ def module_edit(module_id=None):
else: else:
# l'UE peut changer # l'UE peut changer
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!") tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
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 # En APC, force le semestre égal à celui de l'UE
if is_apc: if is_apc:
selected_ue = UniteEns.query.get(tf[2]["ue_id"]) selected_ue = UniteEns.query.get(tf[2]["ue_id"])

View File

@ -1229,7 +1229,8 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
def edit_ue_set_code_apogee(id=None, value=None): def edit_ue_set_code_apogee(id=None, value=None):
"set UE code apogee" "set UE code apogee"
ue_id = id 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)) log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))
ues = ue_list(args={"ue_id": ue_id}) ues = ue_list(args={"ue_id": ue_id})

View File

@ -143,6 +143,7 @@ def evaluation_create_form(
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals: if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
vals["visibulletinlist"] = [] vals["visibulletinlist"] = []
# #
ue_coef_dict = {}
if is_apc: # BUT: poids vers les UE if is_apc: # BUT: poids vers les UE
ue_coef_dict = ModuleImpl.query.get(moduleimpl_id).module.get_ue_coef_dict() ue_coef_dict = ModuleImpl.query.get(moduleimpl_id).module.get_ue_coef_dict()
for ue in sem_ues: for ue in sem_ues:
@ -290,7 +291,10 @@ def evaluation_create_form(
"title": f"Poids {ue.acronyme}", "title": f"Poids {ue.acronyme}",
"size": 2, "size": 2,
"type": "float", "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, "allow_null": False,
}, },
), ),

View File

@ -36,15 +36,6 @@ class ScoException(Exception):
pass pass
class NoteProcessError(ScoException):
"misc errors in process"
pass
class InvalidEtudId(NoteProcessError):
pass
class InvalidNoteValue(ScoException): class InvalidNoteValue(ScoException):
pass pass
@ -56,6 +47,15 @@ class ScoValueError(ScoException):
self.dest_url = dest_url self.dest_url = dest_url
class NoteProcessError(ScoValueError):
"Valeurs notes invalides"
pass
class InvalidEtudId(NoteProcessError):
pass
class ScoFormatError(ScoValueError): class ScoFormatError(ScoValueError):
pass pass

View File

@ -748,7 +748,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
) )
# Choix code semestre: # 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 ! codes.sort() # fortuitement, cet ordre convient bien !
H.append( H.append(

View File

@ -87,7 +87,7 @@ groupEditor = ndb.EditableTable(
group_list = groupEditor.list group_list = groupEditor.list
def get_group(group_id): def get_group(group_id: int):
"""Returns group object, with partition""" """Returns group object, with partition"""
r = ndb.SimpleDictFetch( r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.* """SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
@ -687,6 +687,11 @@ def setGroups(
group_id = fs[0].strip() group_id = fs[0].strip()
if not group_id: if not group_id:
continue 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) group = get_group(group_id)
# Anciens membres du groupe: # Anciens membres du groupe:
old_members = get_group_members(group_id) old_members = get_group_members(group_id)

View File

@ -49,9 +49,11 @@ from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import ScoValueError 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. """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. 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) src_sems = list_source_sems(sem, delai=delai)
inscrits = list_inscrits(sem["formsemestre_id"]) inscrits = list_inscrits(sem["formsemestre_id"])
@ -59,6 +61,11 @@ def list_authorized_etuds_by_sem(sem, delai=274):
candidats = {} # etudid : etud (tous les etudiants candidats) candidats = {} # etudid : etud (tous les etudiants candidats)
nb = 0 # debug nb = 0 # debug
for src in src_sems: for src in src_sems:
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 = list_etuds_from_sem(src, sem)
liste_filtree = [] liste_filtree = []
for e in liste: for e in liste:
@ -125,7 +132,7 @@ def list_inscrits(formsemestre_id, with_dems=False):
return inscr 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.""" """Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
target = dst["semestre_id"] target = dst["semestre_id"]
dpv = sco_pvjury.dict_pvjury(src["formsemestre_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 """Liste des semestres sources
sem est le semestre destination sem est le semestre destination
""" """
@ -265,6 +272,7 @@ def formsemestre_inscr_passage(
inscrit_groupes=False, inscrit_groupes=False,
submitted=False, submitted=False,
dialog_confirmed=False, dialog_confirmed=False,
ignore_jury=False,
): ):
"""Form. pour inscription des etudiants d'un semestre dans un autre """Form. pour inscription des etudiants d'un semestre dans un autre
(donné par formsemestre_id). (donné par formsemestre_id).
@ -280,6 +288,7 @@ def formsemestre_inscr_passage(
""" """
inscrit_groupes = int(inscrit_groupes) inscrit_groupes = int(inscrit_groupes)
ignore_jury = int(ignore_jury)
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# -- check lock # -- check lock
if not sem["etat"]: if not sem["etat"]:
@ -295,7 +304,9 @@ def formsemestre_inscr_passage(
elif etuds and isinstance(etuds[0], str): elif etuds and isinstance(etuds[0], str):
etuds = [int(x) for x in etuds] 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) etuds_set = set(etuds)
candidats_set = set(candidats) candidats_set = set(candidats)
inscrits_set = set(inscrits) inscrits_set = set(inscrits)
@ -323,6 +334,7 @@ def formsemestre_inscr_passage(
candidats_non_inscrits, candidats_non_inscrits,
inscrits_ailleurs, inscrits_ailleurs,
inscrit_groupes=inscrit_groupes, inscrit_groupes=inscrit_groupes,
ignore_jury=ignore_jury,
) )
else: else:
if not dialog_confirmed: if not dialog_confirmed:
@ -363,6 +375,7 @@ def formsemestre_inscr_passage(
"formsemestre_id": formsemestre_id, "formsemestre_id": formsemestre_id,
"etuds": ",".join([str(x) for x in etuds]), "etuds": ",".join([str(x) for x in etuds]),
"inscrit_groupes": inscrit_groupes, "inscrit_groupes": inscrit_groupes,
"ignore_jury": ignore_jury,
"submitted": 1, "submitted": 1,
}, },
) )
@ -411,18 +424,23 @@ def build_page(
candidats_non_inscrits, candidats_non_inscrits,
inscrits_ailleurs, inscrits_ailleurs,
inscrit_groupes=False, inscrit_groupes=False,
ignore_jury=False,
): ):
inscrit_groupes = int(inscrit_groupes) inscrit_groupes = int(inscrit_groupes)
ignore_jury = int(ignore_jury)
if inscrit_groupes: if inscrit_groupes:
inscrit_groupes_checked = " checked" inscrit_groupes_checked = " checked"
else: else:
inscrit_groupes_checked = "" inscrit_groupes_checked = ""
if ignore_jury:
ignore_jury_checked = " checked"
else:
ignore_jury_checked = ""
H = [ H = [
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
"Passages dans le semestre", with_page_header=False "Passages dans le semestre", 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="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>
<input type="submit" name="submitted" value="Appliquer les modifications"/> <input type="submit" name="submitted" value="Appliquer les modifications"/>
&nbsp;<a href="#help">aide</a> &nbsp;<a href="#help">aide</a>
@ -430,6 +448,8 @@ def build_page(
% sem, # " % sem, # "
"""<input name="inscrit_groupes" type="checkbox" value="1" %s>inscrire aux mêmes groupes</input>""" """<input name="inscrit_groupes" type="checkbox" value="1" %s>inscrire aux mêmes groupes</input>"""
% inscrit_groupes_checked, % 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 """<div class="pas_recap">Actuellement <span id="nbinscrits">%s</span> inscrits
et %d candidats supplémentaires et %d candidats supplémentaires
</div>""" </div>"""

View File

@ -401,7 +401,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
eval_index = len(mod_evals) - 1 eval_index = len(mod_evals) - 1
first_eval = True first_eval = True
for eval in mod_evals: 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( etat = sco_evaluations.do_evaluation_etat(
eval["evaluation_id"], eval["evaluation_id"],
partition_id=partition_id, partition_id=partition_id,

View File

@ -169,7 +169,9 @@ def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2):
if doc: if doc:
break break
if not doc: 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)) etuds = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req))
# Filtre sur annee inscription Apogee: # Filtre sur annee inscription Apogee:

View File

@ -111,8 +111,9 @@ get_base_preferences(formsemestre_id)
""" """
import flask import flask
from flask import g, url_for, request from flask import g, request, current_app
from flask_login import current_user
# from flask_login import current_user
from app.models import Departement from app.models import Departement
from app.scodoc import sco_cache from app.scodoc import sco_cache
@ -1537,7 +1538,7 @@ class BasePreferences(object):
( (
"email_from_addr", "email_from_addr",
{ {
"initvalue": "noreply@scodoc.example.com", "initvalue": current_app.config["SCODOC_MAIL_FROM"],
"title": "adresse mail origine", "title": "adresse mail origine",
"size": 40, "size": 40,
"explanation": "adresse expéditeur pour les envois par mails (bulletins)", "explanation": "adresse expéditeur pour les envois par mails (bulletins)",

View File

@ -566,7 +566,7 @@ def formsemestre_pvjury(formsemestre_id, format="html", publish=True):
if "prev_decision" in row and row["prev_decision"]: if "prev_decision" in row and row["prev_decision"]:
counts[row["prev_decision"]] += 0 counts[row["prev_decision"]] += 0
# Légende des codes # Légende des codes
codes = list(counts.keys()) # sco_codes_parcours.CODES_EXPL.keys() codes = list(counts.keys())
codes.sort() codes.sort()
H.append("<h3>Explication des codes</h3>") H.append("<h3>Explication des codes</h3>")
lines = [] lines = []

View File

@ -153,7 +153,10 @@ def _check_notes(notes, evaluation, mod):
for (etudid, note) in notes: for (etudid, note) in notes:
note = str(note).strip().upper() note = str(note).strip().upper()
try:
etudid = int(etudid) # etudid = int(etudid) #
except ValueError as exc:
raise ScoValueError(f"Code étudiant ({etudid}) invalide")
if note[:3] == "DEM": if note[:3] == "DEM":
continue # skip ! continue # skip !
if note: if note:
@ -487,10 +490,10 @@ def notes_add(
} }
for (etudid, value) in notes: for (etudid, value) in notes:
if check_inscription and (etudid not in inscrits): if check_inscription and (etudid not in inscrits):
raise NoteProcessError("etudiant non inscrit dans ce module") raise NoteProcessError(f"etudiant {etudid} non inscrit dans ce module")
if not ((value is None) or (type(value) == type(1.0))): if (value is not None) and not isinstance(value, float):
raise NoteProcessError( raise NoteProcessError(
"etudiant %s: valeur de note invalide (%s)" % (etudid, value) f"etudiant {etudid}: valeur de note invalide ({value})"
) )
# Recherche notes existantes # Recherche notes existantes
notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id) notes_db = sco_evaluation_db.do_evaluation_get_all_notes(evaluation_id)

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) sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
r = ndb.SimpleDictFetch( 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, FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
notes_modules mod, identite i, "user" u notes_modules mod, identite i, "user" u
WHERE mi.id = e.moduleimpl_id WHERE mi.id = e.moduleimpl_id
@ -202,6 +202,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
"value", "value",
"user_name", "user_name",
"titre", "titre",
"evaluation_id",
"description", "description",
"jour", "jour",
"comment", "comment",
@ -214,6 +215,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
"value": "Note", "value": "Note",
"comment": "Remarque", "comment": "Remarque",
"user_name": "Enseignant", "user_name": "Enseignant",
"evaluation_id": "evaluation_id",
"titre": "Module", "titre": "Module",
"description": "Evaluation", "description": "Evaluation",
"jour": "Date éval.", "jour": "Date éval.",

View File

@ -1513,6 +1513,16 @@ table.moduleimpl_evaluations td.eval_poids {
color:rgb(0, 0, 255); 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 */ /* Formulaire edition des partitions */
form#editpart table { form#editpart table {
border: 1px solid gray; border: 1px solid gray;

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

@ -93,6 +93,8 @@
<div class="sco_help">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements):</div> <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()")}} {{ 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> <h1>Bibliothèque de logos</h1>
{% for dept_entry in form.depts.entries %} {% for dept_entry in form.depts.entries %}
{% set dept_form = dept_entry.form %} {% set dept_form = dept_entry.form %}

View File

@ -24,4 +24,24 @@
<a href="https://scodoc.org/BUT" target="_blank">la documentation</a>. <a href="https://scodoc.org/BUT" target="_blank">la documentation</a>.
</p> </p>
{%endif%} {%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> </div>

View File

@ -290,13 +290,17 @@ def formsemestre_bulletinetud(
if etudid: if etudid:
etud = models.Identite.query.get_or_404(etudid) etud = models.Identite.query.get_or_404(etudid)
elif code_nip: elif code_nip:
etud = models.Identite.query.filter_by( etud = (
code_nip=str(code_nip) models.Identite.query.filter_by(code_nip=str(code_nip))
).first_or_404() .filter_by(dept_id=formsemestre.dept_id)
.first_or_404()
)
elif code_ine: elif code_ine:
etud = models.Identite.query.filter_by( etud = (
code_ine=str(code_ine) models.Identite.query.filter_by(code_ine=str(code_ine))
).first_or_404() .filter_by(dept_id=formsemestre.dept_id)
.first_or_404()
)
else: else:
raise ScoValueError( raise ScoValueError(
"Paramètre manquant: spécifier code_nip ou etudid ou code_ine" "Paramètre manquant: spécifier code_nip ou etudid ou code_ine"

View File

@ -33,49 +33,38 @@ Emmanuel Viennet, 2021
import datetime import datetime
import io import io
import wtforms.validators
from app.auth.models import User
import os
import flask import flask
from flask import abort, flash, url_for, redirect, render_template, send_file from flask import abort, flash, url_for, redirect, render_template, send_file
from flask import request from flask import request
from flask.app import Flask
import flask_login import flask_login
from flask_login.utils import login_required, current_user from flask_login.utils import login_required, current_user
from flask_wtf import FlaskForm from PIL import Image as PILImage
from flask_wtf.file import FileField, FileAllowed
from werkzeug.exceptions import BadRequest, NotFound 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
import app
from app import db from app import db
from app.auth.models import User
from app.forms.main import config_forms from app.forms.main import config_forms
from app.forms.main.create_dept import CreateDeptForm 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 Departement, Identite
from app.models import departements from app.models import departements
from app.models import FormSemestre, FormSemestreInscription from app.models import FormSemestre, FormSemestreInscription
import sco_version from app.models import ScoDocSiteConfig
from app.scodoc import sco_logos from app.scodoc import sco_codes_parcours, sco_logos
from app.scodoc import sco_find_etud from app.scodoc import sco_find_etud
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
from app.decorators import ( from app.decorators import (
admin_required, admin_required,
scodoc7func, scodoc7func,
scodoc, scodoc,
permission_required_compat_scodoc7,
permission_required,
) )
from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.views import scodoc_bp as bp from app.views import scodoc_bp as bp
import sco_version
from PIL import Image as PILImage
@bp.route("/") @bp.route("/")
@ -133,6 +122,28 @@ def toggle_dept_vis(dept_id):
return redirect(url_for("scodoc.index")) 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"]) @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required @login_required
def table_etud_in_accessible_depts(): def table_etud_in_accessible_depts():
@ -257,14 +268,16 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True):
suffix = logo.suffix suffix = logo.suffix
if small: if small:
with PILImage.open(logo.filepath) as im: 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) # 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 fmt = { # adapt suffix to be compliant with PIL save format
"PNG": "PNG", "PNG": "PNG",
"JPG": "JPEG", "JPG": "JPEG",
"JPEG": "JPEG", "JPEG": "JPEG",
}[suffix.upper()] }[suffix.upper()]
if fmt == "JPEG":
im = im.convert("RGB")
im.thumbnail(SMALL_SIZE)
stream = io.BytesIO()
im.save(stream, fmt) im.save(stream, fmt)
stream.seek(0) stream.seek(0)
return send_file(stream, mimetype=f"image/{fmt}") return send_file(stream, mimetype=f"image/{fmt}")

View File

@ -81,7 +81,7 @@ _l = _
class ChangePasswordForm(FlaskForm): class ChangePasswordForm(FlaskForm):
user_name = HiddenField() user_name = HiddenField()
old_password = PasswordField(_l("Identifiez-vous")) 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( bis_password = PasswordField(
_l("Répéter"), _l("Répéter"),
validators=[ validators=[

View File

@ -26,6 +26,9 @@ class Config:
SCODOC_ADMIN_LOGIN = os.environ.get("SCODOC_ADMIN_LOGIN") or "admin" SCODOC_ADMIN_LOGIN = os.environ.get("SCODOC_ADMIN_LOGIN") or "admin"
ADMINS = [SCODOC_ADMIN_MAIL] ADMINS = [SCODOC_ADMIN_MAIL]
SCODOC_ERR_MAIL = os.environ.get("SCODOC_ERR_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") BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL")
SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc") SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc")
SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "/opt/scodoc-data") 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

@ -289,20 +289,28 @@ def user_role(username, dept_acronym=None, add_role_name=None, remove_role_name=
db.session.commit() db.session.commit()
def abort_if_false(ctx, param, value):
if not value:
ctx.abort()
@app.cli.command() @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") @click.argument("dept")
def delete_dept(dept): # delete-dept def delete_dept(dept): # delete-dept
"""Delete existing departement""" """Delete existing departement"""
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
from app.scodoc import sco_dept 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() db.reflect()
ndb.open_db_connection() ndb.open_db_connection()
d = models.Departement.query.filter_by(acronym=dept).first() d = models.Departement.query.filter_by(acronym=dept).first()

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}") logging.info(f"connecting to database {dept_db_uri}")
cnx = psycopg2.connect(dept_db_uri) cnx = psycopg2.connect(dept_db_uri)
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) 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: # Create dept:
dept = models.Departement(acronym=dept_id, description="migré de ScoDoc7") dept = models.Departement(acronym=dept_id, description="migré de ScoDoc7")
db.session.add(dept) db.session.add(dept)
@ -374,6 +379,8 @@ def convert_object(
new_ref = id_from_scodoc7[old_ref] new_ref = id_from_scodoc7[old_ref]
elif (not is_table) and table_name in { elif (not is_table) and table_name in {
"scolog", "scolog",
"entreprise_correspondant",
"entreprise_contact",
"etud_annotations", "etud_annotations",
"notes_notes_log", "notes_notes_log",
"scolar_news", "scolar_news",
@ -389,7 +396,6 @@ def convert_object(
new_ref = None new_ref = None
elif is_table and table_name in { elif is_table and table_name in {
"notes_semset_formsemestre", "notes_semset_formsemestre",
"entreprise_contact",
}: }:
# pour anciennes installs où des relations n'avait pas été déclarées clés étrangères # pour anciennes installs où des relations n'avait pas été déclarées clés étrangères
# eg: notes_semset_formsemestre.semset_id n'était pas une clé # eg: notes_semset_formsemestre.semset_id n'était pas une clé