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()
mail_handler = ScoSMTPHandler(
mailhost=(app.config["MAIL_SERVER"], app.config["MAIL_PORT"]),
fromaddr="no-reply@" + app.config["MAIL_SERVER"],
fromaddr=app.config["SCODOC_MAIL_FROM"],
toaddrs=["exception@scodoc.org"],
subject="ScoDoc Exception", # unused see ScoSMTPHandler
credentials=auth,

View File

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

View File

@ -40,6 +40,7 @@ import pandas as pd
from app import db
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu
from app.scodoc.sco_exceptions import ScoValueError
@dataclass
@ -280,7 +281,11 @@ def load_evaluations_poids(moduleimpl_id: int) -> tuple[pd.DataFrame, list]:
for ue_poids in EvaluationUEPoids.query.join(
EvaluationUEPoids.evaluation
).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:
if np.isnan(evals_poids.values.flat).any():
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
SHORT_STR_LEN = 32 # courtes chaine, eg acronymes
APO_CODE_STR_LEN = 24 # nb de car max d'un code Apogée
APO_CODE_STR_LEN = 512 # nb de car max d'un code Apogée (il peut y en avoir plusieurs)
GROUPNAME_STR_LEN = 64
from app.models.raw_sql_init import create_database_functions
from app.models.absences import Absence, AbsenceNotification, BilletAbsence
from app.models.config import ScoDocSiteConfig
from app.models.departements import Departement
from app.models.etudiants import (
Identite,
@ -57,7 +57,7 @@ from app.models.notes import (
NotesNotes,
NotesNotesLog,
)
from app.models.preferences import ScoPreference, ScoDocSiteConfig
from app.models.preferences import ScoPreference
from app.models.but_refcomp import (
ApcReferentielCompetences,

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),
ils ne sont pas affichés.
"""
return ", ".join([f"{p.ue.acronyme}: {p.poids}" for p in self.ue_poids])
# restreint aux UE du semestre dans lequel est cette évaluation
# au cas où le module ait changé de semestre et qu'il reste des poids
evaluation_semestre_idx = self.moduleimpl.module.semestre_id
return ", ".join(
[
f"{p.ue.acronyme}: {p.poids}"
for p in self.ue_poids
if evaluation_semestre_idx == p.ue.semestre_idx
]
)
class EvaluationUEPoids(db.Model):

View File

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

View File

@ -287,7 +287,7 @@ class FormSemestre(db.Model):
self.date_fin.year})"""
def titre_num(self) -> str:
"""Le titre est le semestre, ex ""DUT Informatique semestre 2"" """
"""Le titre et le semestre, ex ""DUT Informatique semestre 2"" """
if self.semestre_id == sco_codes_parcours.NO_SEMESTRE_ID:
return self.titre
return f"{self.titre} {self.formation.get_parcours().SESSION_NAME} {self.semestre_id}"

View File

@ -2,9 +2,8 @@
"""Model : preferences
"""
from app import db, log
from app.comp import bonus_spo
from app.scodoc.sco_exceptions import ScoValueError
from app import db
class ScoPreference(db.Model):
@ -19,128 +18,3 @@ class ScoPreference(db.Model):
name = db.Column(db.String(128), nullable=False, index=True)
value = db.Column(db.Text())
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
class ScoDocSiteConfig(db.Model):
"""Config. d'un site
Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions
antérieures étaient dans scodoc_config.py
"""
__tablename__ = "scodoc_site_config"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), nullable=False, index=True)
value = db.Column(db.Text())
BONUS_SPORT = "bonus_sport_func_name"
NAMES = {
BONUS_SPORT: str,
"always_require_ine": bool,
"SCOLAR_FONT": str,
"SCOLAR_FONT_SIZE": str,
"SCOLAR_FONT_SIZE_FOOT": str,
"INSTITUTION_NAME": str,
"INSTITUTION_ADDRESS": str,
"INSTITUTION_CITY": str,
"DEFAULT_PDF_FOOTER_TEMPLATE": str,
}
def __init__(self, name, value):
self.name = name
self.value = value
def __repr__(self):
return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>"
def get_dict(self) -> dict:
"Returns all data as a dict name = value"
return {
c.name: self.NAMES.get(c.name, lambda x: x)(c.value)
for c in ScoDocSiteConfig.query.all()
}
@classmethod
def set_bonus_sport_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"]
mu["was_capitalized"] = True
event_date = event_date or ue_cap["event_date"]
if (moy_ue_cap != "NA") and (moy_ue_cap > max_moy_ue):
if (
(moy_ue_cap != "NA")
and isinstance(moy_ue_cap, float)
and isinstance(max_moy_ue, float)
and (moy_ue_cap > max_moy_ue)
):
# meilleure UE capitalisée
event_date = ue_cap["event_date"]
max_moy_ue = moy_ue_cap
@ -1329,7 +1334,11 @@ class NotesTable:
t[0] = results.etud_moy_gen[etudid]
for i, ue in enumerate(ues, start=1):
if ue["type"] != UE_SPORT:
t[i] = results.etud_moy_ue[ue["id"]][etudid]
# temporaire pour 9.1.29 !
if ue["id"] in results.etud_moy_ue:
t[i] = results.etud_moy_ue[ue["id"]][etudid]
else:
t[i] = ""
# re-trie selon la nouvelle moyenne générale:
self.T.sort(key=self._row_key)
# Remplace aussi le rang:

