Merge pull request 'master' (#5) from ScoDoc/ScoDoc:master into master

Reviewed-on: lehmann/ScoDoc-Front#5
This commit is contained in:
Sébastien Lehmann 2022-01-08 17:17:34 +01:00
commit 12e92b0c19
38 changed files with 448 additions and 223 deletions

View File

@ -190,6 +190,7 @@ def create_app(config_class=DevConfig):
app.register_error_handler(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, handle_sco_value_error)
app.register_error_handler(AccessDenied, handle_access_denied)
app.register_error_handler(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error)

View File

@ -72,10 +72,10 @@ def bulletin_but_xml_compat(
etud = Identite.query.get_or_404(etudid)
results = bulletin_but.ResultatsSemestreBUT(sem)
nb_inscrits = len(results.etuds)
if sem.bul_hide_xml or force_publishing:
published = "1"
if (not sem.bul_hide_xml) or force_publishing:
published = 1
else:
published = "0"
published = 0
if xml_nodate:
docdate = ""
else:
@ -84,7 +84,7 @@ def bulletin_but_xml_compat(
"etudid": str(etudid),
"formsemestre_id": str(formsemestre_id),
"date": docdate,
"publie": published,
"publie": str(published),
}
if sem.etapes:
el["etape_apo"] = sem.etapes[0].etape_apo or ""
@ -117,7 +117,9 @@ def bulletin_but_xml_compat(
)
# Disponible pour publication ?
if not published:
return doc # stop !
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
scu.SCO_ENCODING
) # stop !
# Moyenne générale:
doc.append(
Element(
@ -218,7 +220,8 @@ def bulletin_but_xml_compat(
value=scu.fmt_note(
results.modimpls_evals_notes[e.moduleimpl_id][
e.id
][etud.id]
][etud.id],
note_max=e.note_max,
),
)
)

View File

@ -36,6 +36,7 @@ import pandas as pd
from pandas.core.frame import DataFrame
from app import db
from app import log
from app import models
from app.models import ModuleImpl, Evaluation, EvaluationUEPoids
from app.scodoc import sco_utils as scu
@ -79,7 +80,11 @@ def check_moduleimpl_conformity(
if nb_ues == 0:
return False # situation absurde (pas d'UE)
if len(modules_coefficients) != nb_ues:
raise ValueError("check_moduleimpl_conformity: nb ue incoherent")
# bug ?
log(
"check_moduleimpl_conformity: nb ue incoherent (moduleimpl.id={moduleimpl.id})"
)
return False
module_evals_poids = evals_poids.transpose().sum(axis=1).to_numpy() != 0
check = all(
(modules_coefficients[moduleimpl.module.id].to_numpy() != 0)

View File

@ -245,7 +245,7 @@ class DeptForm(FlaskForm):
def _make_dept_id_name():
"""Cette section assute que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
"""Cette section assure que tous les départements sont traités (y compris ceux qu'ont pas de logo au départ)
et détermine l'ordre d'affichage des DeptForm (GLOBAL d'abord, puis par ordre alpha de nom de département)
-> [ (None, None), (dept_id, dept_name)... ]"""
depts = [(None, GLOBAL)]

View File

@ -31,16 +31,10 @@ Formulaires création département
from flask import flash, url_for, redirect, render_template
from flask_wtf import FlaskForm
from wtforms import SelectField, SubmitField, FormField, validators, FieldList
from wtforms.fields.simple import StringField, HiddenField
from wtforms import SubmitField, validators
from wtforms.fields.simple import StringField, BooleanField
from app import AccessDenied
from app.models import Departement
from app.models import ScoPreference
from app.models import SHORT_STR_LEN
from app.scodoc import sco_utils as scu
from flask_login import current_user
class CreateDeptForm(FlaskForm):
@ -60,5 +54,10 @@ class CreateDeptForm(FlaskForm):
validators.DataRequired("acronyme du département requis"),
],
)
# description = StringField(label="Description")
visible = BooleanField(
"Visible sur page d'accueil",
default=True,
)
submit = SubmitField("Valider")
cancel = SubmitField("Annuler", render_kw={"formnovalidate": True})

View File

@ -12,8 +12,10 @@ class Departement(db.Model):
"""Un département ScoDoc"""
id = db.Column(db.Integer, primary_key=True)
acronym = db.Column(db.String(SHORT_STR_LEN), nullable=False, index=True)
description = db.Column(db.Text())
acronym = db.Column(
db.String(SHORT_STR_LEN), nullable=False, index=True
) # ne change jamais, voir la pref. DeptName
description = db.Column(db.Text()) # pas utilisé par ScoDoc : voir DeptFullName
date_creation = db.Column(db.DateTime(timezone=True), server_default=db.func.now())
visible = db.Column(
db.Boolean(), nullable=False, default=True, server_default="true"
@ -49,11 +51,11 @@ class Departement(db.Model):
return dept
def create_dept(acronym: str) -> Departement:
def create_dept(acronym: str, visible=True) -> Departement:
"Create new departement"
from app.models import ScoPreference
departement = Departement(acronym=acronym)
departement = Departement(acronym=acronym, visible=visible)
p1 = ScoPreference(name="DeptName", value=acronym, departement=departement)
db.session.add(p1)
db.session.add(departement)

View File

@ -97,14 +97,18 @@ class Formation(db.Model):
for sem in self.formsemestres:
sco_cache.invalidate_formsemestre(formsemestre_id=sem.id)
def force_semestre_modules_aux_ues(self) -> None:
def sanitize_old_formation(self) -> None:
"""
Affecte à chaque module de cette formation le semestre de son UE de rattachement,
Corrige si nécessaire certains champs issus d'anciennes versions de ScoDoc:
- affecte à chaque module de cette formation le semestre de son UE de rattachement,
si elle en a une.
- si le module_type n'est pas renseigné, le met à STANDARD.
Devrait être appelé lorsqu'on change le type de formation vers le BUT, et aussi
lorsqu'on change le semestre d'une UE BUT.
Utile pour la migration des anciennes formations vers le BUT.
Invalide les caches coefs/poids.
En cas de changement, invalide les caches coefs/poids.
"""
if not self.is_apc():
return
@ -118,6 +122,10 @@ class Formation(db.Model):
mod.semestre_id = mod.ue.semestre_idx
db.session.add(mod)
change = True
if mod.module_type is None:
mod.module_type = scu.ModuleType.STANDARD
db.session.add(mod)
change = True
db.session.commit()
if change:
self.invalidate_module_coefs()

View File

@ -49,9 +49,7 @@ class Module(db.Model):
super(Module, self).__init__(**kwargs)
def __repr__(self):
return (
f"<Module{ModuleType(self.module_type).name} id={self.id} code={self.code}>"
)
return f"<Module{ModuleType(self.module_type or ModuleType.STANDARD).name} id={self.id} code={self.code}>"
def to_dict(self):
e = dict(self.__dict__)

View File

@ -46,7 +46,10 @@ class UniteEns(db.Model):
modules = db.relationship("Module", lazy="dynamic", backref="ue")
def __repr__(self):
return f"<{self.__class__.__name__}(id={self.id}, formation_id={self.formation_id}, acronyme='{self.acronyme}')>"
return f"""<{self.__class__.__name__}(id={self.id}, formation_id={
self.formation_id}, acronyme='{self.acronyme}', semestre_idx={
self.semestre_idx} {
'EXTERNE' if self.is_external else ''})>"""
def to_dict(self):
"""as a dict, with the same conversions as in ScoDoc7"""

View File

@ -416,6 +416,20 @@ def bonus_iutbeziers(notes_sport, coefs, infos=None):
return bonus
def bonus_iutlr(notes_sport, coefs, infos=None):
"""Calcul bonus modules optionels (sport, culture), règle IUT La Rochelle
Si la note de sport est comprise entre 0 et 10 : pas d'ajout de point
Si la note de sport est comprise entre 10.1 et 20 : ajout de 1% de cette note sur la moyenne générale du semestre
"""
# les coefs sont ignorés
# une seule note
note_sport = notes_sport[0]
if note_sport <= 10:
return 0
bonus = note_sport * 0.01 # 1%
return bonus
def bonus_demo(notes_sport, coefs, infos=None):
"""Fausse fonction "bonus" pour afficher les informations disponibles
et aider les développeurs.

View File

@ -108,13 +108,14 @@ def apo_compare_csv(A_file, B_file, autodetect=True):
def _load_apo_data(csvfile, autodetect=True):
"Read data from request variable and build ApoData"
data = csvfile.read()
data_b = csvfile.read()
if autodetect:
data, message = sco_apogee_csv.fix_data_encoding(data)
data_b, message = sco_apogee_csv.fix_data_encoding(data_b)
if message:
log("apo_compare_csv: %s" % message)
if not data:
if not data_b:
raise ScoValueError("apo_compare_csv: no data")
data = data_b.decode(sco_apogee_csv.APO_INPUT_ENCODING)
apo_data = sco_apogee_csv.ApoData(data, orig_filename=csvfile.filename)
return apo_data

View File

@ -173,8 +173,10 @@ def guess_data_encoding(text, threshold=0.6):
def fix_data_encoding(
text, default_source_encoding=APO_INPUT_ENCODING, dest_encoding=APO_INPUT_ENCODING
):
text: bytes,
default_source_encoding=APO_INPUT_ENCODING,
dest_encoding=APO_INPUT_ENCODING,
) -> bytes:
"""Try to ensure that text is using dest_encoding
returns converted text, and a message describing the conversion.
"""
@ -200,7 +202,7 @@ def fix_data_encoding(
class StringIOFileLineWrapper(object):
def __init__(self, data):
def __init__(self, data: str):
self.f = io.StringIO(data)
self.lineno = 0
@ -655,7 +657,7 @@ class ApoEtud(dict):
class ApoData(object):
def __init__(
self,
data,
data: str,
periode=None,
export_res_etape=True,
export_res_sem=True,
@ -693,7 +695,7 @@ class ApoData(object):
"<h3>Erreur lecture du fichier Apogée <tt>%s</tt></h3><p>" % filename
+ e.args[0]
+ "</p>"
)
) from e
self.etape_apogee = self.get_etape_apogee() # 'V1RT'
self.vdi_apogee = self.get_vdi_apogee() # '111'
self.etape = ApoEtapeVDI(etape=self.etape_apogee, vdi=self.vdi_apogee)
@ -760,7 +762,6 @@ class ApoData(object):
def read_csv(self, data: str):
if not data:
raise ScoFormatError("Fichier Apogée vide !")
f = StringIOFileLineWrapper(data) # pour traiter comme un fichier
# check that we are at the begining of Apogee CSV
line = f.readline().strip()
@ -768,7 +769,10 @@ class ApoData(object):
raise ScoFormatError("format incorrect: pas de XX-APO_TITRES-XX")
# 1-- En-tête: du début jusqu'à la balise XX-APO_VALEURS-XX
idx = data.index("XX-APO_VALEURS-XX")
try:
idx = data.index("XX-APO_VALEURS-XX")
except ValueError as exc:
raise ScoFormatError("format incorrect: pas de XX-APO_VALEURS-XX") from exc
self.header = data[:idx]
# 2-- Titres:
@ -1178,7 +1182,7 @@ def nar_etuds_table(apo_data, NAR_Etuds):
def export_csv_to_apogee(
apo_csv_data,
apo_csv_data: str,
periode=None,
dest_zip=None,
export_res_etape=True,

View File

@ -93,9 +93,9 @@ def make_xml_formsemestre_bulletinetud(
)
if (not sem["bul_hide_xml"]) or force_publishing:
published = "1"
published = 1
else:
published = "0"
published = 0
if xml_nodate:
docdate = ""
else:
@ -105,7 +105,7 @@ def make_xml_formsemestre_bulletinetud(
"etudid": str(etudid),
"formsemestre_id": str(formsemestre_id),
"date": docdate,
"publie": published,
"publie": str(published),
}
if sem["etapes"]:
el["etape_apo"] = str(sem["etapes"][0]) or ""
@ -141,7 +141,9 @@ def make_xml_formsemestre_bulletinetud(
# Disponible pour publication ?
if not published:
return doc # stop !
return sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(
scu.SCO_ENCODING
) # stop !
# Groupes:
partitions = sco_groups.get_partitions_list(formsemestre_id, with_default=False)

View File

@ -62,7 +62,8 @@ def html_edit_formation_apc(
else:
semestre_ids = [semestre_idx]
other_modules = formation.modules.filter(
Module.module_type != ModuleType.SAE, Module.module_type != ModuleType.RESSOURCE
Module.module_type.is_distinct_from(ModuleType.SAE),
Module.module_type.is_distinct_from(ModuleType.RESSOURCE),
).order_by(
Module.semestre_id, Module.module_type.desc(), Module.numero, Module.code
)
@ -78,7 +79,8 @@ def html_edit_formation_apc(
alt="supprimer",
),
"delete_disabled": scu.icontag(
"delete_small_dis_img", title="Suppression impossible (module utilisé)"
"delete_small_dis_img",
title="Suppression impossible (utilisé dans des semestres)",
),
}

View File

@ -304,9 +304,9 @@ def do_formation_edit(args):
cnx = ndb.GetDBConnexion()
sco_formations._formationEditor.edit(cnx, args)
formation = Formation.query.get(args["formation_id"])
formation: Formation = Formation.query.get(args["formation_id"])
formation.invalidate_cached_sems()
formation.force_semestre_modules_aux_ues()
formation.sanitize_old_formation()
def module_move(module_id, after=0, redirect=True):

View File

@ -30,13 +30,18 @@
"""
import flask
from flask import g, url_for, request
from app.models.formations import Matiere
import app.scodoc.notesdb as ndb
import app.scodoc.sco_utils as scu
from app import log
from app.models import Formation
from app.scodoc.TrivialFormulator import TrivialFormulator, tf_error_message
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header
_matiereEditor = ndb.EditableTable(
@ -156,6 +161,16 @@ associé.
return flask.redirect(dest_url)
def can_delete_matiere(matiere: Matiere) -> tuple[bool, str]:
"True si la matiere n'est pas utilisée dans des formsemestre"
locked = matiere_is_locked(matiere.id)
if locked:
return False
if any(m.modimpls.all() for m in matiere.modules):
return False
return True
def do_matiere_delete(oid):
"delete matiere and attached modules"
from app.scodoc import sco_formations
@ -165,17 +180,16 @@ def do_matiere_delete(oid):
cnx = ndb.GetDBConnexion()
# check
mat = matiere_list({"matiere_id": oid})[0]
matiere = Matiere.query.get_or_404(oid)
mat = matiere_list({"matiere_id": oid})[0] # compat sco7
ue = sco_edit_ue.ue_list({"ue_id": mat["ue_id"]})[0]
locked = matiere_is_locked(mat["matiere_id"])
if locked:
log("do_matiere_delete: mat=%s" % mat)
log("do_matiere_delete: ue=%s" % ue)
log("do_matiere_delete: locked sems: %s" % locked)
raise ScoLockedFormError()
log("do_matiere_delete: matiere_id=%s" % oid)
if not can_delete_matiere(matiere):
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject("Matière", matiere.titre)
log("do_matiere_delete: matiere_id=%s" % matiere.id)
# delete all modules in this matiere
mods = sco_edit_module.module_list({"matiere_id": oid})
mods = sco_edit_module.module_list({"matiere_id": matiere.id})
for mod in mods:
sco_edit_module.do_module_delete(mod["module_id"])
_matiereEditor.delete(cnx, oid)
@ -194,11 +208,25 @@ def matiere_delete(matiere_id=None):
"""Delete matière"""
from app.scodoc import sco_edit_ue
M = matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.ue_list(args={"ue_id": M["ue_id"]})[0]
matiere = Matiere.query.get_or_404(matiere_id)
if not can_delete_matiere(matiere):
# il y a au moins un modimpl dans un module de cette matière
raise ScoNonEmptyFormationObject(
"Matière",
matiere.titre,
dest_url=url_for(
"notes.ue_table",
formation_id=matiere.ue.formation_id,
semestre_idx=matiere.ue.semestre_idx,
scodoc_dept=g.scodoc_dept,
),
)
mat = matiere_list(args={"matiere_id": matiere_id})[0]
UE = sco_edit_ue.ue_list(args={"ue_id": mat["ue_id"]})[0]
H = [
html_sco_header.sco_header(page_title="Suppression d'une matière"),
"<h2>Suppression de la matière %(titre)s" % M,
"<h2>Suppression de la matière %(titre)s" % mat,
" dans l'UE (%(acronyme)s))</h2>" % UE,
]
dest_url = url_for(
@ -210,7 +238,7 @@ def matiere_delete(matiere_id=None):
request.base_url,
scu.get_request_args(),
(("matiere_id", {"input_type": "hidden"}),),
initvalues=M,
initvalues=mat,
submitlabel="Confirmer la suppression",
cancelbutton="Annuler",
)

View File

@ -43,7 +43,12 @@ 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 ScoValueError, ScoLockedFormError, ScoGenError
from app.scodoc.sco_exceptions import (
ScoValueError,
ScoLockedFormError,
ScoGenError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header
from app.scodoc import sco_codes_parcours
from app.scodoc import sco_edit_matiere
@ -330,20 +335,37 @@ def module_create(matiere_id=None, module_type=None, semestre_id=None):
)
def can_delete_module(module):
"True si le module n'est pas utilisée dans des formsemestre"
return len(module.modimpls.all()) == 0
def do_module_delete(oid):
"delete module"
from app.scodoc import sco_formations
mod = module_list({"module_id": oid})[0]
if module_is_locked(mod["module_id"]):
module = Module.query.get_or_404(oid)
mod = module_list({"module_id": oid})[0] # sco7
if module_is_locked(module.id):
raise ScoLockedFormError()
if not can_delete_module(module):
raise ScoNonEmptyFormationObject(
"Module",
msg=module.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
),
)
# S'il y a des moduleimpls, on ne peut pas detruire le module !
mods = sco_moduleimpl.moduleimpl_list(module_id=oid)
if mods:
err_page = f"""<h3>Destruction du module impossible car il est utilisé dans des semestres existants !</h3>
<p class="help">Il faut d'abord supprimer le semestre. Mais il est peut être préférable de
laisser ce programme intact et d'en créer une nouvelle version pour la modifier.
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce module). Mais il est peut être préférable de
laisser ce programme intact et d'en créer une nouvelle version pour la modifier sans affecter les semestres déjà en place.
</p>
<a href="{url_for('notes.ue_table', scodoc_dept=g.scodoc_dept,
formation_id=mod["formation_id"])}">reprendre</a>
@ -365,12 +387,21 @@ def do_module_delete(oid):
def module_delete(module_id=None):
"""Delete a module"""
if not module_id:
raise ScoValueError("invalid module !")
modules = module_list(args={"module_id": module_id})
if not modules:
raise ScoValueError("Module inexistant !")
mod = modules[0]
module = Module.query.get_or_404(module_id)
mod = module_list(args={"module_id": module_id})[0] # sco7
if not can_delete_module(module):
raise ScoNonEmptyFormationObject(
"Module",
msg=module.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=module.formation_id,
semestre_idx=module.ue.semestre_idx,
),
)
H = [
html_sco_header.sco_header(page_title="Suppression d'un module"),
"""<h2>Suppression du module %(titre)s (%(code)s)</h2>""" % mod,

View File

@ -42,7 +42,12 @@ from app import log
from app.scodoc.TrivialFormulator import TrivialFormulator, TF
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_permissions import Permission
from app.scodoc.sco_exceptions import ScoValueError, ScoLockedFormError
from app.scodoc.sco_exceptions import (
ScoGenError,
ScoValueError,
ScoLockedFormError,
ScoNonEmptyFormationObject,
)
from app.scodoc import html_sco_header
from app.scodoc import sco_cache
@ -130,64 +135,83 @@ def do_ue_create(args):
return ue_id
def can_delete_ue(ue: UniteEns) -> bool:
"""True si l'UE n'est pas utilisée dans des formsemestre
et n'a pas de module rattachés
"""
# "pas un seul module de cette UE n'a de modimpl...""
return (not len(ue.modules.all())) and not any(m.modimpls.all() for m in ue.modules)
def do_ue_delete(ue_id, delete_validations=False, force=False):
"delete UE and attached matieres (but not modules)"
from app.scodoc import sco_formations
from app.scodoc import sco_parcours_dut
ue = UniteEns.query.get_or_404(ue_id)
if not can_delete_ue(ue):
raise ScoNonEmptyFormationObject(
"UE",
msg=ue.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
)
cnx = ndb.GetDBConnexion()
log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue_id, delete_validations))
log("do_ue_delete: ue_id=%s, delete_validations=%s" % (ue.id, delete_validations))
# check
ue = ue_list({"ue_id": ue_id})
if not ue:
raise ScoValueError("UE inexistante !")
ue = ue[0]
if ue_is_locked(ue["ue_id"]):
raise ScoLockedFormError()
# if ue_is_locked(ue.id):
# raise ScoLockedFormError()
# Il y a-t-il des etudiants ayant validé cette UE ?
# si oui, propose de supprimer les validations
validations = sco_parcours_dut.scolar_formsemestre_validation_list(
cnx, args={"ue_id": ue_id}
cnx, args={"ue_id": ue.id}
)
if validations and not delete_validations and not force:
return scu.confirm_dialog(
"<p>%d étudiants ont validé l'UE %s (%s)</p><p>Si vous supprimez cette UE, ces validations vont être supprimées !</p>"
% (len(validations), ue["acronyme"], ue["titre"]),
% (len(validations), ue.acronyme, ue.titre),
dest_url="",
target_variable="delete_validations",
cancel_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
parameters={"ue_id": ue_id, "dialog_confirmed": 1},
parameters={"ue_id": ue.id, "dialog_confirmed": 1},
)
if delete_validations:
log("deleting all validations of UE %s" % ue_id)
log("deleting all validations of UE %s" % ue.id)
ndb.SimpleQuery(
"DELETE FROM scolar_formsemestre_validation WHERE ue_id=%(ue_id)s",
{"ue_id": ue_id},
{"ue_id": ue.id},
)
# delete all matiere in this UE
mats = sco_edit_matiere.matiere_list({"ue_id": ue_id})
mats = sco_edit_matiere.matiere_list({"ue_id": ue.id})
for mat in mats:
sco_edit_matiere.do_matiere_delete(mat["matiere_id"])
# delete uecoef and events
ndb.SimpleQuery(
"DELETE FROM notes_formsemestre_uecoef WHERE ue_id=%(ue_id)s",
{"ue_id": ue_id},
{"ue_id": ue.id},
)
ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue_id})
ndb.SimpleQuery("DELETE FROM scolar_events WHERE ue_id=%(ue_id)s", {"ue_id": ue.id})
cnx = ndb.GetDBConnexion()
_ueEditor.delete(cnx, ue_id)
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement utilisé: acceptable de tout invalider ?):
_ueEditor.delete(cnx, ue.id)
# > UE delete + supr. validations associées etudiants (cas compliqué, mais rarement
# utilisé: acceptable de tout invalider):
sco_cache.invalidate_formsemestre()
# news
F = sco_formations.formation_list(args={"formation_id": ue["formation_id"]})[0]
F = sco_formations.formation_list(args={"formation_id": ue.formation_id})[0]
sco_news.add(
typ=sco_news.NEWS_FORM,
object=ue["formation_id"],
object=ue.formation_id,
text="Modification de la formation %(acronyme)s" % F,
max_frequency=3,
)
@ -197,11 +221,11 @@ def do_ue_delete(ue_id, delete_validations=False, force=False):
url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue["formation_id"],
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
)
)
else:
return None
return None
def ue_create(formation_id=None):
@ -211,8 +235,6 @@ def ue_create(formation_id=None):
def ue_edit(ue_id=None, create=False, formation_id=None):
"""Modification ou création d'une UE"""
from app.scodoc import sco_formations
create = int(create)
if not create:
U = ue_list(args={"ue_id": ue_id})
@ -444,36 +466,50 @@ def next_ue_numero(formation_id, semestre_id=None):
def ue_delete(ue_id=None, delete_validations=False, dialog_confirmed=False):
"""Delete an UE"""
ues = ue_list(args={"ue_id": ue_id})
if not ues:
raise ScoValueError("UE inexistante !")
ue = ues[0]
if not dialog_confirmed:
return scu.confirm_dialog(
"<h2>Suppression de l'UE %(titre)s (%(acronyme)s))</h2>" % ue,
dest_url="",
parameters={"ue_id": ue_id},
cancel_url=url_for(
ue = UniteEns.query.get_or_404(ue_id)
if ue.modules.all():
raise ScoValueError(
f"""Suppression de l'UE {ue.titre} impossible car
des modules (ou SAÉ ou ressources) lui sont rattachés."""
)
if not can_delete_ue(ue):
raise ScoNonEmptyFormationObject(
"UE",
msg=ue.titre,
dest_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=str(ue["formation_id"]),
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
)
return do_ue_delete(ue_id, delete_validations=delete_validations)
if not dialog_confirmed:
return scu.confirm_dialog(
f"<h2>Suppression de l'UE {ue.titre} ({ue.acronyme})</h2>",
dest_url="",
parameters={"ue_id": ue.id},
cancel_url=url_for(
"notes.ue_table",
scodoc_dept=g.scodoc_dept,
formation_id=ue.formation_id,
semestre_idx=ue.semestre_idx,
),
)
return do_ue_delete(ue.id, delete_validations=delete_validations)
def ue_table(formation_id=None, semestre_idx=1, msg=""): # was ue_list
"""Liste des matières et modules d'une formation, avec liens pour
éditer (si non verrouillée).
"""
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_validation
formation = Formation.query.get(formation_id)
formation: Formation = Formation.query.get(formation_id)
if not formation:
raise ScoValueError("invalid formation_id")
formation.sanitize_old_formation()
parcours = formation.get_parcours()
is_apc = parcours.APC_SAE
locked = formation.has_locked_sems()
@ -1010,12 +1046,14 @@ def _ue_table_modules(
H.append(arrow_none)
im += 1
if mod["nb_moduleimpls"] == 0 and editable:
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], delete_icon)
)
icon = delete_icon
else:
H.append(delete_disabled_icon)
icon = delete_disabled_icon
H.append(
'<a class="smallbutton" href="module_delete?module_id=%s">%s</a>'
% (mod["module_id"], icon)
)
H.append("</span>")
mod_editable = (
@ -1167,7 +1205,7 @@ def do_ue_edit(args, bypass_lock=False, dont_invalidate_cache=False):
if not dont_invalidate_cache:
# Invalide les semestres utilisant cette formation:
formation.invalidate_cached_sems()
formation.force_semestre_modules_aux_ues()
formation.sanitize_old_formation()
# essai edition en ligne:

View File

@ -43,7 +43,7 @@
apo_csv_get()
API:
apo_csv_store( annee_scolaire, sem_id)
# apo_csv_store(csv_data, annee_scolaire, sem_id)
store maq file (archive)
apo_csv_get(etape_apo, annee_scolaire, sem_id, vdi_apo=None)
@ -101,7 +101,7 @@ ApoCSVArchive = ApoCSVArchiver()
def apo_csv_store(csv_data: str, annee_scolaire, sem_id):
"""
csv_data: maquette content (string)
csv_data: maquette content (str))
annee_scolaire: int (2016)
sem_id: 0 (année ?), 1 (premier semestre de l'année) ou 2 (deuxième semestre)
:return: etape_apo du fichier CSV stocké
@ -378,7 +378,7 @@ e.associate_sco( apo_data)
print apo_csv_list_stored_archives()
apo_csv_store(csv_data, annee_scolaire, sem_id)
# apo_csv_store(csv_data, annee_scolaire, sem_id)

View File

@ -48,7 +48,7 @@ from app.scodoc import sco_preferences
from app.scodoc import sco_semset
from app.scodoc import sco_etud
from app.scodoc.gen_tables import GenTable
from app.scodoc.sco_apogee_csv import APO_PORTAL_ENCODING, APO_INPUT_ENCODING
from app.scodoc.sco_apogee_csv import APO_INPUT_ENCODING, APO_OUTPUT_ENCODING
from app.scodoc.sco_exceptions import ScoValueError
@ -585,7 +585,7 @@ def _view_etuds_page(semset_id, title="", etuds=[], keys=(), format="html"):
return "\n".join(H) + html_sco_header.sco_footer()
def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
def view_apo_csv_store(semset_id="", csvfile=None, data: bytes = "", autodetect=False):
"""Store CSV data
Le semset identifie l'annee scolaire et le semestre
Si csvfile, lit depuis FILE, sinon utilise data
@ -593,9 +593,8 @@ def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
if not semset_id:
raise ValueError("invalid null semset_id")
semset = sco_semset.SemSet(semset_id=semset_id)
if csvfile:
data = csvfile.read()
data = csvfile.read() # bytes
if autodetect:
# check encoding (although documentation states that users SHOULD upload LATIN1)
data, message = sco_apogee_csv.fix_data_encoding(data)
@ -605,19 +604,26 @@ def view_apo_csv_store(semset_id="", csvfile=None, data="", autodetect=False):
log("view_apo_csv_store: autodetection of encoding disabled by user")
if not data:
raise ScoValueError("view_apo_csv_store: no data")
# data est du bytes, encodé en APO_INPUT_ENCODING
data_str = data.decode(APO_INPUT_ENCODING)
# check si etape maquette appartient bien au semset
apo_data = sco_apogee_csv.ApoData(
data, periode=semset["sem_id"]
data_str, periode=semset["sem_id"]
) # parse le fichier -> exceptions
if apo_data.etape not in semset["etapes"]:
raise ScoValueError(
"Le code étape de ce fichier ne correspond pas à ceux de cet ensemble"
)
sco_etape_apogee.apo_csv_store(data, semset["annee_scolaire"], semset["sem_id"])
sco_etape_apogee.apo_csv_store(data_str, semset["annee_scolaire"], semset["sem_id"])
return flask.redirect("apo_semset_maq_status?semset_id=" + semset_id)
return flask.redirect(
url_for(
"notes.apo_semset_maq_status",
scodoc_dept=g.scodoc_dept,
semset_id=semset_id,
)
)
def view_apo_csv_download_and_store(etape_apo="", semset_id=""):
@ -629,9 +635,9 @@ def view_apo_csv_download_and_store(etape_apo="", semset_id=""):
data = sco_portal_apogee.get_maquette_apogee(
etape=etape_apo, annee_scolaire=semset["annee_scolaire"]
)
# here, data is utf8
# here, data is str
# but we store and generate latin1 files, to ease further import in Apogée
data = data.decode(APO_PORTAL_ENCODING).encode(APO_INPUT_ENCODING) # XXX #py3
data = data.encode(APO_OUTPUT_ENCODING)
return view_apo_csv_store(semset_id, data=data, autodetect=False)
@ -669,7 +675,7 @@ def view_apo_csv(etape_apo="", semset_id="", format="html"):
sem_id = semset["sem_id"]
csv_data = sco_etape_apogee.apo_csv_get(etape_apo, annee_scolaire, sem_id)
if format == "raw":
scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
return scu.send_file(csv_data, etape_apo, suffix=".txt", mime=scu.CSV_MIMETYPE)
apo_data = sco_apogee_csv.ApoData(csv_data, periode=semset["sem_id"])

View File

@ -52,7 +52,7 @@ class InvalidNoteValue(ScoException):
# Exception qui stoque dest_url, utilisee dans Zope standard_error_message
class ScoValueError(ScoException):
def __init__(self, msg, dest_url=None):
ScoException.__init__(self, msg)
super().__init__(msg)
self.dest_url = dest_url
@ -72,20 +72,35 @@ class ScoConfigurationError(ScoValueError):
pass
class ScoLockedFormError(ScoException):
def __init__(self, msg=""):
class ScoLockedFormError(ScoValueError):
"Modification d'une formation verrouillée"
def __init__(self, msg="", dest_url=None):
msg = (
"Cette formation est verrouillée (car il y a un semestre verrouillé qui s'y réfère). "
+ str(msg)
)
ScoException.__init__(self, msg)
super().__init__(msg=msg, dest_url=dest_url)
class ScoNonEmptyFormationObject(ScoValueError):
"""On ne peut pas supprimer un module/matiere ou UE si des formsemestre s'y réfèrent"""
def __init__(self, type_objet="objet'", msg="", dest_url=None):
msg = f"""<h3>{type_objet} "{msg}" utilisé dans des semestres: suppression impossible.</h3>
<p class="help">Il faut d'abord supprimer le semestre (ou en retirer ce {type_objet}).
Mais il est peut-être préférable de laisser ce programme intact et d'en créer une
nouvelle version pour la modifier sans affecter les semestres déjà en place.
</p>
"""
super().__init__(msg=msg, dest_url=dest_url)
class ScoGenError(ScoException):
"exception avec affichage d'une page explicative ad-hoc"
def __init__(self, msg=""):
ScoException.__init__(self, msg)
super().__init__(msg)
class AccessDenied(ScoGenError):
@ -101,7 +116,7 @@ class APIInvalidParams(Exception):
status_code = 400
def __init__(self, message, status_code=None, payload=None):
Exception.__init__(self)
super().__init__()
self.message = message
if status_code is not None:
self.status_code = status_code

View File

@ -198,10 +198,13 @@ def do_formsemestre_createwithmodules(edit=False):
NB_SEM = parcours.NB_SEM
else:
NB_SEM = 10 # fallback, max 10 semestres
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
if NB_SEM == 1:
semestre_id_list = [-1]
else:
semestre_id_list = [-1] + list(range(1, NB_SEM + 1))
semestre_id_labels = []
for sid in semestre_id_list:
if sid == "-1":
if sid == -1:
semestre_id_labels.append("pas de semestres")
else:
semestre_id_labels.append(f"S{sid}")
@ -329,6 +332,8 @@ def do_formsemestre_createwithmodules(edit=False):
"labels": modalites_titles,
},
),
]
modform.append(
(
"semestre_id",
{
@ -338,7 +343,7 @@ def do_formsemestre_createwithmodules(edit=False):
"labels": semestre_id_labels,
},
),
]
)
etapes = sco_portal_apogee.get_etapes_apogee_dept()
# Propose les etapes renvoyées par le portail
# et ajoute les étapes du semestre qui ne sont pas dans la liste (soit la liste a changé, soit l'étape a été ajoutée manuellement)

