This commit is contained in:
IDK 2021-10-11 16:26:22 +02:00
commit aed2d6ce10
49 changed files with 726 additions and 278 deletions

View File

@ -106,13 +106,15 @@ Ou avec couverture (`pip install pytest-cov`)
#### Utilisation des tests unitaires pour initialiser la base de dev #### Utilisation des tests unitaires pour initialiser la base de dev
On peut aussi utiliser les tests unitaires pour mettre la base On peut aussi utiliser les tests unitaires pour mettre la base
de données de développement dans un état connu, par exemple pour éviter de recréer à la main étudianst et semestres quand on développe. de données de développement dans un état connu, par exemple pour éviter de
recréer à la main étudiants et semestres quand on développe.
Il suffit de positionner une variable d'environnement indiquant la BD utilisée par les tests: Il suffit de positionner une variable d'environnement indiquant la BD utilisée par les tests:
export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV export SCODOC_TEST_DATABASE_URI=postgresql:///SCODOC_DEV
puis de les lancer normalement, par exemple: (si elle n'existe pas, voir plus loin pour la créer) puis de les lancer
normalement, par exemple:
pytest tests/unit/test_sco_basic.py pytest tests/unit/test_sco_basic.py
@ -133,7 +135,8 @@ On utilise SQLAlchemy avec Alembic et Flask-Migrate.
Ne pas oublier de commiter les migrations (`git add migrations` ...). Ne pas oublier de commiter les migrations (`git add migrations` ...).
Mémo pour développeurs: séquence re-création d'une base: Mémo pour développeurs: séquence re-création d'une base (vérifiez votre `.env`
ou variables d'environnement pour interroger la bonne base !).
dropdb SCODOC_DEV dropdb SCODOC_DEV
tools/create_database.sh SCODOC_DEV # créé base SQL tools/create_database.sh SCODOC_DEV # créé base SQL
@ -148,7 +151,25 @@ Si la base utilisée pour les dev n'est plus en phase avec les scripts de
migration, utiliser les commandes `flask db history`et `flask db stamp`pour se migration, utiliser les commandes `flask db history`et `flask db stamp`pour se
positionner à la bonne étape. positionner à la bonne étape.
# Paquet debian 11 ### Profiling
Sur une machine de DEV, lancer
flask profile --host 0.0.0.0 --length 32 --profile-dir /opt/scodoc-data
le fichier `.prof` sera alors écrit dans `/opt/scoidoc-data` (on peut aussi utiliser `/tmp`).
Pour la visualisation, [snakeviz](https://jiffyclub.github.io/snakeviz/) est bien:
pip install snakeviz
puis
snakeviz -s --hostname 0.0.0.0 -p 5555 /opt/scodoc-data/GET.ScoDoc......prof
# Paquet Debian 11
Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus Les scripts associés au paquet Debian (.deb) sont dans `tools/debian`. Le plus
important est `postinst`qui se charge de configurer le système (install ou important est `postinst`qui se charge de configurer le système (install ou

View File

@ -1,6 +1,7 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
# pylint: disable=invalid-name # pylint: disable=invalid-name
import datetime
import os import os
import socket import socket
import sys import sys
@ -24,7 +25,12 @@ from flask_moment import Moment
from flask_caching import Cache from flask_caching import Cache
import sqlalchemy import sqlalchemy
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError, APIInvalidParams from app.scodoc.sco_exceptions import (
AccessDenied,
ScoGenError,
ScoValueError,
APIInvalidParams,
)
from config import DevConfig from config import DevConfig
import sco_version import sco_version
@ -50,10 +56,21 @@ def handle_sco_value_error(exc):
return render_template("sco_value_error.html", exc=exc), 404 return render_template("sco_value_error.html", exc=exc), 404
def handle_access_denied(exc):
return render_template("error_access_denied.html", exc=exc), 403
def internal_server_error(e): def internal_server_error(e):
"""Bugs scodoc, erreurs 500""" """Bugs scodoc, erreurs 500"""
# note that we set the 500 status explicitly # note that we set the 500 status explicitly
return render_template("error_500.html", SCOVERSION=sco_version.SCOVERSION), 500 return (
render_template(
"error_500.html",
SCOVERSION=sco_version.SCOVERSION,
date=datetime.datetime.now().isoformat(),
),
500,
)
def handle_invalid_usage(error): def handle_invalid_usage(error):
@ -93,6 +110,10 @@ class LogRequestFormatter(logging.Formatter):
record.url = None record.url = None
record.remote_addr = None record.remote_addr = None
record.sco_user = current_user record.sco_user = current_user
if has_request_context():
record.sco_admin_mail = current_app.config["SCODOC_ADMIN_MAIL"]
else:
record.sco_admin_mail = "(pas de requête)"
return super().format(record) return super().format(record)
@ -121,6 +142,10 @@ class LogExceptionFormatter(logging.Formatter):
record.http_params = None record.http_params = None
record.sco_user = current_user record.sco_user = current_user
if has_request_context():
record.sco_admin_mail = current_app.config["SCODOC_ADMIN_MAIL"]
else:
record.sco_admin_mail = "(pas de requête)"
return super().format(record) return super().format(record)
@ -165,6 +190,7 @@ def create_app(config_class=DevConfig):
app.register_error_handler(ScoGenError, handle_sco_value_error) app.register_error_handler(ScoGenError, handle_sco_value_error)
app.register_error_handler(ScoValueError, 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(500, internal_server_error)
app.register_error_handler(503, postgresql_server_error) app.register_error_handler(503, postgresql_server_error)
app.register_error_handler(APIInvalidParams, handle_invalid_usage) app.register_error_handler(APIInvalidParams, handle_invalid_usage)
@ -197,12 +223,14 @@ def create_app(config_class=DevConfig):
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n" "[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
"%(levelname)s: %(message)s" "%(levelname)s: %(message)s"
) )
# les champs additionnels sont définis dans LogRequestFormatter
scodoc_exc_formatter = LogExceptionFormatter( scodoc_exc_formatter = LogExceptionFormatter(
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n" "[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
"%(levelname)s: %(message)s\n" "%(levelname)s: %(message)s\n"
"Referrer: %(http_referrer)s\n" "Referrer: %(http_referrer)s\n"
"Method: %(http_method)s\n" "Method: %(http_method)s\n"
"Params: %(http_params)s\n" "Params: %(http_params)s\n"
"Admin mail: %(sco_admin_mail)s\n"
) )
if not app.testing: if not app.testing:
if not app.debug: if not app.debug:
@ -259,15 +287,19 @@ def create_app(config_class=DevConfig):
) )
# ---- INITIALISATION SPECIFIQUES A SCODOC # ---- INITIALISATION SPECIFIQUES A SCODOC
from app.scodoc import sco_bulletins_generator from app.scodoc import sco_bulletins_generator
from app.scodoc.sco_bulletins_example import BulletinGeneratorExample
from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy from app.scodoc.sco_bulletins_legacy import BulletinGeneratorLegacy
from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard from app.scodoc.sco_bulletins_standard import BulletinGeneratorStandard
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample) # l'ordre est important, le premeir sera le "défaut" pour les nouveaux départements.
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC) sco_bulletins_generator.register_bulletin_class(BulletinGeneratorUCAC)
if app.testing or app.debug:
from app.scodoc.sco_bulletins_example import BulletinGeneratorExample
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample)
return app return app

View File

@ -422,7 +422,7 @@ class UserRole(db.Model):
def get_super_admin(): def get_super_admin():
"""L'utilisateur admin (où le premier, s'il y en a plusieurs). """L'utilisateur admin (ou le premier, s'il y en a plusieurs).
Utilisé par les tests unitaires et le script de migration. Utilisé par les tests unitaires et le script de migration.
""" """
admin_role = Role.query.filter_by(name="SuperAdmin").first() admin_role = Role.query.filter_by(name="SuperAdmin").first()

View File

@ -273,6 +273,7 @@ class NotesModuleImplInscription(db.Model):
"""Inscription à un module (etudiants,moduleimpl)""" """Inscription à un module (etudiants,moduleimpl)"""
__tablename__ = "notes_moduleimpl_inscription" __tablename__ = "notes_moduleimpl_inscription"
__table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),)
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
moduleimpl_inscription_id = db.synonym("id") moduleimpl_inscription_id = db.synonym("id")

View File