View File

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

View File

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

View File

@ -32,15 +32,16 @@ import flask
from flask import url_for, render_template
from flask import g, request
from flask_login import current_user
from app import log
from app import models
from app.models import APO_CODE_STR_LEN
from app.models import Matiere, Module, UniteEns
from app.models import Formation, Matiere, Module, UniteEns
from app.models import FormSemestre, ModuleImpl
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app.scodoc.sco_utils import ModuleType
from app import log
from app import models
from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import (
@ -294,6 +295,7 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
"title": "Code Apogée",
"size": 25,
"explanation": "(optionnel) code élément pédagogique Apogée ou liste de codes ELP séparés par des virgules",
"validator": lambda val, _: len(val) < APO_CODE_STR_LEN,
},
),
(
@ -472,16 +474,31 @@ def module_edit(module_id=None):
formation_id = module["formation_id"]
formation = sco_formations.formation_list(args={"formation_id": formation_id})[0]
parcours = sco_codes_parcours.get_parcours_from_code(formation["type_parcours"])
is_apc = parcours.APC_SAE
ues_matieres = ndb.SimpleDictFetch(
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
FROM notes_matieres mat, notes_ue ue
WHERE mat.ue_id = ue.id
AND ue.formation_id = %(formation_id)s
ORDER BY ue.numero, mat.numero
""",
{"formation_id": formation_id},
)
is_apc = parcours.APC_SAE # BUT
in_use = len(a_module.modimpls.all()) > 0 # il y a des modimpls
if in_use:
# matières du même semestre seulement
ues_matieres = ndb.SimpleDictFetch(
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
FROM notes_matieres mat, notes_ue ue
WHERE mat.ue_id = ue.id
AND ue.formation_id = %(formation_id)s
AND ue.semestre_idx = %(semestre_idx)s
ORDER BY ue.numero, mat.numero
""",
{"formation_id": formation_id, "semestre_idx": a_module.ue.semestre_idx},
)
else:
# matières de la formation
ues_matieres = ndb.SimpleDictFetch(
"""SELECT ue.acronyme, mat.*, mat.id AS matiere_id
FROM notes_matieres mat, notes_ue ue
WHERE mat.ue_id = ue.id
AND ue.formation_id = %(formation_id)s
ORDER BY ue.numero, mat.numero
""",
{"formation_id": formation_id},
)
mat_names = ["%s / %s" % (x["acronyme"], x["titre"]) for x in ues_matieres]
ue_mat_ids = ["%s!%s" % (x["ue_id"], x["matiere_id"]) for x in ues_matieres]
module["ue_matiere_id"] = "%s!%s" % (module["ue_id"], module["matiere_id"])
@ -500,12 +517,25 @@ def module_edit(module_id=None):
),
"""<h2>Modification du module %(titre)s""" % module,
""" (formation %(acronyme)s, version %(version)s)</h2>""" % formation,
render_template("scodoc/help/modules.html", is_apc=is_apc),
render_template(
"scodoc/help/modules.html",
is_apc=is_apc,
formsemestres=FormSemestre.query.filter(
ModuleImpl.formsemestre_id == FormSemestre.id,
ModuleImpl.module_id == module_id,
).all(),
),
]
if not unlocked:
H.append(
"""<div class="ue_warning"><span>Formation verrouillée, seuls certains éléments peuvent être modifiés</span></div>"""
)
if in_use:
H.append(
"""<div class="ue_warning"><span>Module déjà utilisé dans des semestres,
soyez prudents !
</span></div>"""
)
descr = [
(
@ -680,6 +710,13 @@ def module_edit(module_id=None):
else:
# l'UE peut changer
tf[2]["ue_id"], tf[2]["matiere_id"] = tf[2]["ue_matiere_id"].split("!")
old_ue_id = a_module.ue.id
new_ue_id = int(tf[2]["ue_id"])
if (old_ue_id != new_ue_id) and in_use:
# pas changer de semestre un module utilisé !
raise ScoValueError(
"Module utilisé: il ne peut pas être changé de semestre !"
)
# En APC, force le semestre égal à celui de l'UE
if is_apc:
selected_ue = UniteEns.query.get(tf[2]["ue_id"])

View File

@ -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):
"set UE code apogee"
ue_id = id
value = value.strip("-_ \t")
value = value.strip("-_ \t")[:APO_CODE_STR_LEN] # tronque
log("edit_ue_set_code_apogee: ue_id=%s code_apogee=%s" % (ue_id, value))
ues = ue_list(args={"ue_id": ue_id})

View File

@ -143,6 +143,7 @@ def evaluation_create_form(
if vals.get("tf_submitted", False) and "visibulletinlist" not in vals:
vals["visibulletinlist"] = []
#
ue_coef_dict = {}
if is_apc: # BUT: poids vers les UE
ue_coef_dict = ModuleImpl.query.get(moduleimpl_id).module.get_ue_coef_dict()
for ue in sem_ues:
@ -290,7 +291,10 @@ def evaluation_create_form(
"title": f"Poids {ue.acronyme}",
"size": 2,
"type": "float",
"explanation": f"{ue.titre}",
"explanation": f"""
<span class="eval_coef_ue" title="coef. du module dans cette UE">{ue_coef_dict.get(ue.id, 0.)}</span>
<span class="eval_coef_ue_titre">{ue.titre}</span>
""",
"allow_null": False,
},
),

View File

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

View File

@ -748,7 +748,7 @@ def form_decision_manuelle(Se, formsemestre_id, etudid, desturl="", sortcol=None
)
# Choix code semestre:
codes = list(sco_codes_parcours.CODES_EXPL.keys())
codes = list(sco_codes_parcours.CODES_JURY_SEM)
codes.sort() # fortuitement, cet ordre convient bien !
H.append(

View File

@ -87,7 +87,7 @@ groupEditor = ndb.EditableTable(
group_list = groupEditor.list
def get_group(group_id):
def get_group(group_id: int):
"""Returns group object, with partition"""
r = ndb.SimpleDictFetch(
"""SELECT gd.id AS group_id, gd.*, p.id AS partition_id, p.*
@ -687,6 +687,11 @@ def setGroups(
group_id = fs[0].strip()
if not group_id:
continue
try:
group_id = int(group_id)
except ValueError as exc:
log("setGroups: ignoring invalid group_id={group_id}")
continue
group = get_group(group_id)
# Anciens membres du groupe:
old_members = get_group_members(group_id)

View File

@ -49,9 +49,11 @@ from app.scodoc import sco_etud
from app.scodoc.sco_exceptions import ScoValueError
def list_authorized_etuds_by_sem(sem, delai=274):
def list_authorized_etuds_by_sem(sem, delai=274, ignore_jury=False):
"""Liste des etudiants autorisés à s'inscrire dans sem.
delai = nb de jours max entre la date de l'autorisation et celle de debut du semestre cible.
ignore_jury: si vrai, considère tous les étudiants comem autorisés, même
s'ils n'ont pas de décision de jury.
"""
src_sems = list_source_sems(sem, delai=delai)
inscrits = list_inscrits(sem["formsemestre_id"])
@ -59,7 +61,12 @@ def list_authorized_etuds_by_sem(sem, delai=274):
candidats = {} # etudid : etud (tous les etudiants candidats)
nb = 0 # debug
for src in src_sems:
liste = list_etuds_from_sem(src, sem)
if ignore_jury:
# liste de tous les inscrits au semestre (sans dems)
liste = list_inscrits(src["formsemestre_id"]).values()
else:
# liste des étudiants autorisés par le jury à s'inscrire ici
liste = list_etuds_from_sem(src, sem)
liste_filtree = []
for e in liste:
# Filtre ceux qui se sont déjà inscrit dans un semestre APRES le semestre src
@ -125,7 +132,7 @@ def list_inscrits(formsemestre_id, with_dems=False):
return inscr
def list_etuds_from_sem(src, dst):
def list_etuds_from_sem(src, dst) -> list[dict]:
"""Liste des etudiants du semestre src qui sont autorisés à passer dans le semestre dst."""
target = dst["semestre_id"]
dpv = sco_pvjury.dict_pvjury(src["formsemestre_id"])
@ -224,7 +231,7 @@ def do_desinscrit(sem, etudids):
)
def list_source_sems(sem, delai=None):
def list_source_sems(sem, delai=None) -> list[dict]:
"""Liste des semestres sources
sem est le semestre destination
"""
@ -265,6 +272,7 @@ def formsemestre_inscr_passage(
inscrit_groupes=False,
submitted=False,
dialog_confirmed=False,
ignore_jury=False,
):
"""Form. pour inscription des etudiants d'un semestre dans un autre
(donné par formsemestre_id).
@ -280,6 +288,7 @@ def formsemestre_inscr_passage(
"""
inscrit_groupes = int(inscrit_groupes)
ignore_jury = int(ignore_jury)
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
# -- check lock
if not sem["etat"]:
@ -295,7 +304,9 @@ def formsemestre_inscr_passage(
elif etuds and isinstance(etuds[0], str):
etuds = [int(x) for x in etuds]
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(sem)
auth_etuds_by_sem, inscrits, candidats = list_authorized_etuds_by_sem(
sem, ignore_jury=ignore_jury
)
etuds_set = set(etuds)
candidats_set = set(candidats)
inscrits_set = set(inscrits)
@ -323,6 +334,7 @@ def formsemestre_inscr_passage(
candidats_non_inscrits,
inscrits_ailleurs,
inscrit_groupes=inscrit_groupes,
ignore_jury=ignore_jury,
)
else:
if not dialog_confirmed:
@ -363,6 +375,7 @@ def formsemestre_inscr_passage(
"formsemestre_id": formsemestre_id,
"etuds": ",".join([str(x) for x in etuds]),
"inscrit_groupes": inscrit_groupes,
"ignore_jury": ignore_jury,
"submitted": 1,
},
)
@ -411,18 +424,23 @@ def build_page(
candidats_non_inscrits,
inscrits_ailleurs,
inscrit_groupes=False,
ignore_jury=False,
):
inscrit_groupes = int(inscrit_groupes)
ignore_jury = int(ignore_jury)
if inscrit_groupes:
inscrit_groupes_checked = " checked"
else:
inscrit_groupes_checked = ""
if ignore_jury:
ignore_jury_checked = " checked"
else:
ignore_jury_checked = ""
H = [
html_sco_header.html_sem_header(
"Passages dans le semestre", with_page_header=False
),
"""<form method="post" action="%s">""" % request.base_url,
"""<form name="f" method="post" action="%s">""" % request.base_url,
"""<input type="hidden" name="formsemestre_id" value="%(formsemestre_id)s"/>
<input type="submit" name="submitted" value="Appliquer les modifications"/>
&nbsp;<a href="#help">aide</a>
@ -430,6 +448,8 @@ def build_page(
% sem, # "
"""<input name="inscrit_groupes" type="checkbox" value="1" %s>inscrire aux mêmes groupes</input>"""
% inscrit_groupes_checked,
"""<input name="ignore_jury" type="checkbox" value="1" onchange="document.f.submit()" %s>inclure tous les étudiants (même sans décision de jury)</input>"""
% ignore_jury_checked,
"""<div class="pas_recap">Actuellement <span id="nbinscrits">%s</span> inscrits
et %d candidats supplémentaires
</div>"""

View File

@ -401,7 +401,9 @@ def moduleimpl_status(moduleimpl_id=None, partition_id=None):
eval_index = len(mod_evals) - 1
first_eval = True
for eval in mod_evals:
evaluation = Evaluation.query.get(eval["evaluation_id"]) # TODO unifier
evaluation: Evaluation = Evaluation.query.get(
eval["evaluation_id"]
) # TODO unifier
etat = sco_evaluations.do_evaluation_etat(
eval["evaluation_id"],
partition_id=partition_id,

View File

@ -169,7 +169,9 @@ def get_inscrits_etape(code_etape, anneeapogee=None, ntrials=2):
if doc:
break
if not doc:
raise ScoValueError("pas de réponse du portail ! (timeout=%s)" % portal_timeout)
raise ScoValueError(
f"pas de réponse du portail ! <br>(timeout={portal_timeout}, requête: <tt>{req}</tt>)"
)
etuds = _normalize_apo_fields(xml_to_list_of_dicts(doc, req=req))
# Filtre sur annee inscription Apogee:

View File

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

View File

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

View File

@ -153,7 +153,10 @@ def _check_notes(notes, evaluation, mod):
for (etudid, note) in notes:
note = str(note).strip().upper()
etudid = int(etudid) #
try:
etudid = int(etudid) #
except ValueError as exc:
raise ScoValueError(f"Code étudiant ({etudid}) invalide")
if note[:3] == "DEM":
continue # skip !
if note:
@ -487,10 +490,10 @@ def notes_add(
}
for (etudid, value) in notes:
if check_inscription and (etudid not in inscrits):
raise NoteProcessError("etudiant non inscrit dans ce module")
if not ((value is None) or (type(value) == type(1.0))):
raise NoteProcessError(f"etudiant {etudid} non inscrit dans ce module")
if (value is not None) and not isinstance(value, float):
raise NoteProcessError(
"etudiant %s: valeur de note invalide (%s)" % (etudid, value)
f"etudiant {etudid}: valeur de note invalide ({value})"
)
# Recherche notes existantes
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)
r = ndb.SimpleDictFetch(
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name
"""SELECT i.nom, i.prenom, code_nip, n.*, mod.titre, e.description, e.jour, u.user_name, e.id as evaluation_id
FROM notes_notes n, notes_evaluation e, notes_moduleimpl mi,
notes_modules mod, identite i, "user" u
WHERE mi.id = e.moduleimpl_id
@ -202,6 +202,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
"value",
"user_name",
"titre",
"evaluation_id",
"description",
"jour",
"comment",
@ -214,6 +215,7 @@ def formsemestre_list_saisies_notes(formsemestre_id, format="html"):
"value": "Note",
"comment": "Remarque",
"user_name": "Enseignant",
"evaluation_id": "evaluation_id",
"titre": "Module",
"description": "Evaluation",
"jour": "Date éval.",

View File

@ -1513,6 +1513,16 @@ table.moduleimpl_evaluations td.eval_poids {
color:rgb(0, 0, 255);
}
span.eval_coef_ue {
color:rgb(6, 73, 6);
font-style: normal;
font-size: 80%;
margin-right: 2em;
}
span.eval_coef_ue_titre {
}
/* Formulaire edition des partitions */
form#editpart table {
border: 1px solid gray;

View File

@ -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>
{{ render_field(form.bonus_sport_func_name, onChange="submit_form()")}}
<h1>Exports Apogée</h1>
<p><a href="{{url_for('scodoc.config_codes_decisions')}}">configuration des codes de décision</a></p>
<h1>Bibliothèque de logos</h1>
{% for dept_entry in form.depts.entries %}
{% set dept_form = dept_entry.form %}

View File

@ -24,4 +24,24 @@
<a href="https://scodoc.org/BUT" target="_blank">la documentation</a>.
</p>
{%endif%}
{% if formsemestres %}
<p class="help">
Ce module est utilisé dans des semestres déjà mis en place, il faut prêter attention
aux conséquences des changements effectués ici: par exemple les coefficients vont modifier
les notes moyennes calculées. Les modules déjà utilisés ne peuvent pas être changés de semestre, ni détruits.
Si vous souhaitez faire cela, allez d'abord modifier les semestres concernés pour déselectionner le module.
</p>
<h4>Semestres utilisant ce module:</h4>
<ul>
{%for formsemestre in formsemestres %}
<li><a class="stdlink" href="{{
url_for('notes.formsemestre_status',
scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre.id)
}}">{{formsemestre.titre_mois()}}</a>
</li>
{% endfor %}
</ul>
{%endif%}
</div>

View File

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

View File

@ -33,49 +33,38 @@ Emmanuel Viennet, 2021
import datetime
import io
import wtforms.validators
from app.auth.models import User
import os
import flask
from flask import abort, flash, url_for, redirect, render_template, send_file
from flask import request
from flask.app import Flask
import flask_login
from flask_login.utils import login_required, current_user
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from werkzeug.exceptions import BadRequest, NotFound
from wtforms import SelectField, SubmitField, FormField, validators, Form, FieldList
from wtforms.fields import IntegerField
from wtforms.fields.simple import BooleanField, StringField, TextAreaField, HiddenField
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
from PIL import Image as PILImage
from werkzeug.exceptions import BadRequest, NotFound
import app
from app import db
from app.auth.models import User
from app.forms.main import config_forms
from app.forms.main.create_dept import CreateDeptForm
from app.forms.main.config_apo import CodesDecisionsForm
from app import models
from app.models import Departement, Identite
from app.models import departements
from app.models import FormSemestre, FormSemestreInscription
import sco_version
from app.scodoc import sco_logos
from app.models import ScoDocSiteConfig
from app.scodoc import sco_codes_parcours, sco_logos
from app.scodoc import sco_find_etud
from app.scodoc import sco_utils as scu
from app.decorators import (
admin_required,
scodoc7func,
scodoc,
permission_required_compat_scodoc7,
permission_required,
)
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.sco_logos import find_logo
from app.scodoc.sco_permissions import Permission
from app.views import scodoc_bp as bp
from PIL import Image as PILImage
import sco_version
@bp.route("/")
@ -133,6 +122,28 @@ def toggle_dept_vis(dept_id):
return redirect(url_for("scodoc.index"))
@bp.route("/ScoDoc/config_codes_decisions", methods=["GET", "POST"])
@admin_required
def config_codes_decisions():
"""Form config codes decisions"""
form = CodesDecisionsForm()
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.index"))
if form.validate_on_submit():
for code in models.config.CODES_SCODOC_TO_APO:
ScoDocSiteConfig.set_code_apo(code, getattr(form, code).data)
flash(f"Codes décisions enregistrés.")
return redirect(url_for("scodoc.index"))
elif request.method == "GET":
for code in models.config.CODES_SCODOC_TO_APO:
getattr(form, code).data = ScoDocSiteConfig.get_code_apo(code)
return render_template(
"config_codes_decisions.html",
form=form,
title="Configuration des codes de décisions",
)
@bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required
def table_etud_in_accessible_depts():
@ -257,14 +268,16 @@ def _return_logo(name="header", dept_id="", small=False, strict: bool = True):
suffix = logo.suffix
if small:
with PILImage.open(logo.filepath) as im:
im.thumbnail(SMALL_SIZE)
stream = io.BytesIO()
# on garde le même format (on pourrait plus simplement générer systématiquement du JPEG)
fmt = { # adapt suffix to be compliant with PIL save format
"PNG": "PNG",
"JPG": "JPEG",
"JPEG": "JPEG",
}[suffix.upper()]
if fmt == "JPEG":
im = im.convert("RGB")
im.thumbnail(SMALL_SIZE)
stream = io.BytesIO()
im.save(stream, fmt)
stream.seek(0)
return send_file(stream, mimetype=f"image/{fmt}")

View File

@ -81,7 +81,7 @@ _l = _
class ChangePasswordForm(FlaskForm):
user_name = HiddenField()
old_password = PasswordField(_l("Identifiez-vous"))
new_password = PasswordField(_l("Nouveau mot de passe"))
new_password = PasswordField(_l("Nouveau mot de passe de l'utilisateur"))
bis_password = PasswordField(
_l("Répéter"),
validators=[

View File

@ -26,6 +26,9 @@ class Config:
SCODOC_ADMIN_LOGIN = os.environ.get("SCODOC_ADMIN_LOGIN") or "admin"
ADMINS = [SCODOC_ADMIN_MAIL]
SCODOC_ERR_MAIL = os.environ.get("SCODOC_ERR_MAIL")
# Le "from" des mails émis. Attention: peut être remplacée par la préférence email_from_addr:
SCODOC_MAIL_FROM = os.environ.get("SCODOC_MAIL_FROM") or ("no-reply@" + MAIL_SERVER)
BOOTSTRAP_SERVE_LOCAL = os.environ.get("BOOTSTRAP_SERVE_LOCAL")
SCODOC_DIR = os.environ.get("SCODOC_DIR", "/opt/scodoc")
SCODOC_VAR_DIR = os.environ.get("SCODOC_VAR_DIR", "/opt/scodoc-data")

View File

@ -0,0 +1,84 @@
"""augmente taille codes Apogée
Revision ID: 28874ed6af64
Revises: f40fbaf5831c
Create Date: 2022-01-19 22:57:59.678313
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "28874ed6af64"
down_revision = "f40fbaf5831c"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"notes_formsemestre_etapes",
"etape_apo",
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=512),
existing_nullable=True,
)
op.alter_column(
"notes_formsemestre_inscription",
"etape",
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=512),
existing_nullable=True,
)
op.alter_column(
"notes_modules",
"code_apogee",
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=512),
existing_nullable=True,
)
op.alter_column(
"notes_ue",
"code_apogee",
existing_type=sa.VARCHAR(length=24),
type_=sa.String(length=512),
existing_nullable=True,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column(
"notes_ue",
"code_apogee",
existing_type=sa.String(length=512),
type_=sa.VARCHAR(length=24),
existing_nullable=True,
)
op.alter_column(
"notes_modules",
"code_apogee",
existing_type=sa.String(length=512),
type_=sa.VARCHAR(length=24),
existing_nullable=True,
)
op.alter_column(
"notes_formsemestre_inscription",
"etape",
existing_type=sa.String(length=512),
type_=sa.VARCHAR(length=24),
existing_nullable=True,
)
op.alter_column(
"notes_formsemestre_etapes",
"etape_apo",
existing_type=sa.String(length=512),
type_=sa.VARCHAR(length=24),
existing_nullable=True,
)
# ### end Alembic commands ###

View File

@ -289,20 +289,28 @@ def user_role(username, dept_acronym=None, add_role_name=None, remove_role_name=
db.session.commit()
def abort_if_false(ctx, param, value):
if not value:
ctx.abort()
@app.cli.command()
@click.option(
"--yes",
is_flag=True,
callback=abort_if_false,
expose_value=False,
prompt=f"""Attention: Cela va effacer toutes les données du département
(étudiants, notes, formations, etc)
Voulez-vous vraiment continuer ?
""",
)
@click.argument("dept")
def delete_dept(dept): # delete-dept
"""Delete existing departement"""
from app.scodoc import notesdb as ndb
from app.scodoc import sco_dept
click.confirm(
f"""Attention: Cela va effacer toutes les données du département {dept}
(étudiants, notes, formations, etc)
Voulez-vous vraiment continuer ?
""",
abort=True,
)
db.reflect()
ndb.open_db_connection()
d = models.Departement.query.filter_by(acronym=dept).first()

View File

@ -170,6 +170,11 @@ def import_scodoc7_dept(dept_id: str, dept_db_uri=None):
logging.info(f"connecting to database {dept_db_uri}")
cnx = psycopg2.connect(dept_db_uri)
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
# FIX : des dates aberrantes (dans le futur) peuvent tenir en SQL mais pas en Python
cursor.execute(
"""UPDATE scolar_events SET event_date='2021-09-30' WHERE event_date > '2200-01-01'"""
)
cnx.commit()
# Create dept:
dept = models.Departement(acronym=dept_id, description="migré de ScoDoc7")
db.session.add(dept)
@ -374,6 +379,8 @@ def convert_object(
new_ref = id_from_scodoc7[old_ref]
elif (not is_table) and table_name in {
"scolog",
"entreprise_correspondant",
"entreprise_contact",
"etud_annotations",
"notes_notes_log",
"scolar_news",
@ -389,7 +396,6 @@ def convert_object(
new_ref = None
elif is_table and table_name in {
"notes_semset_formsemestre",
"entreprise_contact",
}:
# 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é