View File

@ -292,6 +292,8 @@ def formsemestre_inscr_passage(
etuds = [int(x) for x in etuds.split(",") if x]
elif isinstance(etuds, int):
etuds = [etuds]
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)
etuds_set = set(etuds)

View File

@ -544,7 +544,7 @@ def check_paiement_etuds(etuds):
etud["paiementinscription_str"] = "(pb cnx Apogée)"
def get_maquette_apogee(etape="", annee_scolaire=""):
def get_maquette_apogee(etape="", annee_scolaire="") -> str:
"""Maquette CSV Apogee pour une étape et une annee scolaire"""
maquette_url = get_maquette_url()
if not maquette_url:

View File

@ -279,6 +279,7 @@ class BasePreferences(object):
{
"initvalue": "Dept",
"title": "Nom abrégé du département",
"explanation": "acronyme: par exemple R&T, ORTF, HAL",
"size": 12,
"category": "general",
"only_global": True,
@ -289,7 +290,7 @@ class BasePreferences(object):
{
"initvalue": "nom du département",
"title": "Nom complet du département",
"explanation": "inutilisé par défaut",
"explanation": "apparaît sur la page d'accueil",
"size": 40,
"category": "general",
"only_global": True,

View File

@ -49,16 +49,12 @@ from app.scodoc import sco_etud
from app.scodoc import sco_excel
from app.scodoc import sco_formsemestre
from app.scodoc import sco_formsemestre_inscriptions
from app.scodoc import sco_formsemestre_status
from app.scodoc import sco_parcours_dut
from app.scodoc import sco_pdf
from app.scodoc import sco_preferences
import sco_version
from app.scodoc.gen_tables import GenTable
from app import log
from app.scodoc.sco_codes_parcours import code_semestre_validant
from app.scodoc.sco_exceptions import ScoValueError
from app.scodoc.sco_pdf import SU
MAX_ETUD_IN_DESCR = 20
@ -121,9 +117,9 @@ def _categories_and_results(etuds, category, result):
categories[etud[category]] = True
results[etud[result]] = True
categories = list(categories.keys())
categories.sort()
categories.sort(key=scu.heterogeneous_sorting_key)
results = list(results.keys())
results.sort()
results.sort(key=scu.heterogeneous_sorting_key)
return categories, results
@ -166,7 +162,7 @@ def _results_by_category(
l["sumpercent"] = "%2.1f%%" % ((100.0 * l["sum"]) / tot)
#
codes = list(results.keys())
codes.sort()
codes.sort(key=scu.heterogeneous_sorting_key)
bottom_titles = []
if C: # ligne du bas avec totaux:
@ -314,7 +310,7 @@ def formsemestre_report_counts(
"type_admission",
"boursier_prec",
]
keys.sort()
keys.sort(key=scu.heterogeneous_sorting_key)
F = [
"""<form name="f" method="get" action="%s"><p>
Colonnes: <select name="result" onchange="document.f.submit()">"""
@ -497,7 +493,7 @@ def table_suivi_cohorte(
P.append(p)
# 4-- regroupe par indice de semestre S_i
indices_sems = list(set([s["semestre_id"] for s in sems]))
indices_sems = list({s["semestre_id"] for s in sems})
indices_sems.sort()
for p in P:
p.nb_etuds = 0 # nombre total d'etudiants dans la periode
@ -788,9 +784,9 @@ def _gen_form_selectetuds(
):
"""HTML form pour choix criteres selection etudiants"""
bacs = list(bacs)
bacs.sort()
bacs.sort(key=scu.heterogeneous_sorting_key)
bacspecialites = list(bacspecialites)
bacspecialites.sort()
bacspecialites.sort(key=scu.heterogeneous_sorting_key)
# on peut avoir un mix de chaines vides et d'entiers:
annee_bacs = [int(x) if x else 0 for x in annee_bacs]
annee_bacs.sort()

View File

@ -395,6 +395,8 @@ def do_semset_add_sem(semset_id, formsemestre_id):
"""Add a sem to a semset"""
if not semset_id:
raise ScoValueError("empty semset_id")
if formsemestre_id == "":
raise ScoValueError("pas de semestre choisi !")
s = SemSet(semset_id=semset_id)
# check for valid formsemestre_id
_ = sco_formsemestre.get_formsemestre(formsemestre_id) # raise exc

View File

@ -93,6 +93,7 @@ MODULE_TYPE_NAMES = {
ModuleType.MALUS: "Malus",
ModuleType.RESSOURCE: "Ressource",
ModuleType.SAE: "SAÉ",
None: "Module",
}
MALUS_MAX = 20.0
@ -897,6 +898,11 @@ def sort_dates(L, reverse=False):
raise
def heterogeneous_sorting_key(x):
"key to sort non homogeneous sequences"
return (float(x), "") if isinstance(x, (bool, float, int)) else (-1e34, str(x))
def query_portal(req, msg="Portail Apogee", timeout=3):
"""Retreives external data using HTTP request
(used to connect to Apogee portal, or ScoDoc server)

View File

@ -239,6 +239,17 @@ div.box-chercheetud {
margin-top: 12px;
}
/* Page accueil général */
span.dept_full_name {
font-style: italic;
}
span.dept_visible {
color: rgb(6, 158, 6);
}
span.dept_cache {
color: rgb(194, 5, 5);
}
div.table_etud_in_accessible_depts {
margin-left: 3em;
margin-bottom: 2em;

View File

@ -23,7 +23,7 @@
{% block app_content %}
<h1>Modification du compte ScoDoc <tt>{{form.user_name.data}}</tt></h1>
<div class="help">
<p>Identifiez-vous avez votre mot de passe actuel</p>
<p>Identifiez-vous avec votre mot de passe actuel</p>
</div>
<form method=post>
{{ form.user_name }}

View File

@ -23,14 +23,11 @@
{{icons.arrow_none|safe}}
{% endif %}
</span>
{% if editable and not ue.modules.count() %}
<a class="smallbutton" href="{{ url_for('notes.ue_delete',
scodoc_dept=g.scodoc_dept, ue_id=ue.id)
}}">{{icons.delete|safe}}</a>
{% else %}
{{icons.delete_disabled|safe}}
{% endif %}
}}">{% if editable and not ue.modules.count() %}{{icons.delete|safe}}{% else %}{{icons.delete_disabled|safe}}{% endif %}</a>
<b>{{ue.acronyme}}</b> <a class="discretelink" href="{{
url_for('notes.ue_infos', scodoc_dept=g.scodoc_dept, ue_id=ue.id)}}"
>{{ue.titre}}</a>

View File

@ -7,10 +7,9 @@
{{ exc | safe }}
<p class="footer">
<p>
{% if g.scodoc_dept %}
<a href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">retour page d'accueil
departement {{ g.scodoc_dept }}</a>
<a href="{{ exc.dest_url or url_for('scolar.index_html', scodoc_dept=g.scodoc_dept) }}">continuer</a>
{% else %}
<a href="{{ exc.dest_url or url_for('scodoc.index') }}">retour page d'accueil</a>
{% endif %}

View File

@ -12,11 +12,25 @@
<ul class="main">
{% for dept in depts %}
{% if dept.visible or current_user.is_administrator() %}
<li>
<a class="stdlink {{'link_accessible' if current_user.has_permission(Permission.ScoView, dept=dept.acronym) else 'link_unauthorized'}}"
href="{{url_for('scolar.index_html', scodoc_dept=dept.acronym)}}">Département
{{dept.preferences.filter_by(name="DeptName").first().value}}</a>
{{dept.preferences.filter_by(name="DeptName").first().value}}
</a>
<span class="dept_full_name">
{{ dept.preferences.filter_by( name="DeptFullName" ).first().value or "" }}
</span>
{% if current_user.is_administrator() %}
<span {% if dept.visible %}class="dept_visible">visible{% else %}class="dept_cache">caché aux utilisateurs{% endif %}
</span>
<a href="{{ url_for('scodoc.toggle_dept_vis', dept_id=dept.id) }}">
{% if dept.visible %}cacher{% else %}rendre visible{% endif %}
</a>
</span>
{% endif %}
</li>
{% endif %}
{% else %}
<li>
<b>Aucun département défini !</b>

View File

@ -53,6 +53,7 @@ from wtforms.fields.simple import BooleanField, StringField, TextAreaField, Hidd
from wtforms.validators import ValidationError, DataRequired, Email, EqualTo
import app
from app import db
from app.forms.main import config_forms
from app.forms.main.create_dept import CreateDeptForm
from app.models import Departement, Identite
@ -82,9 +83,7 @@ from PIL import Image as PILImage
@bp.route("/ScoDoc/index")
def index():
"Page d'accueil: liste des départements"
depts = (
Departement.query.filter_by(visible=True).order_by(Departement.acronym).all()
)
depts = Departement.query.filter_by().order_by(Departement.acronym).all()
return render_template(
"scodoc.html",
title=sco_version.SCONAME,
@ -108,7 +107,11 @@ def create_dept():
if request.method == "POST" and form.cancel.data: # cancel button
return redirect(url_for("scodoc.index"))
if form.validate_on_submit():
departements.create_dept(form.acronym.data)
departements.create_dept(
form.acronym.data,
visible=form.visible.data,
# description=form.description.data,
)
flash(f"Département {form.acronym.data} créé.")
return redirect(url_for("scodoc.index"))
return render_template(
@ -118,6 +121,17 @@ def create_dept():
)
@bp.route("/ScoDoc/toggle_dept_vis/<dept_id>", methods=["GET", "POST"])
@admin_required
def toggle_dept_vis(dept_id):
"""Cache ou rend visible un dept"""
dept = Departement.query.get_or_404(dept_id)
dept.visible = not dept.visible
db.session.add(dept)
db.session.commit()
return redirect(url_for("scodoc.index"))
@bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required
def table_etud_in_accessible_depts():

View File

@ -152,7 +152,6 @@ def user_info(user_name, format="json"):
def create_user_form(user_name=None, edit=0, all_roles=1):
"form. création ou edition utilisateur"
auth_dept = current_user.dept
auth_username = current_user.user_name
from_mail = current_user.email
initvalues = {}
edit = int(edit)
@ -204,7 +203,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
administrable_dept_acronyms = sorted(
set(
[
x.dept
x.dept or ""
for x in UserRole.query.filter_by(user=current_user)
if x.role.has_permission(Permission.ScoUsersAdmin) and x.dept
]
@ -249,7 +248,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
r.name + "_" + (dept or "") for (r, dept) in displayed_roles
]
displayed_roles_labels = [f"{dept}: {r.name}" for (r, dept) in displayed_roles]
disabled_roles = {} # pour desactiver les roles que l'on ne peut pas editer
disabled_roles = {} # pour désactiver les roles que l'on ne peut pas éditer
for i in range(len(displayed_roles_strings)):
if displayed_roles_strings[i] not in editable_roles_strings:
disabled_roles[i] = True
@ -375,7 +374,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
can_choose_dept = True
else:
selectable_dept_acronyms = set(administrable_dept_acronyms)
if edit: # ajoute dept actuel de l'utilisateur
if edit and the_user.dept is not None: # ajoute dept actuel de l'utilisateur
selectable_dept_acronyms |= {the_user.dept}
if len(selectable_dept_acronyms) > 1:
can_choose_dept = True
@ -389,6 +388,9 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
"explanation": """département de rattachement de l'utilisateur""",
"labels": selectable_dept_acronyms,
"allowed_values": selectable_dept_acronyms,
"default": g.scodoc_dept
if g.scodoc_dept in selectable_dept_acronyms
else "",
},
)
)
@ -541,10 +543,10 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
vals["active"] = vals["status"] == ""
# Département:
if auth_dept: # pas super-admin
if vals["dept"] not in selectable_dept_acronyms:
if ("dept" in vals) and (vals["dept"] not in selectable_dept_acronyms):
del vals["dept"] # ne change pas de dept
# traitement des roles: ne doit pas affecter les roles
# que l'on en controle pas:
# Traitement des roles: ne doit pas affecter les rôles
# que l'on en contrôle pas:
for role in orig_roles_strings: # { "Ens_RT", "Secr_CJ", ... }
if role and not role in editable_roles_strings:
roles.add(role)
@ -573,7 +575,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
# A: envoi de welcome + procedure de reset
# B: envoi de welcome seulement (mot de passe saisie dans le formulaire)
# C: Aucun envoi (mot de passe saisi dans le formulaire)
if vals["welcome:list"] == "1":
if vals["welcome"] == "1":
if vals["reset_password:list"] == "1":
mode = Mode.WELCOME_AND_CHANGE_PASSWORD
else:
@ -751,7 +753,7 @@ def user_info_page(user_name=None):
if not user_name:
user = current_user
else:
user = User.query.filter_by(user_name=user_name).first()
user = User.query.filter_by(user_name=str(user_name)).first()
if not user:
raise ScoValueError("invalid user_name")

View File

@ -1,7 +1,7 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
SCOVERSION = "9.1.16"
SCOVERSION = "9.1.19"
SCONAME = "ScoDoc"

View File

@ -42,17 +42,17 @@
# - do_formation_delete
import json
import xml.dom.minidom
import flask
from flask import g
import pytest
from tests.unit import sco_fake_gen
from app.scodoc import sco_edit_formation
from app.scodoc import sco_edit_matiere
from app.scodoc import sco_edit_module
from app.scodoc import sco_edit_ue
from app.scodoc import sco_exceptions
from app.scodoc import sco_formations
from app.scodoc import sco_formsemestre_edit
from app.scodoc import sco_moduleimpl
@ -273,31 +273,31 @@ def test_formations(test_client):
# --- Suppression du module, matiere et ue test du semestre 2
# on doit d'abbord supprimer le semestre
# sco_formsemestre_edit.formsemestre_delete( formsemestre_id=sem2["formsemestre_id"])
# sco_formsemestre_edit.formsemestre_createwithmodules( formsemestre_id=sem2["formsemestre_id"])
# RIEN NE SE PASSE AVEC CES FONCTIONS
# on doit d'abord supprimer le semestre:
sco_formsemestre_edit.do_formsemestre_delete(
formsemestre_id=sem2["formsemestre_id"]
)
# sco_edit_module.module_delete( module_id=modt["module_id"])
# sco_edit_matiere.matiere_delete( matiere_id=matt["matiere_id"])
# sco_edit_ue.ue_delete( ue_id=uet["ue_id"])
# RIEN NE SE PASSE AVEC CES FONCTIONS
li_module = sco_edit_module.module_list()
assert len(li_module) == 4
sco_edit_module.do_module_delete(oid=modt["module_id"]) # on supprime le semestre
# sco_formsemestre_edit.formsemestre_delete_moduleimpls( formsemestre_id=sem2["formsemestre_id"], module_ids_to_del=[modt["module_id"]])
# deuxieme methode de supression d'un module
li_module2 = sco_edit_module.module_list()
# Suppression impossible car utilisé dans le semestre semt:
with pytest.raises(sco_exceptions.ScoNonEmptyFormationObject):
sco_edit_module.module_delete(module_id=mi3["module_id"])
assert len(li_module2) == 3 # verification de la suppression du module
sco_formsemestre_edit.do_formsemestre_delete(semt["formsemestre_id"])
li_module2_before = sco_edit_module.module_list()
sco_edit_module.do_module_delete(mi3["module_id"])
sco_edit_module.do_module_delete(modt["module_id"])
# deuxieme methode de supression d'un module
li_module2_after = sco_edit_module.module_list()
assert (
len(li_module2_after) == len(li_module2_before) - 2
) # verification de la suppression
lim_sem2 = sco_moduleimpl.moduleimpl_list(formsemestre_id=sem2["formsemestre_id"])
@ -316,10 +316,6 @@ def test_formations(test_client):
assert len(li_ue2) == 3 # verification de la suppression de l'UE
# --- Suppression d'une formation
# Il faut d'abbord supprimer le semestre aussi.
sco_formsemestre_edit.do_formsemestre_delete(
formsemestre_id=semt["formsemestre_id"]
)
sco_edit_formation.do_formation_delete(oid=f2["formation_id"])
lif3 = notes.formation_list(format="json").get_data(as_text=True)

View File

@ -24,53 +24,74 @@ usage() {
exit 1
}
# analyse de la ligne de commande
# calcule:
# SRC = fichier source de la restauration
# DB_DEST = base de données destination
# KEEP_ENV = vide si restauration à l'identique (i.e. production)
if (($# < 1 || $# > 2))
then
usage
elif [ $# -eq 2 -a $1 != '--keep-env' -a $2 != '--keep-env' ] ; then
elif [ $# -eq 2 ] && [ "$1" != "--keep-env" ] && [ "$2" != "--keep-env" ]
then
usage
elif [ $# -eq 1 ] ; then
elif [ $# -eq 1 ]
then
echo "restauration des données et de la configuration originale (production)"
SRC=$1
SRC="$1"
DB_DEST="SCODOC"
else
echo "restauration des données dans la configuration actuelle"
DB_CURRENT=$(su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask scodoc-database -n)")
DB_DEST="$DB_CURRENT"
KEEP=1
if [ $1 = '--keep-env' ]; then
SRC=$2
KEEP_ENV="Y"
if [ "$1" = "--keep-env" ]
then
SRC="$2"
else
SRC=$1
SRC="$1"
fi
fi
DB_DUMP="${SCODOC_VAR_DIR}"/SCODOC.dump
# Safety check
echo "Ce script va remplacer les donnees de votre installation ScoDoc par celles"
echo "Ce script va remplacer les données de votre installation ScoDoc par celles"
echo "enregistrées dans le fichier fourni."
echo "Ce fichier doit avoir ete cree par le script save_scodoc9_data.sh."
echo "Ce fichier doit avoir été créé par le script save_scodoc9_data.sh."
echo
echo "Attention: TOUTES LES DONNEES DE CE SCODOC SERONT REMPLACEES !"
echo "Notamment, tous les utilisateurs et departements existants seront effaces !"
echo "Notamment, tous les utilisateurs et départements existants seront effacés !"
echo
echo "La base SQL $DB_CURRENT sera effacée et remplacée !!!"
echo
echo -n "Voulez vous poursuivre cette operation ? (y/n) [n]"
read -r ans
if [ ! "$(norm_ans "$ans")" = 'Y' ]
# Préparation si une copie 'antique' doit être effacée, demander confirmation, puis effacer
SCODOC_VAR_OLD=${SCODOC_VAR_DIR}.old
if [ -e "$SCODOC_VAR_DIR" ] && [ -e "$SCODOC_VAR_OLD" ]
then
echo "Annulation"
echo "Une ancienne sauvegarde (\"$SCODOC_VAR_OLD\" en date du $(stat -c %w "$SCODOC_VAR_OLD") ) va être effacée."
echo
fi
if [ -n "$KEEP_ENV" ]
then
echo -n "Restauration des données sans changement de configuration: Assurez-vous d'avoir arrêté le serveur scodoc."
echo
fi
echo -n "Voulez-vous poursuivre la restauration ? (y/n) [n]"
read -r ans
if [ ! "$(norm_ans "$ans")" = "Y" ]
then
echo "Annulation de la restauration par l\'utilisateur"
exit 1
fi
rm -rf "$SCODOC_VAR_OLD" || die "Erreur suppression $SCODOC_VAR_OLD"
# -- Stop ScoDoc
if [ $KEEP -ne 1 ]; then
if [ -z "$KEEP_ENV" ]
then
echo "Arrêt de scodoc9..."
systemctl stop scodoc9
else
echo -n "Assurez-vous d'avoir arrété le serveur scodoc (validez pour continuer)"
read ans
fi
# Clear caches
@ -86,7 +107,7 @@ fi
# -- Ouverture archive
echo "Ouverture archive $SRC..."
(cd $(dirname "$SCODOC_VAR_DIR"); tar xfz "$SRC") || die "Error opening archive"
(cd "$(dirname "$SCODOC_VAR_DIR")"; tar xfz "$SRC") || die "Error opening archive"
# -- Ckeck/fix owner
echo "Vérification du propriétaire..."
@ -103,7 +124,7 @@ su -c "createdb $DB_DEST" "$SCODOC_USER" || die "Erreur création db"
if [ ! -z $KEEP_ENV ] ; then
echo "conservation de la configuration actuelle"
cp "$SCODOC_VAR_DIR".old/.env "$SCODOC_VAR_DIR"/.env
cp -p "$SCODOC_VAR_OLD"/.env "$SCODOC_VAR_DIR"/.env
echo "récupération des données..."
su -c "pg_restore -f - $DB_DUMP | psql -q $DB_DEST" "$SCODOC_USER" >/dev/null || die "Erreur chargement/renommage de la base SQL"
su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade)" "$SCODOC_USER"
@ -119,5 +140,4 @@ else
systemctl start scodoc9
fi
echo "Terminé."