forked from ScoDoc/ScoDoc
Merge branch 'master' of https://scodoc.org/git/viennet/ScoDoc into refactor_nt
This commit is contained in:
commit
f14a14ee85
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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):
|
||||||
evals_poids[ue_poids.ue_id][ue_poids.evaluation_id] = ue_poids.poids
|
try:
|
||||||
|
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()
|
||||||
|
78
app/forms/main/config_apo.py
Normal file
78
app/forms/main/config_apo.py
Normal 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})
|
@ -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
192
app/models/config.py
Normal 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()
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -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}"
|
||||||
|
@ -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)."""
|
|
||||||
)
|
|
||||||
|
@ -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:
|
||||||
t[i] = results.etud_moy_ue[ue["id"]][etudid]
|
# temporaire pour 9.1.29 !
|
||||||
|
if ue["id"] in results.etud_moy_ue:
|
||||||
|
t[i] = results.etud_moy_ue[ue["id"]][etudid]
|
||||||
|
else:
|
||||||
|
t[i] = ""
|
||||||
# re-trie selon la nouvelle moyenne générale:
|
# 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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,16 +474,31 @@ 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
|
||||||
ues_matieres = ndb.SimpleDictFetch(
|
in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls
|
||||||
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
|
if in_use:
|
||||||
FROM notes_matieres mat, notes_ue ue
|
# matières du même semestre seulement
|
||||||
WHERE mat.ue_id = ue.id
|
ues_matieres = ndb.SimpleDictFetch(
|
||||||
AND ue.formation_id = %(formation_id)s
|
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
|
||||||
ORDER BY ue.numero, mat.numero
|
FROM notes_matieres mat, notes_ue ue
|
||||||
""",
|
WHERE mat.ue_id = ue.id
|
||||||
{"formation_id": formation_id},
|
AND ue.formation_id = %(formation_id)s
|
||||||
)
|
AND ue.semestre_idx = %(semestre_idx)s
|
||||||
|
ORDER BY ue.numero, mat.numero
|
||||||
|
""",
|
||||||
|
{"formation_id": formation_id, "semestre_idx": a_module.ue.semestre_idx},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# matières de la formation
|
||||||
|
ues_matieres = ndb.SimpleDictFetch(
|
||||||
|
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
|
||||||
|
FROM notes_matieres mat, notes_ue ue
|
||||||
|
WHERE mat.ue_id = ue.id
|
||||||
|
AND ue.formation_id = %(formation_id)s
|
||||||
|
ORDER BY ue.numero, mat.numero
|
||||||
|
""",
|
||||||
|
{"formation_id": formation_id},
|
||||||
|
)
|
||||||
mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres]
|
mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres]
|
||||||
ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres]
|
ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres]
|
||||||
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
|
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
|
||||||
@ -500,12 +517,25 @@ def module_edit(module_id=None):
|
|||||||
),
|
),
|
||||||
"""<h2>Modification du module %(titre)s""" % module,
|
"""<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"])
|
||||||
|
@ -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})
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
|
@ -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,7 +61,12 @@ 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:
|
||||||
liste = list_etuds_from_sem(src, sem)
|
if ignore_jury:
|
||||||
|
# liste de tous les inscrits au semestre (sans dems)
|
||||||
|
liste = list_inscrits(src["formsemestre_id"]).values()
|
||||||
|
else:
|
||||||
|
# liste des étudiants autorisés par le jury à s'inscrire ici
|
||||||
|
liste = list_etuds_from_sem(src, sem)
|
||||||
liste_filtree = []
|
liste_filtree = []
|
||||||
for e in liste:
|
for e in liste:
|
||||||
# Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src
|
# Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src
|
||||||
@ -125,7 +132,7 @@ def list_inscrits(formsemestre_id, with_dems=False):
|
|||||||
return inscr
|
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"/>
|
||||||
<a href="#help">aide</a>
|
<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>"""
|
||||||
|
@ -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,
|
||||||
|
@ -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:
|
||||||
|
@ -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)",
|
||||||
|
@ -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 = []
|
||||||
|
@ -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()
|
||||||
etudid = int(etudid) #
|
try:
|
||||||
|
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)
|
||||||
|
@ -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.",
|
||||||
|
@ -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;
|
||||||
|
23
app/templates/config_codes_decisions.html
Normal file
23
app/templates/config_codes_decisions.html
Normal 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 %}
|
@ -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 %}
|
||||||
|
@ -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>
|
@ -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"
|
||||||
|
@ -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}")
|
||||||
|
@ -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=[
|
||||||
|
@ -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")
|
||||||
|
@ -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 ###
|
22
scodoc.py
22
scodoc.py
@ -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()
|
||||||
|
@ -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é
|
||||||
|
Loading…
Reference in New Issue
Block a user