@ -40,7 +40,7 @@ from app.scodoc.sco_permissions import Permission
def sidebar_common(): def sidebar_common():
"partie commune à toutes les sidebar" "partie commune à toutes les sidebar"
H = [ H = [
f"""<a class="scodoc_title" href="about">ScoDoc 9</a> f"""<a class="scodoc_title" href="{url_for("scodoc.about", scodoc_dept=g.scodoc_dept)}">ScoDoc 9</a>
<div id="authuser"><a id="authuserlink" href="{ <div id="authuser"><a id="authuserlink" href="{
url_for("users.user_info_page", url_for("users.user_info_page",
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name) scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)

View File

@ -1265,7 +1265,7 @@ class NotesTable(object):
), ),
self.get_nom_long(etudid), self.get_nom_long(etudid),
url_for( url_for(
"scolar.formsemestre_edit_uecoefs", "notes.formsemestre_edit_uecoefs",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
formsemestre_id=self.formsemestre_id, formsemestre_id=self.formsemestre_id,
err_ue_id=ue["ue_id"], err_ue_id=ue["ue_id"],

View File

@ -136,7 +136,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
prefs = sco_preferences.SemPreferences(formsemestre_id) prefs = sco_preferences.SemPreferences(formsemestre_id)
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes nt = sco_cache.NotesTableCache.get(formsemestre_id) # > toutes notes
if not nt.get_etud_etat(etudid):
raise ScoValueError("Etudiant non inscrit à ce semestre")
I = scu.DictDefault(defaultvalue="") I = scu.DictDefault(defaultvalue="")
I["etudid"] = etudid I["etudid"] = etudid
I["formsemestre_id"] = formsemestre_id I["formsemestre_id"] = formsemestre_id
@ -774,8 +775,8 @@ def formsemestre_bulletinetud(
except: except:
sco_etud.log_unknown_etud() sco_etud.log_unknown_etud()
raise ScoValueError("étudiant inconnu") raise ScoValueError("étudiant inconnu")
# API, donc erreurs admises en ScoValueError
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
bulletin = do_formsemestre_bulletinetud( bulletin = do_formsemestre_bulletinetud(
formsemestre_id, formsemestre_id,

View File

@ -348,7 +348,7 @@ def do_moduleimpl_moyennes(nt, mod):
if etudid in eval_rattr["notes"]: if etudid in eval_rattr["notes"]:
note = eval_rattr["notes"][etudid]["value"] note = eval_rattr["notes"][etudid]["value"]
if note != None and note != NOTES_NEUTRALISE and note != NOTES_ATTENTE: if note != None and note != NOTES_NEUTRALISE and note != NOTES_ATTENTE:
if isinstance(R[etudid], float): if not isinstance(R[etudid], float):
R[etudid] = note R[etudid] = note
else: else:
note_sur_20 = note * 20.0 / eval_rattr["note_max"] note_sur_20 = note * 20.0 / eval_rattr["note_max"]

View File

@ -48,9 +48,19 @@ import sco_version
def report_debouche_date(start_year=None, format="html"): def report_debouche_date(start_year=None, format="html"):
"""Rapport (table) pour les débouchés des étudiants sortis à partir de l'année indiquée.""" """Rapport (table) pour les débouchés des étudiants sortis
à partir de l'année indiquée.
"""
if not start_year: if not start_year:
return report_debouche_ask_date() return report_debouche_ask_date("Année de début de la recherche")
else:
try:
start_year = int(start_year)
except ValueError:
return report_debouche_ask_date(
"Année invalide. Année de début de la recherche"
)
if format == "xls": if format == "xls":
keep_numeric = True # pas de conversion des notes en strings keep_numeric = True # pas de conversion des notes en strings
else: else:
@ -96,8 +106,9 @@ def get_etudids_with_debouche(start_year):
FROM notes_formsemestre_inscription i, notes_formsemestre s, itemsuivi it FROM notes_formsemestre_inscription i, notes_formsemestre s, itemsuivi it
WHERE i.etudid = it.etudid WHERE i.etudid = it.etudid
AND i.formsemestre_id = s.id AND s.date_fin >= %(start_date)s AND i.formsemestre_id = s.id AND s.date_fin >= %(start_date)s
AND s.dept_id = %(dept_id)s
""", """,
{"start_date": start_date}, {"start_date": start_date, "dept_id": g.scodoc_dept_id},
) )
return [x["etudid"] for x in r] return [x["etudid"] for x in r]
@ -193,15 +204,16 @@ def table_debouche_etudids(etudids, keep_numeric=True):
return tab return tab
def report_debouche_ask_date(): def report_debouche_ask_date(msg: str) -> str:
"""Formulaire demande date départ""" """Formulaire demande date départ"""
return ( return f"""{html_sco_header.sco_header()}
html_sco_header.sco_header() <h2>Table des débouchés des étudiants</h2>
+ """<form method="GET"> <form method="GET">
Date de départ de la recherche: <input type="text" name="start_year" value="" size=10/> {msg}
</form>""" <input type="text" name="start_year" value="" size=10/>
+ html_sco_header.sco_footer() </form>
) {html_sco_header.sco_footer()}
"""
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------

View File

@ -48,6 +48,7 @@ from app.scodoc import sco_users
def index_html(showcodes=0, showsemtable=0): def index_html(showcodes=0, showsemtable=0):
"Page accueil département (liste des semestres)" "Page accueil département (liste des semestres)"
showcodes = int(showcodes)
showsemtable = int(showsemtable) showsemtable = int(showsemtable)
H = [] H = []
@ -78,7 +79,7 @@ def index_html(showcodes=0, showsemtable=0):
# Responsable de formation: # Responsable de formation:
sco_formsemestre.sem_set_responsable_name(sem) sco_formsemestre.sem_set_responsable_name(sem)
if showcodes == "1": if showcodes:
sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"] sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"]
else: else:
sem["tmpcode"] = "" sem["tmpcode"] = ""
@ -126,7 +127,7 @@ def index_html(showcodes=0, showsemtable=0):
""" """
% sco_preferences.get_preference("DeptName") % sco_preferences.get_preference("DeptName")
) )
H.append(_sem_table_gt(sems).html()) H.append(_sem_table_gt(sems, showcodes=showcodes).html())
H.append("</table>") H.append("</table>")
if not showsemtable: if not showsemtable:
H.append( H.append(

View File

@ -845,6 +845,7 @@ def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
""" """
from app.scodoc import sco_formations from app.scodoc import sco_formations
ue_code = str(ue_code)
if ue_id: if ue_id:
ue = do_ue_list(args={"ue_id": ue_id})[0] ue = do_ue_list(args={"ue_id": ue_id})[0]
if not ue_code: if not ue_code:

View File

@ -640,7 +640,7 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
if not semset_id: if not semset_id:
raise ValueError("invalid null semset_id") raise ValueError("invalid null semset_id")
semset = sco_semset.SemSet(semset_id=semset_id) semset = sco_semset.SemSet(semset_id=semset_id)
dest_url = "apo_semset_maq_status?semset_id=" + semset_id dest_url = f"apo_semset_maq_status?semset_id={semset_id}"
if not dialog_confirmed: if not dialog_confirmed:
return scu.confirm_dialog( return scu.confirm_dialog(
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2> """<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>

View File

@ -655,7 +655,7 @@ def log_unknown_etud():
def get_etud_info(etudid=False, code_nip=False, filled=False) -> list: def get_etud_info(etudid=False, code_nip=False, filled=False) -> list:
"""infos sur un etudiant (API). If not foud, returns empty list. """infos sur un etudiant (API). If not found, returns empty list.
On peut specifier etudid ou code_nip On peut specifier etudid ou code_nip
ou bien cherche dans les argumenst de la requête courante: ou bien cherche dans les argumenst de la requête courante:
etudid, code_nip, code_ine (dans cet ordre). etudid, code_nip, code_ine (dans cet ordre).
@ -671,6 +671,19 @@ def get_etud_info(etudid=False, code_nip=False, filled=False) -> list:
return etud return etud
# Optim par cache local, utilité non prouvée mais
# on s'oriente vers un cahce persistent dans Redis ou bien l'utilisation de NT
# def get_etud_info_filled_by_etudid(etudid, cnx=None) -> dict:
# """Infos sur un étudiant, avec cache local à la requête"""
# if etudid in g.stored_etud_info:
# return g.stored_etud_info[etudid]
# cnx = cnx or ndb.GetDBConnexion()
# etud = etudident_list(cnx, args={"etudid": etudid})
# fill_etuds_info(etud)
# g.stored_etud_info[etudid] = etud[0]
# return etud[0]
def create_etud(cnx, args={}): def create_etud(cnx, args={}):
"""Creation d'un étudiant. génère aussi évenement et "news". """Creation d'un étudiant. génère aussi évenement et "news".

View File

@ -35,7 +35,7 @@ from enum import Enum
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
import openpyxl.utils.datetime import openpyxl.utils.datetime
from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL from openpyxl.styles.numbers import FORMAT_NUMBER_00, FORMAT_GENERAL, FORMAT_DATE_DDMMYY
from openpyxl.comments import Comment from openpyxl.comments import Comment
from openpyxl import Workbook, load_workbook from openpyxl import Workbook, load_workbook
from openpyxl.cell import WriteOnlyCell from openpyxl.cell import WriteOnlyCell
@ -65,10 +65,16 @@ class COLORS(Enum):
def xldate_as_datetime(xldate, datemode=0): def xldate_as_datetime(xldate, datemode=0):
"""Conversion d'une date Excel en date """Conversion d'une date Excel en datetime python
Deux formats de chaîne acceptés:
* JJ/MM/YYYY (chaîne naïve)
* Date ISO (valeur de type date lue dans la feuille)
Peut lever une ValueError Peut lever une ValueError
""" """
return openpyxl.utils.datetime.from_ISO8601(xldate) try:
return datetime.datetime.strptime(xldate, "%d/%m/%Y")
except:
return openpyxl.utils.datetime.from_ISO8601(xldate)
def adjust_sheetname(sheet_name): def adjust_sheetname(sheet_name):
@ -283,10 +289,6 @@ class ScoExcelSheet:
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
""" """
cell = WriteOnlyCell(self.ws, value or "") cell = WriteOnlyCell(self.ws, value or "")
if not (isinstance(value, int) or isinstance(value, float)):
cell.data_type = "s"
# if style is not None and "fill" in style:
# toto()
if style is None: if style is None:
style = self.default_style style = self.default_style
if "font" in style: if "font" in style:
@ -308,6 +310,14 @@ class ScoExcelSheet:
lines = comment.splitlines() lines = comment.splitlines()
cell.comment.width = 7 * max([len(line) for line in lines]) cell.comment.width = 7 * max([len(line) for line in lines])
cell.comment.height = 20 * len(lines) cell.comment.height = 20 * len(lines)
# test datatype at the end so that datetime format may be overwritten
if isinstance(value, datetime.date):
cell.data_type = "d"
cell.number_format = FORMAT_DATE_DDMMYY
elif isinstance(value, int) or isinstance(value, float):
cell.data_type = "n"
else:
cell.data_type = "s"
return cell return cell
def make_row(self, values: list, style=None, comments=None): def make_row(self, values: list, style=None, comments=None):
@ -568,10 +578,9 @@ def excel_bytes_to_list(bytes_content):
return _excel_to_list(filelike) return _excel_to_list(filelike)
except: except:
raise ScoValueError( raise ScoValueError(
"""Le fichier xlsx attendu n'est pas lisible !
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ..)
""" """
scolars_import_excel_file: un contenu xlsx semble corrompu!
peut-être avez vous fourni un fichier au mauvais format (txt, xls, ..)
"""
) )
@ -580,10 +589,9 @@ def excel_file_to_list(filename):
return _excel_to_list(filename) return _excel_to_list(filename)
except: except:
raise ScoValueError( raise ScoValueError(
"""scolars_import_excel_file: un contenu xlsx """Le fichier xlsx attendu n'est pas lisible !
semble corrompu ! Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...) """
"""
) )

View File

@ -45,10 +45,6 @@ class InvalidEtudId(NoteProcessError):
pass pass
class AccessDenied(ScoException):
pass
class InvalidNoteValue(ScoException): class InvalidNoteValue(ScoException):
pass pass
@ -92,6 +88,10 @@ class ScoGenError(ScoException):
ScoException.__init__(self, msg) ScoException.__init__(self, msg)
class AccessDenied(ScoGenError):
pass
class ScoInvalidDateError(ScoValueError): class ScoInvalidDateError(ScoValueError):
pass pass

View File

@ -93,16 +93,21 @@ _formsemestreEditor = ndb.EditableTable(
) )
def get_formsemestre(formsemestre_id): def get_formsemestre(formsemestre_id, raise_soft_exc=False):
"list ONE formsemestre" "list ONE formsemestre"
if formsemestre_id in g.stored_get_formsemestre:
return g.stored_get_formsemestre[formsemestre_id]
if not isinstance(formsemestre_id, int): if not isinstance(formsemestre_id, int):
raise ValueError("formsemestre_id must be an integer !") raise ValueError("formsemestre_id must be an integer !")
try: sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
sem = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})[0] if not sems:
return sem
except:
log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id) log("get_formsemestre: invalid formsemestre_id (%s)" % formsemestre_id)
raise if raise_soft_exc:
raise ScoValueError(f"semestre {formsemestre_id} inconnu !")
else:
raise ValueError(f"semestre {formsemestre_id} inconnu !")
g.stored_get_formsemestre[formsemestre_id] = sems[0]
return sems[0]
def do_formsemestre_list(*a, **kw): def do_formsemestre_list(*a, **kw):

View File

@ -337,7 +337,7 @@ def formsemestre_status_menubar(sem):
submenu.append( submenu.append(
{ {
"title": "%s" % partition["partition_name"], "title": "%s" % partition["partition_name"],
"endpoint": "scolar.affectGroups", "endpoint": "scolar.affect_groups",
"args": {"partition_id": partition["partition_id"]}, "args": {"partition_id": partition["partition_id"]},
"enabled": enabled, "enabled": enabled,
} }
@ -505,15 +505,29 @@ def formsemestre_page_title():
fill_formsemestre(sem) fill_formsemestre(sem)
H = [ h = f"""<div class="formsemestre_page_title">
"""<div class="formsemestre_page_title">""", <div class="infos">
"""<div class="infos"> <span class="semtitle"><a class="stdlink" title="{sem['session_id']}"
<span class="semtitle"><a class="stdlink" title="%(session_id)s" href="%(notes_url)s/formsemestre_status?formsemestre_id=%(formsemestre_id)s">%(titre)s</a><a title="%(etape_apo_str)s">%(num_sem)s</a>%(modalitestr)s</span><span class="dates"><a title="du %(date_debut)s au %(date_fin)s ">%(mois_debut)s - %(mois_fin)s</a></span><span class="resp"><a title="%(nomcomplet)s">%(resp)s</a></span><span class="nbinscrits"><a class="discretelink" href="%(notes_url)s/formsemestre_lists?formsemestre_id=%(formsemestre_id)s">%(nbinscrits)d inscrits</a></span><span class="lock">%(locklink)s</span><span class="eye">%(eyelink)s</span></div>""" href="{url_for('notes.formsemestre_status',
% sem, scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
formsemestre_status_menubar(sem), >{sem['titre']}</a><a
"""</div>""", title="{sem['etape_apo_str']}">{sem['num_sem']}</a>{sem['modalitestr']}</span><span
] class="dates"><a
return "\n".join(H) title="du {sem['date_debut']} au {sem['date_fin']} "
>{sem['mois_debut']} - {sem['mois_fin']}</a></span><span
class="resp"><a title="{sem['nomcomplet']}">{sem['resp']}</a></span><span
class="nbinscrits"><a class="discretelink"
href="{url_for("scolar.groups_view",
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
>{sem['nbinscrits']} inscrits</a></span><span
class="lock">{sem['locklink']}</span><span
class="eye">{sem['eyelink']}</span>
</div>
{formsemestre_status_menubar(sem)}
</div>
"""
return h
def fill_formsemestre(sem): def fill_formsemestre(sem):
@ -843,7 +857,7 @@ def _make_listes_sem(sem, with_absences=True):
H.append('<p class="help indent">Aucun groupe dans cette partition') H.append('<p class="help indent">Aucun groupe dans cette partition')
if sco_groups.sco_permissions_check.can_change_groups(formsemestre_id): if sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
H.append( H.append(
f""" (<a href="{url_for("scolar.affectGroups", f""" (<a href="{url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=partition["partition_id"]) partition_id=partition["partition_id"])
}" class="stdlink">créer</a>)""" }" class="stdlink">créer</a>)"""
@ -967,7 +981,7 @@ def formsemestre_status(formsemestre_id=None):
"""Tableau de bord semestre HTML""" """Tableau de bord semestre HTML"""
# porté du DTML # porté du DTML
cnx = ndb.GetDBConnexion() cnx = ndb.GetDBConnexion()
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list( Mlist = sco_moduleimpl.do_moduleimpl_withmodule_list(
formsemestre_id=formsemestre_id formsemestre_id=formsemestre_id
) )

View File

@ -492,6 +492,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
""" """
from app.scodoc import sco_formsemestre from app.scodoc import sco_formsemestre
cnx = ndb.GetDBConnexion()
t0 = time.time() t0 = time.time()
partition = get_partition(partition_id) partition = get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
@ -500,6 +502,7 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > inscrdict nt = sco_cache.NotesTableCache.get(formsemestre_id) # > inscrdict
etuds_set = set(nt.inscrdict) etuds_set = set(nt.inscrdict)
# Build XML: # Build XML:
t1 = time.time()
doc = Element("ajax-response") doc = Element("ajax-response")
x_response = Element("response", type="object", id="MyUpdater") x_response = Element("response", type="object", id="MyUpdater")
doc.append(x_response) doc.append(x_response)
@ -513,7 +516,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
) )
x_response.append(x_group) x_response.append(x_group)
for e in get_group_members(group["group_id"]): for e in get_group_members(group["group_id"]):
etud = sco_etud.get_etud_info(etudid=e["etudid"], filled=1)[0] etud = sco_etud.get_etud_info(etudid=e["etudid"], filled=True)[0]
# etud = sco_etud.get_etud_info_filled_by_etudid(e["etudid"], cnx)
x_group.append( x_group.append(
Element( Element(
"etud", "etud",
@ -540,6 +544,7 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
doc.append(x_group) doc.append(x_group)
for etudid in etuds_set: for etudid in etuds_set:
etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0] etud = sco_etud.get_etud_info(etudid=etudid, filled=True)[0]
# etud = sco_etud.get_etud_info_filled_by_etudid(etudid, cnx)
x_group.append( x_group.append(
Element( Element(
"etud", "etud",
@ -550,7 +555,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
origin=comp_origin(etud, sem), origin=comp_origin(etud, sem),
) )
) )
log("XMLgetGroupsInPartition: %s seconds" % (time.time() - t0)) t2 = time.time()
log(f"XMLgetGroupsInPartition: {t2-t0} seconds ({t1-t0}+{t2-t1})")
# XML response: # XML response:
data = sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING) data = sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
response = make_response(data) response = make_response(data)
@ -911,7 +917,7 @@ def editPartitionForm(formsemestre_id=None):
H.append(", ".join(lg)) H.append(", ".join(lg))
H.append( H.append(
f"""</td><td><a class="stdlink" href="{ f"""</td><td><a class="stdlink" href="{
url_for("scolar.affectGroups", url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=p["partition_id"]) partition_id=p["partition_id"])
}">répartir</a></td> }">répartir</a></td>
@ -1173,7 +1179,7 @@ def group_set_name(group_id, group_name, redirect=1):
if redirect: if redirect:
return flask.redirect( return flask.redirect(
url_for( url_for(
"scolar.affectGroups", "scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=group["partition_id"], partition_id=group["partition_id"],
) )
@ -1216,7 +1222,7 @@ def group_rename(group_id):
elif tf[0] == -1: elif tf[0] == -1:
return flask.redirect( return flask.redirect(
url_for( url_for(
"scolar.affectGroups", "scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=group["partition_id"], partition_id=group["partition_id"],
) )
@ -1236,7 +1242,7 @@ def groups_auto_repartition(partition_id=None):
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
# renvoie sur page édition groupes # renvoie sur page édition groupes
dest_url = url_for( dest_url = url_for(
"scolar.affectGroups", scodoc_dept=g.scodoc_dept, partition_id=partition_id "scolar.affect_groups", scodoc_dept=g.scodoc_dept, partition_id=partition_id
) )
if not sco_permissions_check.can_change_groups(formsemestre_id): if not sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !") raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")

View File

@ -27,70 +27,33 @@
"""Formulaires gestion des groupes """Formulaires gestion des groupes
""" """
from flask import render_template
from app.scodoc import html_sco_header from app.scodoc import html_sco_header
from app.scodoc import sco_groups from app.scodoc import sco_groups
from app.scodoc.sco_exceptions import AccessDenied from app.scodoc.sco_exceptions import AccessDenied
def affectGroups(partition_id): def affect_groups(partition_id):
"""Formulaire affectation des etudiants aux groupes de la partition. """Formulaire affectation des etudiants aux groupes de la partition.
Permet aussi la creation et la suppression de groupes. Permet aussi la creation et la suppression de groupes.
""" """
# Ported from DTML and adapted to new group management (nov 2009) # réécrit pour 9.0.47 avec un template
partition = sco_groups.get_partition(partition_id) partition = sco_groups.get_partition(partition_id)
formsemestre_id = partition["formsemestre_id"] formsemestre_id = partition["formsemestre_id"]
if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id): if not sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
raise AccessDenied("vous n'avez pas la permission d'effectuer cette opération") raise AccessDenied("vous n'avez pas la permission d'effectuer cette opération")
return render_template(
H = [ "scolar/affect_groups.html",
html_sco_header.sco_header( sco_header=html_sco_header.sco_header(
page_title="Affectation aux groupes", page_title="Affectation aux groupes",
javascripts=["js/groupmgr.js"], javascripts=["js/groupmgr.js"],
cssstyles=["css/groups.css"], cssstyles=["css/groups.css"],
), ),
"""<h2 class="formsemestre">Affectation aux groupes de %s</h2><form id="sp">""" sco_footer=html_sco_header.sco_footer(),
% partition["partition_name"], partition=partition,
] partitions_list=sco_groups.get_partitions_list(
formsemestre_id, with_default=False
H += [ ),
"""</select></form>""", formsemestre_id=formsemestre_id,
"""<p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>". Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien "suppr." en haut à droite de sa boite. Vous pouvez aussi <a class="stdlink" href="groups_auto_repartition?partition_id=%(partition_id)s">répartir automatiquement les groupes</a>. )
</p>"""
% partition,
"""<div id="gmsg" class="head_message"></div>""",
"""<div id="ginfo"></div>""",
"""<div id="savedinfo"></div>""",
"""<form name="formGroup" id="formGroup" onSubmit="return false;">""",
"""<input type="hidden" name="partition_id" value="%s"/>""" % partition_id,
"""<input name="groupName" size="6"/>
<input type="button" onClick="createGroup();" value="Créer groupe"/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" onClick="document.location = 'formsemestre_status?formsemestre_id=%s'" value="Annuler" />&nbsp;&nbsp;&nbsp;&nbsp;
Editer groupes de
<select name="other_partition_id" onchange="GotoAnother();">"""
% formsemestre_id,
]
for p in sco_groups.get_partitions_list(formsemestre_id, with_default=False):
H.append('<option value="%s"' % p["partition_id"])
if p["partition_id"] == partition_id:
H.append(" selected")
H.append(">%s</option>" % p["partition_name"])
H += [
"""</select>
</form>
<div id="groups">
</div>
<div style="clear: left; margin-top: 15px;">
<p class="help"></p>
</div>
</div>
""",
html_sco_header.sco_footer(),
]
return "\n".join(H)

View File

@ -489,7 +489,7 @@ def groups_table(
columns_ids += ["etape", "etudid", "code_nip", "code_ine"] columns_ids += ["etape", "etudid", "code_nip", "code_ine"]
if with_paiement: if with_paiement:
columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"] columns_ids += ["datefinalisationinscription_str", "paiementinscription_str"]
if with_paiement or with_codes: if with_paiement: # or with_codes:
sco_portal_apogee.check_paiement_etuds(groups_infos.members) sco_portal_apogee.check_paiement_etuds(groups_infos.members)
if with_archives: if with_archives:
from app.scodoc import sco_archives_etud from app.scodoc import sco_archives_etud

View File

@ -25,16 +25,16 @@
# #
############################################################################## ##############################################################################
""" Importation des etudiants à partir de fichiers CSV """ Importation des étudiants à partir de fichiers CSV
""" """
import collections import collections
import io
import os import os
import re import re
import time import time
from datetime import date from datetime import date
import flask
from flask import g, url_for from flask import g, url_for
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
@ -252,7 +252,7 @@ def students_import_excel(
def scolars_import_excel_file( def scolars_import_excel_file(
datafile, datafile: io.BytesIO,
formsemestre_id=None, formsemestre_id=None,
check_homonyms=True, check_homonyms=True,
require_ine=False, require_ine=False,
@ -414,16 +414,14 @@ def scolars_import_excel_file(
if NbHomonyms: if NbHomonyms:
NbImportedHomonyms += 1 NbImportedHomonyms += 1
# Insert in DB tables # Insert in DB tables
formsemestre_to_invalidate.add( formsemestre_id_etud = _import_one_student(
_import_one_student( cnx,
cnx, formsemestre_id,
formsemestre_id, values,
values, GroupIdInferers,
GroupIdInferers, annee_courante,
annee_courante, created_etudids,
created_etudids, linenum,
linenum,
)
) )
# Verification proportion d'homonymes: si > 10%, abandonne # Verification proportion d'homonymes: si > 10%, abandonne
@ -522,7 +520,7 @@ def _import_one_student(
annee_courante, annee_courante,
created_etudids, created_etudids,
linenum, linenum,
): ) -> int:
""" """
Import d'un étudiant et inscription dans le semestre. Import d'un étudiant et inscription dans le semestre.
Return: id du semestre dans lequel il a été inscrit. Return: id du semestre dans lequel il a été inscrit.
@ -550,6 +548,12 @@ def _import_one_student(
else: else:
args["formsemestre_id"] = values["codesemestre"] args["formsemestre_id"] = values["codesemestre"]
formsemestre_id = values["codesemestre"] formsemestre_id = values["codesemestre"]
try:
formsemestre_id = int(formsemestre_id)
except ValueError as exc:
raise ScoValueError(
f"valeur invalide dans la colonne codesemestre, ligne {linenum+1}"
) from exc
# recupere liste des groupes: # recupere liste des groupes:
if formsemestre_id not in GroupIdInferers: if formsemestre_id not in GroupIdInferers:
GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(formsemestre_id) GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(formsemestre_id)
@ -566,7 +570,7 @@ def _import_one_student(
) )
do_formsemestre_inscription_with_modules( do_formsemestre_inscription_with_modules(
args["formsemestre_id"], int(args["formsemestre_id"]),
etudid, etudid,
group_ids, group_ids,
etat="I", etat="I",

View File

@ -109,8 +109,11 @@ def import_excel_file(datafile):
if not exceldata: if not exceldata:
raise ScoValueError("Ficher excel vide ou invalide") raise ScoValueError("Ficher excel vide ou invalide")
_, data = sco_excel.excel_bytes_to_list(exceldata) _, data = sco_excel.excel_bytes_to_list(exceldata)
if not data: # probably a bug if not data:
raise ScoException("import_excel_file: empty file !") raise ScoValueError(
"""Le fichier xlsx attendu semble vide !
"""
)
# 1- --- check title line # 1- --- check title line
fs = [scu.stripquotes(s).lower() for s in data[0]] fs = [scu.stripquotes(s).lower() for s in data[0]]
log("excel: fs='%s'\ndata=%s" % (str(fs), str(data))) log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
@ -179,11 +182,13 @@ def import_users(users):
line = line + 1 line = line + 1
user_ok, msg = sco_users.check_modif_user( user_ok, msg = sco_users.check_modif_user(
0, 0,
ignore_optionals=False,
user_name=u["user_name"], user_name=u["user_name"],
nom=u["nom"], nom=u["nom"],
prenom=u["prenom"], prenom=u["prenom"],
email=u["email"], email=u["email"],
roles=u["roles"].split(","), roles=u["roles"].split(","),
dept=u["dept"],
) )
if not user_ok: if not user_ok:
append_msg("identifiant '%s' %s" % (u["user_name"], msg)) append_msg("identifiant '%s' %s" % (u["user_name"], msg))
@ -193,39 +198,12 @@ def import_users(users):
u["passwd"] = generate_password() u["passwd"] = generate_password()
# #
# check identifiant # check identifiant
if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]*$", u["user_name"]):
user_ok = False
append_msg(
"identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)"
% u["user_name"]
)
if len(u["user_name"]) > 64:
user_ok = False
append_msg(
"identifiant '%s' trop long (64 caractères)" % u["user_name"]
)
if len(u["nom"]) > 64:
user_ok = False
append_msg("nom '%s' trop long (64 caractères)" % u["nom"])
if len(u["prenom"]) > 64:
user_ok = False
append_msg("prenom '%s' trop long (64 caractères)" % u["prenom"])
if len(u["email"]) > 120:
user_ok = False
append_msg("email '%s' trop long (120 caractères)" % u["email"])
# check that tha same user_name has not already been described in this import
if u["user_name"] in created.keys(): if u["user_name"] in created.keys():
user_ok = False user_ok = False
append_msg( append_msg(
"l'utilisateur '%s' a déjà été décrit ligne %s" "l'utilisateur '%s' a déjà été décrit ligne %s"
% (u["user_name"], created[u["user_name"]]["line"]) % (u["user_name"], created[u["user_name"]]["line"])
) )
# check département
if u["dept"] != "":
dept = Departement.query.filter_by(acronym=u["dept"]).first()
if dept is None:
user_ok = False
append_msg("département '%s' inexistant" % u["dept"])
# check roles / ignore whitespaces around roles / build roles_string # check roles / ignore whitespaces around roles / build roles_string
# roles_string (expected by User) appears as column 'roles' in excel file # roles_string (expected by User) appears as column 'roles' in excel file
roles_list = [] roles_list = []

View File

@ -390,7 +390,7 @@ def formsemestre_inscr_passage(
): # il y a au moins une vraie partition ): # il y a au moins une vraie partition
H.append( H.append(
f"""<li><a class="stdlink" href="{ f"""<li><a class="stdlink" href="{
url_for("scolar.affectGroups", url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept, partition_id=partition["partition_id"]) scodoc_dept=g.scodoc_dept, partition_id=partition["partition_id"])
}">Répartir les groupes de {partition["partition_name"]}</a></li> }">Répartir les groupes de {partition["partition_name"]}</a></li>
""" """

View File

@ -315,7 +315,7 @@ def _make_table_notes(
rows.append( rows.append(
{ {
"code": code, "code": str(code), # INE, NIP ou etudid
"_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"', "_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
"etudid": etudid, "etudid": etudid,
"nom": etud["nom"].upper(), "nom": etud["nom"].upper(),
@ -374,9 +374,11 @@ def _make_table_notes(
columns_ids.append(e["evaluation_id"]) columns_ids.append(e["evaluation_id"])
# #
if anonymous_listing: if anonymous_listing:
rows.sort(key=lambda x: x["code"]) rows.sort(key=lambda x: x["code"] or "")
else: else:
rows.sort(key=lambda x: (x["nom"], x["prenom"])) # sort by nom, prenom rows.sort(
key=lambda x: (x["nom"] or "", x["prenom"] or "")
) # sort by nom, prenom
# Si module, ajoute moyenne du module: # Si module, ajoute moyenne du module:
if len(evals) > 1: if len(evals) > 1:

View File

@ -527,15 +527,15 @@ def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
cursor.execute( cursor.execute(
"""DELETE FROM notes_moduleimpl_inscription """DELETE FROM notes_moduleimpl_inscription
WHERE moduleimpl_inscription_id IN ( WHERE id IN (
SELECT i.moduleimpl_inscription_id FROM SELECT i.id FROM
notes_moduleimpl mi, notes_modules mod, notes_moduleimpl mi, notes_modules mod,
notes_formsemestre sem, notes_moduleimpl_inscription i notes_formsemestre sem, notes_moduleimpl_inscription i
WHERE sem.formsemestre_id = %(formsemestre_id)s WHERE sem.id = %(formsemestre_id)s
AND mi.formsemestre_id = sem.formsemestre_id AND mi.formsemestre_id = sem.id
AND mod.module_id = mi.module_id AND mod.id = mi.module_id
AND mod.ue_id = %(ue_id)s AND mod.ue_id = %(ue_id)s
AND i.moduleimpl_id = mi.moduleimpl_id AND i.moduleimpl_id = mi.id
AND i.etudid = %(etudid)s AND i.etudid = %(etudid)s
) )
""", """,

View File

@ -916,7 +916,7 @@ def formsemestre_validate_ues(formsemestre_id, etudid, code_etat_sem, assiduite)
and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE and ue_status["moy"] >= nt.parcours.NOTES_BARRE_VALID_UE
): ):
code_ue = ADM code_ue = ADM
elif isinstance(ue_status["moy"], float): elif not isinstance(ue_status["moy"], float):
# aucune note (pas de moyenne) dans l'UE: ne la valide pas # aucune note (pas de moyenne) dans l'UE: ne la valide pas
code_ue = None code_ue = None
elif valid_semestre: elif valid_semestre:

View File

@ -66,7 +66,7 @@ from app.scodoc.sco_utils import (
LOGOS_IMAGES_ALLOWED_TYPES, LOGOS_IMAGES_ALLOWED_TYPES,
) )
from app import log from app import log
from app.scodoc.sco_exceptions import ScoGenError from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
import sco_version import sco_version
PAGE_HEIGHT = defaultPageSize[1] PAGE_HEIGHT = defaultPageSize[1]
@ -121,6 +121,7 @@ def makeParas(txt, style, suppress_empty=False):
"""Returns a list of Paragraph instances from a text """Returns a list of Paragraph instances from a text
with one or more <para> ... </para> with one or more <para> ... </para>
""" """
result = []
try: try:
paras = _splitPara(txt) paras = _splitPara(txt)
if suppress_empty: if suppress_empty:
@ -133,21 +134,30 @@ def makeParas(txt, style, suppress_empty=False):
if m.group(1): # non empty paragraph if m.group(1): # non empty paragraph
r.append(para) r.append(para)
paras = r paras = r
return [Paragraph(SU(s), style) for s in paras] result = [Paragraph(SU(s), style) for s in paras]
except OSError as e:
msg = str(e)
# If a file is missing, try to display the invalid name
m = re.match(r".*\sfilename=\'(.*?)\'.*", msg, re.DOTALL)
if m:
filename = os.path.split(m.group(1))[1]
if filename.startswith("logo_"):
filename = filename[len("logo_") :]
raise ScoValueError(
f"Erreur dans le format PDF paramétré: fichier logo <b>{filename}</b> non trouvé"
) from e
else:
raise e
except Exception as e: except Exception as e:
detail = " " + str(e)
log(traceback.format_exc()) log(traceback.format_exc())
log("Invalid pdf para format: %s" % txt) log("Invalid pdf para format: %s" % txt)
return [ result = [
Paragraph( Paragraph(
SU( SU('<font color="red"><i>Erreur: format invalide</i></font>'),
'<font color="red"><i>Erreur: format invalide{}</i></font>'.format(
detail
)
),
style, style,
) )
] ]
return result
def bold_paras(L, tag="b", close=None): def bold_paras(L, tag="b", close=None):

View File

@ -88,10 +88,9 @@ def _get_group_info(evaluation_id):
groups_tree[partition][group_name] = group_id groups_tree[partition][group_name] = group_id
if partition != TOUS: if partition != TOUS:
has_groups = True has_groups = True
nb_groups = len(groups_tree)
else: else:
has_groups = False has_groups = False
nb_groups = 1 nb_groups = sum([len(groups_tree[p]) for p in groups_tree])
return groups_tree, has_groups, nb_groups return groups_tree, has_groups, nb_groups

View File

@ -827,7 +827,7 @@ def feuille_saisie_notes(evaluation_id, group_ids=[]):
] ]
) )
filename = "notes_%s_%s.xlsx" % (evalname, gr_title_filename) filename = "notes_%s_%s" % (evalname, gr_title_filename)
xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L) xls = sco_excel.excel_feuille_saisie(E, sem["titreannee"], description, lines=L)
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE) return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
# return sco_excel.send_excel_file(xls, filename) # return sco_excel.send_excel_file(xls, filename)

View File

@ -271,7 +271,7 @@ def formsemestre_synchro_etuds(
if partitions: # il y a au moins une vraie partition if partitions: # il y a au moins une vraie partition
H.append( H.append(
f"""<li><a class="stdlink" href="{ f"""<li><a class="stdlink" href="{
url_for("scolar.affectGroups", url_for("scolar.affect_groups",
scodoc_dept=g.scodoc_dept, scodoc_dept=g.scodoc_dept,
partition_id=partitions[0]["partition_id"] partition_id=partitions[0]["partition_id"]
)}">Répartir les groupes de {partitions[0]["partition_name"]}</a></li> )}">Répartir les groupes de {partitions[0]["partition_name"]}</a></li>

View File

@ -29,13 +29,14 @@
""" """
# Anciennement ZScoUsers.py, fonctions de gestion des données réécrite avec flask/SQLAlchemy # Anciennement ZScoUsers.py, fonctions de gestion des données réécrite avec flask/SQLAlchemy
import re
from flask import url_for, g, request from flask import url_for, g, request
from flask_login import current_user from flask_login import current_user
import cracklib # pylint: disable=import-error import cracklib # pylint: disable=import-error
from app import db from app import db, Departement
from app.auth.models import Permission from app.auth.models import Permission
from app.auth.models import User from app.auth.models import User
@ -171,21 +172,25 @@ def list_users(
if not can_modify: if not can_modify:
d["date_modif_passwd"] = "(non visible)" d["date_modif_passwd"] = "(non visible)"
columns_ids = [
"user_name",
"nom_fmt",
"prenom_fmt",
"email",
"dept",
"roles_string",
"date_expiration",
"date_modif_passwd",
"passwd_temp",
"status_txt",
]
# Seul l'admin peut voir les dates de dernière connexion
if current_user.is_administrator():
columns_ids.append("last_seen")
title = "Utilisateurs définis dans ScoDoc" title = "Utilisateurs définis dans ScoDoc"
tab = GenTable( tab = GenTable(
rows=r, rows=r,
columns_ids=( columns_ids=columns_ids,
"user_name",
"nom_fmt",
"prenom_fmt",
"email",
"dept",
"roles_string",
"date_expiration",
"date_modif_passwd",
"passwd_temp",
"status_txt",
),
titles={ titles={
"user_name": "Login", "user_name": "Login",
"nom_fmt": "Nom", "nom_fmt": "Nom",
@ -195,6 +200,7 @@ def list_users(
"roles_string": "Rôles", "roles_string": "Rôles",
"date_expiration": "Expiration", "date_expiration": "Expiration",
"date_modif_passwd": "Modif. mot de passe", "date_modif_passwd": "Modif. mot de passe",
"last_seen": "Dernière cnx.",
"passwd_temp": "Temp.", "passwd_temp": "Temp.",
"status_txt": "Etat", "status_txt": "Etat",
}, },
@ -206,7 +212,7 @@ def list_users(
html_class="table_leftalign list_users", html_class="table_leftalign list_users",
html_with_td_classes=True, html_with_td_classes=True,
html_sortable=True, html_sortable=True,
base_url="%s?all=%s" % (request.base_url, all), base_url="%s?all_depts=%s" % (request.base_url, 1 if all_depts else 0),
pdf_link=False, # table is too wide to fit in a paper page => disable pdf pdf_link=False, # table is too wide to fit in a paper page => disable pdf
preferences=sco_preferences.SemPreferences(), preferences=sco_preferences.SemPreferences(),
) )
@ -379,7 +385,16 @@ def user_info_page(user_name=None):
return "\n".join(H) + F return "\n".join(H) + F
def check_modif_user(edit, user_name="", nom="", prenom="", email="", roles=[]): def check_modif_user(
edit,
ignore_optionals=False,
user_name="",
nom="",
prenom="",
email="",
dept="",
roles=[],
):
"""Vérifie que cet utilisateur peut être créé (edit=0) ou modifié (edit=1) """Vérifie que cet utilisateur peut être créé (edit=0) ou modifié (edit=1)
Cherche homonymes. Cherche homonymes.
returns (ok, msg) returns (ok, msg)
@ -387,17 +402,44 @@ def check_modif_user(edit, user_name="", nom="", prenom="", email="", roles=[]):
(si ok est faux, l'utilisateur peut quand même forcer la creation) (si ok est faux, l'utilisateur peut quand même forcer la creation)
- msg: message warning a presenter l'utilisateur - msg: message warning a presenter l'utilisateur
""" """
if not user_name or not nom or not prenom: MSG_OPT = """Attention: %s (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
return False, "champ requis vide"
if not email:
return False, "vous devriez indiquer le mail de l'utilisateur créé !"
# ce login existe ? # ce login existe ?
user = _user_list(user_name) user = _user_list(user_name)
if edit and not user: # safety net, le user_name ne devrait pas changer if edit and not user: # safety net, le user_name ne devrait pas changer
return False, "identifiant %s inexistant" % user_name return False, "identifiant %s inexistant" % user_name
if not edit and user: if not edit and user:
return False, "identifiant %s déjà utilisé" % user_name return False, "identifiant %s déjà utilisé" % user_name
if not user_name or not nom or not prenom:
return False, "champ requis vide"
if not re.match(r"^[a-zA-Z0-9@\\\-_\\\.]*$", user_name):
return (
False,
"identifiant '%s' invalide (pas d'accents ni de caractères spéciaux)"
% user_name,
)
if ignore_optionals and len(user_name) > 64:
return False, "identifiant '%s' trop long (64 caractères)" % user_name
if ignore_optionals and len(nom) > 64:
return False, "nom '%s' trop long (64 caractères)" % nom + MSG_OPT
if ignore_optionals and len(prenom) > 64:
return False, "prenom '%s' trop long (64 caractères)" % prenom + MSG_OPT
# check that tha same user_name has not already been described in this import
if not email:
return False, "vous devriez indiquer le mail de l'utilisateur créé !"
if len(email) > 120:
return False, "email '%s' trop long (120 caractères)" % email
if not re.fullmatch(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", email):
return False, "l'adresse mail semble incorrecte"
# check département
if (
ignore_optionals
and dept != ""
and Departement.query.filter_by(acronym=dept).first() is None
):
return False, "département '%s' inexistant" % u["dept"] + MSG_OPT
if ignore_optionals and not roles:
return False, "aucun rôle sélectionné, êtes vous sûr ?" + MSG_OPT
# ok
# Des noms/prénoms semblables existent ? # Des noms/prénoms semblables existent ?
nom = nom.lower().strip() nom = nom.lower().strip()
prenom = prenom.lower().strip() prenom = prenom.lower().strip()
@ -417,12 +459,10 @@ def check_modif_user(edit, user_name="", nom="", prenom="", email="", roles=[]):
"%s %s (pseudo=%s)" % (x.prenom, x.nom, x.user_name) "%s %s (pseudo=%s)" % (x.prenom, x.nom, x.user_name)
for x in similar_users for x in similar_users
] ]
), )
+ MSG_OPT,
) )
# Roles ? # Roles ?
if not roles:
return False, "aucun rôle sélectionné, êtes vous sûr ?"
# ok
return True, "" return True, ""

View File

@ -402,7 +402,7 @@ function GotoAnother() {
if (groups_unsaved) { if (groups_unsaved) {
alert("Enregistrez ou annulez vos changement avant !"); alert("Enregistrez ou annulez vos changement avant !");
} else } else
document.location = SCO_URL + '/affectGroups?partition_id=' + document.formGroup.other_partition_id.value; document.location = SCO_URL + '/affect_groups?partition_id=' + document.formGroup.other_partition_id.value;
} }

View File

@ -4,8 +4,9 @@
{% block title %}Une erreur est survenue !{% endblock %} {% block title %}Une erreur est survenue !{% endblock %}
{% block body %} {% block body %}
<h1>Une erreur est survenue !</h1> <h1>Une erreur est survenue !</h1>
<p>Oops... <span style="color:red;"><b>ScoDoc version <span style="font-size: 120%;">{{SCOVERSION}}</span></b></span> a <p>Oups...</tt> <span style="color:red;"><b>ScoDoc version <span style="font-size: 120%;">{{SCOVERSION}}</span></b></span> a
un problème, désolé.</p> un problème, désolé.</p>
<p><tt style="font-size:60%">{{date}}</tt></p>
<p> Si le problème persiste, contacter l'administrateur de votre site, <p> Si le problème persiste, contacter l'administrateur de votre site,
ou écrire la liste "notes" <a href="mailto:notes@listes.univ-paris13.fr">notes@listes.univ-paris13.fr</a> en ou écrire la liste "notes" <a href="mailto:notes@listes.univ-paris13.fr">notes@listes.univ-paris13.fr</a> en

View File

@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h2>Accès non autorisé</h2>
{{ exc | safe }}
<p class="footer">
{% 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>
{% else %}
<a href="{{ exc.dest_url or url_for('scodoc.index') }}">retour page d'accueil</a>
{% endif %}
</p>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% macro render_field(field) %} {% macro render_field(field) %}
<tr> <tr>
<td class="wtf-field">{{ field.label }}</td> <td class="wtf-field">{{ field.label }}</td>
<td class="wtf-field">{{ field()|safe }} <td class="wtf-field">{{ field(**kwargs)|safe }}
{% if field.errors %} {% if field.errors %}
<ul class=errors> <ul class=errors>
{% for error in field.errors %} {% for error in field.errors %}
@ -27,7 +27,7 @@
{{ render_field(form.nb_rangs) }} {{ render_field(form.nb_rangs) }}
{{ render_field(form.etiquetage) }} {{ render_field(form.etiquetage) }}
{% if form.has_groups %} {% if form.has_groups %}
{{ render_field(form.groups) }} {{ render_field(form.groups, size=form.nb_groups) }}
<!-- Tentative de recréer le choix des groupes sous forme de cases à cocher // demande à créer des champs wtf dynamiquement <!-- Tentative de recréer le choix des groupes sous forme de cases à cocher // demande à créer des champs wtf dynamiquement
{% for partition in form.groups_tree %} {% for partition in form.groups_tree %}
<tr> <tr>

View File

@ -0,0 +1,43 @@
{{ sco_header|safe }}
<h2 class="formsemestre">Affectation aux groupes de {{ partition["partition_name"] }}</h2>
<p>Faites glisser les étudiants d'un groupe à l'autre. Les modifications ne
sont enregistrées que lorsque vous cliquez sur le bouton "<em>Enregistrer ces groupes</em>".
Vous pouvez créer de nouveaux groupes. Pour <em>supprimer</em> un groupe, utiliser le lien
"suppr." en haut à droite de sa boite.
Vous pouvez aussi <a class="stdlink"
href="{{ url_for('scolar.groups_auto_repartition', scodoc_dept=g.scodoc_dept, partition_id=partition['partition_id']) }}"
>répartir automatiquement les groupes</a>.
</p>
<div id="gmsg" class="head_message"></div>
<div id="ginfo"></div>
<div id="savedinfo"></div>
<form name="formGroup" id="formGroup" onSubmit="return false;">
<input type="hidden" name="partition_id" value="{{ partition['partition_id'] }}"/>
<input name="groupName" size="6"/>
<input type="button" onClick="createGroup();" value="Créer groupe"/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" />
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<input type="button"
onClick="document.location = '{{ url_for( 'notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }}'"
value="Annuler" />&nbsp;&nbsp;&nbsp;&nbsp;Éditer groupes de
<select name="other_partition_id" onchange="GotoAnother();">
{% for p in partitions_list %}
<option value="{{ p['id'] }}" {{"selected" if p["partition_id"] == partition_id }}>{{p["partition_name"]}}</option>
{% endfor %}
</select>
</form>
<div id="groups">
</div>
<div style="clear: left; margin-top: 15px;">
<p class="help"></p>
</div>
</div>
{{ sco_footer|safe }}

View File

@ -1,8 +1,13 @@
# -*- coding: UTF-8 -* # -*- coding: UTF-8 -*
"""ScoDoc Flask views """ScoDoc Flask views
""" """
import datetime
from flask import Blueprint from flask import Blueprint
from flask import g, current_app from flask import g, current_app
from flask_login import current_user
from app import db
from app.scodoc import notesdb as ndb from app.scodoc import notesdb as ndb
scodoc_bp = Blueprint("scodoc", __name__) scodoc_bp = Blueprint("scodoc", __name__)
@ -20,7 +25,14 @@ from app.views import scodoc, notes, scolar, absences, users
@scodoc_bp.before_app_request @scodoc_bp.before_app_request
def start_scodoc_request(): def start_scodoc_request():
"""Affecte toutes les requêtes, de tous les blueprints""" """Affecte toutes les requêtes, de tous les blueprints"""
# current_app.logger.info(f"start_scodoc_request")
ndb.open_db_connection() ndb.open_db_connection()
if current_user.is_authenticated:
current_user.last_seen = datetime.datetime.utcnow()
db.session.commit()
# caches locaux (durée de vie=la requête en cours)
g.stored_get_formsemestre = {}
# g.stored_etud_info = {} optim en cours, voir si utile
@scodoc_bp.teardown_app_request @scodoc_bp.teardown_app_request

View File

@ -1064,6 +1064,10 @@ def AddBilletAbsence(
begin et end sont au format ISO (eg "1999-01-08 04:05:06") begin et end sont au format ISO (eg "1999-01-08 04:05:06")
""" """
t0 = time.time() t0 = time.time()
begin = str(begin)
end = str(end)
code_nip = str(code_nip) if code_nip else None
# check etudid # check etudid
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True) etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
if not etuds: if not etuds:

View File

@ -274,7 +274,12 @@ def formsemestre_bulletinetud(
xml_with_decisions=False, xml_with_decisions=False,
force_publishing=False, force_publishing=False,
prefer_mail_perso=False, prefer_mail_perso=False,
code_nip=None,
): ):
if not (etudid or code_nip):
raise ScoValueError("Paramètre manquant: spécifier code_nip ou etudid")
if not formsemestre_id:
raise ScoValueError("Paramètre manquant: formsemestre_id est requis")
return sco_bulletins.formsemestre_bulletinetud( return sco_bulletins.formsemestre_bulletinetud(
etudid=etudid, etudid=etudid,
formsemestre_id=formsemestre_id, formsemestre_id=formsemestre_id,
@ -2268,6 +2273,7 @@ sco_publish(
"/view_apo_csv_delete", "/view_apo_csv_delete",
sco_etape_apogee_view.view_apo_csv_delete, sco_etape_apogee_view.view_apo_csv_delete,
Permission.ScoEditApo, Permission.ScoEditApo,
methods=["GET", "POST"],
) )
sco_publish( sco_publish(
"/view_scodoc_etuds", sco_etape_apogee_view.view_scodoc_etuds, Permission.ScoEditApo "/view_scodoc_etuds", sco_etape_apogee_view.view_scodoc_etuds, Permission.ScoEditApo

View File

@ -61,6 +61,7 @@ from app.decorators import (
scodoc, scodoc,
permission_required_compat_scodoc7, permission_required_compat_scodoc7,
) )
from app.scodoc.sco_exceptions import AccessDenied
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.views import scodoc_bp as bp from app.views import scodoc_bp as bp
@ -82,6 +83,12 @@ def index():
) )
# Renvoie les url /ScoDoc/RT/ vers /ScoDoc/RT/Scolarite
@bp.route("/ScoDoc/<scodoc_dept>/")
def index_dept(scodoc_dept):
return redirect(url_for("scolar.index_html", scodoc_dept=scodoc_dept))
@bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"]) @bp.route("/ScoDoc/table_etud_in_accessible_depts", methods=["POST"])
@login_required @login_required
def table_etud_in_accessible_depts(): def table_etud_in_accessible_depts():

View File

@ -651,8 +651,8 @@ def formChangeCoordonnees(etudid):
# --- Gestion des groupes: # --- Gestion des groupes:
sco_publish( sco_publish(
"/affectGroups", "/affect_groups",
sco_groups_edit.affectGroups, sco_groups_edit.affect_groups,
Permission.ScoView, Permission.ScoView,
methods=["GET", "POST"], methods=["GET", "POST"],
) )
@ -1589,6 +1589,7 @@ def etudident_delete(etudid, dialog_confirmed=False):
"admissions", "admissions",
"adresse", "adresse",
"absences", "absences",
"absences_notifications",
"billet_absence", "billet_absence",
] ]
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor) cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
@ -1756,6 +1757,7 @@ def check_group_apogee(group_id, etat=None, fix=False, fixmail=False):
@scodoc7func @scodoc7func
def form_students_import_excel(formsemestre_id=None): def form_students_import_excel(formsemestre_id=None):
"formulaire import xls" "formulaire import xls"
formsemestre_id = int(formsemestre_id) if formsemestre_id else None
if formsemestre_id: if formsemestre_id:
sem = sco_formsemestre.get_formsemestre(formsemestre_id) sem = sco_formsemestre.get_formsemestre(formsemestre_id)
dest_url = ( dest_url = (
@ -1888,7 +1890,7 @@ Les champs avec un astérisque (*) doivent être présents (nulls non autorisés
else: else:
return sco_import_etuds.students_import_excel( return sco_import_etuds.students_import_excel(
tf[2]["csvfile"], tf[2]["csvfile"],
formsemestre_id=formsemestre_id, formsemestre_id=int(formsemestre_id) if formsemestre_id else None,
check_homonyms=tf[2]["check_homonyms"], check_homonyms=tf[2]["check_homonyms"],
require_ine=tf[2]["require_ine"], require_ine=tf[2]["require_ine"],
) )

View File

@ -178,7 +178,7 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
orig_roles_strings = {r.name + "_" + (dept or "") for (r, dept) in orig_roles} orig_roles_strings = {r.name + "_" + (dept or "") for (r, dept) in orig_roles}
# add existing user roles # add existing user roles
displayed_roles = list(editable_roles_set.union(orig_roles)) displayed_roles = list(editable_roles_set.union(orig_roles))
displayed_roles.sort(key=lambda x: (x[1], x[0].name)) displayed_roles.sort(key=lambda x: (x[1] or "", x[0].name or ""))
displayed_roles_strings = [ displayed_roles_strings = [
r.name + "_" + (dept or "") for (r, dept) in displayed_roles r.name + "_" + (dept or "") for (r, dept) in displayed_roles
] ]
@ -381,23 +381,29 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
H.append(tf_error_message("""Erreur: %s""" % err)) H.append(tf_error_message("""Erreur: %s""" % err))
return "\n".join(H) + "\n" + tf[1] + F return "\n".join(H) + "\n" + tf[1] + F
if not force: ok, msg = sco_users.check_modif_user(
ok, msg = sco_users.check_modif_user( edit,
edit, ignore_optionals=force,
user_name=user_name, user_name=user_name,
nom=vals["nom"], nom=vals["nom"],
prenom=vals["prenom"], prenom=vals["prenom"],
email=vals["email"], email=vals["email"],
roles=vals["roles"], roles=vals["roles"],
) )
if not ok: if not ok:
H.append( H.append(tf_error_message(msg))
tf_error_message( return "\n".join(H) + "\n" + tf[1] + F
"""Attention: %s (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
% msg
)
)
if "date_expiration" in vals:
try:
if vals["date_expiration"]:
vals["date_expiration"] = datetime.datetime.strptime(
vals["date_expiration"], "%d/%m/%Y"
)
else:
vals["date_expiration"] = None
except ValueError:
H.append(tf_error_message("date expiration invalide"))
return "\n".join(H) + "\n" + tf[1] + F return "\n".join(H) + "\n" + tf[1] + F
if edit: # modif utilisateur (mais pas password ni user_name !) if edit: # modif utilisateur (mais pas password ni user_name !)
@ -411,17 +417,6 @@ def create_user_form(user_name=None, edit=0, all_roles=1):
del vals["user_name"] del vals["user_name"]
if (current_user.user_name == user_name) and "status" in vals: if (current_user.user_name == user_name) and "status" in vals:
del vals["status"] # no one can't change its own status del vals["status"] # no one can't change its own status
if "date_expiration" in vals:
try:
if vals["date_expiration"]:
vals["date_expiration"] = datetime.datetime.strptime(
vals["date_expiration"], "%d/%m/%Y"
)
else:
vals["date_expiration"] = None
except ValueError:
H.append(tf_error_message("date expiration invalide"))
return "\n".join(H) + "\n" + tf[1] + F
if "status" in vals: if "status" in vals:
vals["active"] = vals["status"] == "" vals["active"] = vals["status"] == ""
# traitement des roles: ne doit pas affecter les roles # traitement des roles: ne doit pas affecter les roles

View File

@ -0,0 +1,46 @@
"""ScoDoc 9.0.51: add unicity constraint on notes_moduleimpl_inscription
Revision ID: d74b4e16fb3c
Revises: f86c013c9fbd
Create Date: 2021-10-09 20:08:50.927330
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.orm import sessionmaker # added by ev
# revision identifiers, used by Alembic.
revision = "d74b4e16fb3c"
down_revision = "f86c013c9fbd"
branch_labels = None
depends_on = None
Session = sessionmaker()
def upgrade():
# Added by ev: remove duplicates
bind = op.get_bind()
session = Session(bind=bind)
session.execute(
"""
DELETE FROM notes_moduleimpl_inscription i1
USING notes_moduleimpl_inscription i2
WHERE i1.id < i2.id
AND i1.moduleimpl_id = i2.moduleimpl_id
AND i1.etudid = i2.etudid;
"""
)
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint(
None, "notes_moduleimpl_inscription", ["moduleimpl_id", "etudid"]
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "notes_moduleimpl_inscription", type_="unique")
# ### end Alembic commands ###

View File

@ -274,6 +274,23 @@ def list_depts(depts=""): # list-dept
print(f"{dept.id}\t{dept.acronym}") print(f"{dept.id}\t{dept.acronym}")
@app.cli.command()
@click.option(
"-n",
"--name",
is_flag=True,
help="show database name instead of connexion string (required for "
"dropdb/createddb commands)",
)
def scodoc_database(name): # list-dept
"""print the database connexion string"""
uri = app.config["SQLALCHEMY_DATABASE_URI"]
if name:
print(uri.split("/")[-1])
else:
print(uri)
@app.cli.command() @app.cli.command()
@with_appcontext @with_appcontext
def import_scodoc7_users(): # import-scodoc7-users def import_scodoc7_users(): # import-scodoc7-users
@ -329,3 +346,27 @@ def recursive_help(cmd, parent=None):
@app.cli.command() @app.cli.command()
def dumphelp(): def dumphelp():
recursive_help(app.cli) recursive_help(app.cli)
@app.cli.command()
@click.option("-h", "--host", default="127.0.0.1", help="The interface to bind to.")
@click.option("-p", "--port", default=5000, help="The port to bind to.")
@click.option(
"--length",
default=25,
help="Number of functions to include in the profiler report.",
)
@click.option(
"--profile-dir", default=None, help="Directory where profiler data files are saved."
)
def profile(host, port, length, profile_dir):
"""Start the application under the code profiler."""
from werkzeug.middleware.profiler import ProfilerMiddleware
from werkzeug.serving import run_simple
app.wsgi_app = ProfilerMiddleware(
app.wsgi_app, restrictions=[length], profile_dir=profile_dir
)
run_simple(
host, port, app, use_debugger=False
) # use run_simple instead of app.run()

View File

@ -22,6 +22,8 @@ def test_client():
with apptest.test_client() as client: with apptest.test_client() as client:
with apptest.app_context(): with apptest.app_context():
with apptest.test_request_context(): with apptest.test_request_context():
# initialize scodoc "g":
g.stored_get_formsemestre = {}
# erase and reset database: # erase and reset database:
initialize_scodoc_database(erase=True, create_all=True) initialize_scodoc_database(erase=True, create_all=True)
# Loge l'utilisateur super-admin # Loge l'utilisateur super-admin

View File

@ -0,0 +1,100 @@
"""Test calculs rattrapages
"""
from config import TestConfig
from tests.unit import sco_fake_gen
from flask import g
import app
from app.scodoc import sco_bulletins
from app.scodoc import sco_utils as scu
DEPT = TestConfig.DEPT_TEST
def test_notes_rattrapage(test_client):
"""Test quelques opérations élémentaires de ScoDoc
Création 10 étudiants, formation, semestre, inscription etudiant,
creation 1 evaluation, saisie 10 notes.
"""
app.set_sco_dept(DEPT)
G = sco_fake_gen.ScoFake(verbose=False)
etuds = [G.create_etud(code_nip=None)] # un seul
f = G.create_formation(acronyme="")
ue = G.create_ue(formation_id=f["formation_id"], acronyme="TST1", titre="ue test")
mat = G.create_matiere(ue_id=ue["ue_id"], titre="matière test")
mod = G.create_module(
matiere_id=mat["matiere_id"],
code="TSM1",
coefficient=1.0,
titre="module test",
ue_id=ue["ue_id"],
formation_id=f["formation_id"],
)
# --- Mise place d'un semestre
sem = G.create_formsemestre(
formation_id=f["formation_id"],
semestre_id=1,
date_debut="01/01/2020",
date_fin="30/06/2020",
)
mi = G.create_moduleimpl(
module_id=mod["module_id"],
formsemestre_id=sem["formsemestre_id"],
)
# --- Inscription des étudiants
for etud in etuds:
G.inscrit_etudiant(sem, etud)
# --- Creation évaluation
e = G.create_evaluation(
moduleimpl_id=mi["moduleimpl_id"],
jour="01/01/2020",
description="evaluation test",
coefficient=1.0,
)
# --- Création d'une évaluation "de rattrapage"
e_rat = G.create_evaluation(
moduleimpl_id=mi["moduleimpl_id"],
jour="02/01/2020",
description="evaluation rattrapage",
coefficient=1.0,
evaluation_type=scu.EVALUATION_RATTRAPAGE,
)
etud = etuds[0]
_, _, _ = G.create_note(evaluation=e, etud=etud, note=12.0)
_, _, _ = G.create_note(evaluation=e_rat, etud=etud, note=11.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
# Vérifie structure du bulletin:
assert b["etudid"] == etud["etudid"]
assert len(b["ues"][0]["modules"][0]["evaluations"]) == 2
assert len(b["ues"][0]["modules"]) == 1
# Note moyenne: ici le ratrapage est inférieur à la note:
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(12.0)
# rattrapage > moyenne:
_, _, _ = G.create_note(evaluation=e_rat, etud=etud, note=18.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(18.0)
# rattrapage vs absences
_, _, _ = G.create_note(evaluation=e, etud=etud, note=None) # abs
_, _, _ = G.create_note(evaluation=e_rat, etud=etud, note=17.0)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(17.0)
# et sans note de rattrapage
_, _, _ = G.create_note(evaluation=e, etud=etud, note=10.0) # abs
_, _, _ = G.create_note(evaluation=e_rat, etud=etud, note=None)
b = sco_bulletins.formsemestre_bulletinetud_dict(
sem["formsemestre_id"], etud["etudid"]
)
assert b["ues"][0]["modules"][0]["mod_moy_txt"] == scu.fmt_note(10.0)

View File

@ -27,6 +27,7 @@ from app.scodoc import sco_codes_parcours
from app.scodoc import sco_evaluations from app.scodoc import sco_evaluations
from app.scodoc import sco_formsemestre_validation from app.scodoc import sco_formsemestre_validation
from app.scodoc import sco_parcours_dut from app.scodoc import sco_parcours_dut
from app.scodoc import sco_cache
from app.scodoc import sco_saisie_notes from app.scodoc import sco_saisie_notes
from app.scodoc import sco_utils as scu from app.scodoc import sco_utils as scu
@ -197,3 +198,19 @@ def run_sco_basic(verbose=False):
assert not sco_parcours_dut.formsemestre_has_decisions( assert not sco_parcours_dut.formsemestre_has_decisions(
sem["formsemestre_id"] sem["formsemestre_id"]
), "décisions non effacées" ), "décisions non effacées"
# --- Décision de jury et validations des ECTS d'UE
for etud in etuds[:5]: # les etudiants notés
sco_formsemestre_validation.formsemestre_validation_etud_manu(
sem["formsemestre_id"],
etud["etudid"],
code_etat=sco_codes_parcours.ADJ,
assidu=True,
redirect=False,
)
# Vérifie que toutes les UE des étudiants notés ont été acquises:
nt = sco_cache.NotesTableCache.get(sem["formsemestre_id"])
for etud in etuds[:5]:
dec_ues = nt.get_etud_decision_ues(etud["etudid"])
for ue_id in dec_ues:
assert dec_ues[ue_id]["code"] in {"ADM", "CMP"}

View File

@ -15,29 +15,47 @@ source "$SCRIPT_DIR/utils.sh"
# Ce script doit tourner comme "root" # Ce script doit tourner comme "root"
check_uid_root "$0" check_uid_root "$0"
# Usage # Usage
if [ ! $# -eq 2 ] usage() {
then echo "Usage: $0 [ --keep-env ] archive"
echo "Usage: $0 archive dbname" echo "Exemple: $0 /tmp/mon-scodoc.tgz"
echo "Exemple: $0 /tmp/mon-scodoc.tgz SCODOC" echo "OPTION"
echo "--keep_env garde la configuration courante"
exit 1 exit 1
}
if (($# < 1 || $# > 2))
then
usage
elif [ $# -eq 2 -a $1 != '--keep-env' -a $2 != '--keep-env' ] ; then
usage
elif [ $# -eq 1 ] ; then
echo "restauration des données et de la configuration originale (production)"
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
else
SRC=$1
fi
fi fi
DB_DUMP="${SCODOC_VAR_DIR}"/SCODOC.dump
SRC=$1
DBNAME=$2
# Safety check # Safety check
echo "Ce script va remplacer les donnees de votre installation ScoDoc par celles" echo "Ce script va remplacer les donnees de votre installation ScoDoc par celles"
echo "enregistrees dans le fichier fourni." 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 ete cree par le script save_scodoc9_data.sh."
echo echo
echo "Attention: TOUTES LES DONNEES DE CE SCODOC SERONT REMPLACEES !" 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 departements existants seront effaces !"
echo echo
echo "La base SQL $DBNAME sera effacée et remplacée !!!" echo "La base SQL $DB_CURRENT sera effacée et remplacée !!!"
echo echo
echo -n "Voulez vous poursuivre cette operation ? (y/n) [n]" echo -n "Voulez vous poursuivre cette operation ? (y/n) [n]"
read -r ans read -r ans
if [ ! "$(norm_ans "$ans")" = 'Y' ] if [ ! "$(norm_ans "$ans")" = 'Y' ]
@ -47,8 +65,13 @@ then
fi fi
# -- Stop ScoDoc # -- Stop ScoDoc
echo "Arrêt de scodoc9..." if [ $KEEP -ne 1 ]; then
systemctl stop scodoc9 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 # Clear caches
echo "Purge des caches..." echo "Purge des caches..."
@ -70,22 +93,31 @@ echo "Vérification du propriétaire..."
chown -R "${SCODOC_USER}:${SCODOC_GROUP}" "${SCODOC_VAR_DIR}" || die "Error chowning ${SCODOC_VAR_DIR}" chown -R "${SCODOC_USER}:${SCODOC_GROUP}" "${SCODOC_VAR_DIR}" || die "Error chowning ${SCODOC_VAR_DIR}"
# --- La base SQL: nommée $(db_name).dump # --- La base SQL: nommée $(db_name).dump
nb=$(su -c "psql -l" "$SCODOC_USER" | awk '{print $1}' | grep -c -e '^'"$DBNAME"'$') nb=$(su -c "psql -l" "$SCODOC_USER" | awk '{print $1}' | grep -c -x "$DB_DEST")
if [ "$nb" -gt 0 ] if [ "$nb" -gt 0 ]
then then
echo "Suppression de la base $DBNAME..." echo "Suppression de la base $DB_DEST..."
su -c "dropdb $DBNAME" "$SCODOC_USER" || die "Erreur destruction db" su -c "dropdb $DB_DEST" "$SCODOC_USER" || die "Erreur destruction db"
fi fi
su -c "createdb $DBNAME" "$SCODOC_USER" || die "Erreur création db" su -c "createdb $DB_DEST" "$SCODOC_USER" || die "Erreur création db"
echo "Chargement de la base SQL..." if [ ! -z $KEEP_ENV ] ; then
su -c "pg_restore -d $DBNAME ${SCODOC_VAR_DIR}/SCODOC.dump" "$SCODOC_USER" || die "Erreur chargement de la base SQL" echo "conservation de la configuration actuelle"
cp "$SCODOC_VAR_DIR".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"
echo "redémarrez scodoc selon votre configuration"
else
# -- Apply migrations if needed (only on "production" database, = SCODOC sauf config particulière)
echo "restauration environnement de production"
echo "Chargement de la base SQL..."
su -c "pg_restore -d $DB_DEST $DB_DUMP" "$SCODOC_USER" || die "Erreur chargement de la base SQL"
export FLASK_ENV="production" # peut-être pas utile? : .env a été recopié
su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade)" "$SCODOC_USER"
# -- Start ScoDoc
systemctl start scodoc9
fi
# -- Apply migrations if needed (only on "production" databse, = SCODOC sauf config particulière)
export FLASK_ENV="production"
su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask db upgrade)" "$SCODOC_USER"
# -- Start ScoDoc
systemctl start scodoc9
echo "Terminé." echo "Terminé."

View File

@ -20,6 +20,14 @@ then
exit 1 exit 1
fi fi
echo "vérification de la configuration..."
DB_CURRENT=$(cd $SCODOC_DIR && source venv/bin/activate && flask scodoc-database -n)
if [ $DB_CURRENT != 'SCODOC' ]; then
echo "Ce script ne peut transférer les données que depuis une base nommée SCODOC (c'est normalement le cas pour un serveur en production)"
echo "Annulation"
exit 1
fi
echo "Ce script est utile pour transférer toutes les données d'un serveur ScoDoc 9" echo "Ce script est utile pour transférer toutes les données d'un serveur ScoDoc 9"
echo "à un autre ScoDoc 9." echo "à un autre ScoDoc 9."
echo "Il est vivement recommandé de mettre à jour votre ScoDoc avant." echo "Il est vivement recommandé de mettre à jour votre ScoDoc avant."
@ -44,8 +52,10 @@ DEST=$1
db_name="$SCODOC_DB_PROD" # SCODOC db_name="$SCODOC_DB_PROD" # SCODOC
# dump dans /opt/scodoc-data/SCODOC.dump # dump dans /opt/scodoc-data/SCODOC.dump
echo "sauvegarde de la base de données"
pg_dump --format=custom --file="$SCODOC_VAR_DIR/$db_name.dump" "$db_name" || die "Error dumping database" pg_dump --format=custom --file="$SCODOC_VAR_DIR/$db_name.dump" "$db_name" || die "Error dumping database"
echo "création du fichier d'archivage..."
# tar scodoc-data vers le fichier indiqué ou stdout # tar scodoc-data vers le fichier indiqué ou stdout
(cd $(dirname "$SCODOC_VAR_DIR"); tar cfz "$DEST" $(basename "$SCODOC_VAR_DIR")) || die "Error archiving data" (cd $(dirname "$SCODOC_VAR_DIR"); tar cfz "$DEST" $(basename "$SCODOC_VAR_DIR")) || die "Error archiving data"