forked from ScoDoc/ScoDoc
maj
This commit is contained in:
commit
aed2d6ce10
29
README.md
29
README.md
@ -106,13 +106,15 @@ Ou avec couverture (`pip install pytest-cov`)
|
||||
|
||||
#### Utilisation des tests unitaires pour initialiser la base de dev
|
||||
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:
|
||||
|
||||
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
|
||||
|
||||
@ -133,7 +135,8 @@ On utilise SQLAlchemy avec Alembic et Flask-Migrate.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
important est `postinst`qui se charge de configurer le système (install ou
|
||||
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
# pylint: disable=invalid-name
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
@ -24,7 +25,12 @@ from flask_moment import Moment
|
||||
from flask_caching import Cache
|
||||
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
|
||||
import sco_version
|
||||
|
||||
@ -50,10 +56,21 @@ def handle_sco_value_error(exc):
|
||||
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):
|
||||
"""Bugs scodoc, erreurs 500"""
|
||||
# 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):
|
||||
@ -93,6 +110,10 @@ class LogRequestFormatter(logging.Formatter):
|
||||
record.url = None
|
||||
record.remote_addr = None
|
||||
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)
|
||||
|
||||
@ -121,6 +142,10 @@ class LogExceptionFormatter(logging.Formatter):
|
||||
record.http_params = None
|
||||
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)
|
||||
|
||||
|
||||
@ -165,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)
|
||||
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"
|
||||
"%(levelname)s: %(message)s"
|
||||
)
|
||||
# les champs additionnels sont définis dans LogRequestFormatter
|
||||
scodoc_exc_formatter = LogExceptionFormatter(
|
||||
"[%(asctime)s] %(sco_user)s@%(remote_addr)s requested %(url)s\n"
|
||||
"%(levelname)s: %(message)s\n"
|
||||
"Referrer: %(http_referrer)s\n"
|
||||
"Method: %(http_method)s\n"
|
||||
"Params: %(http_params)s\n"
|
||||
"Admin mail: %(sco_admin_mail)s\n"
|
||||
)
|
||||
if not app.testing:
|
||||
if not app.debug:
|
||||
@ -259,15 +287,19 @@ def create_app(config_class=DevConfig):
|
||||
)
|
||||
# ---- INITIALISATION SPECIFIQUES A SCODOC
|
||||
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_standard import BulletinGeneratorStandard
|
||||
from app.scodoc.sco_bulletins_ucac import BulletinGeneratorUCAC
|
||||
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorExample)
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
|
||||
# l'ordre est important, le premeir sera le "défaut" pour les nouveaux départements.
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorStandard)
|
||||
sco_bulletins_generator.register_bulletin_class(BulletinGeneratorLegacy)
|
||||
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
|
||||
|
||||
|
@ -422,7 +422,7 @@ class UserRole(db.Model):
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
admin_role = Role.query.filter_by(name="SuperAdmin").first()
|
||||
|
@ -273,6 +273,7 @@ class NotesModuleImplInscription(db.Model):
|
||||
"""Inscription à un module (etudiants,moduleimpl)"""
|
||||
|
||||
__tablename__ = "notes_moduleimpl_inscription"
|
||||
__table_args__ = (db.UniqueConstraint("moduleimpl_id", "etudid"),)
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
moduleimpl_inscription_id = db.synonym("id")
|
||||
|
@ -40,7 +40,7 @@ from app.scodoc.sco_permissions import Permission
|
||||
def sidebar_common():
|
||||
"partie commune à toutes les sidebar"
|
||||
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="{
|
||||
url_for("users.user_info_page",
|
||||
scodoc_dept=g.scodoc_dept, user_name=current_user.user_name)
|
||||
|
@ -1265,7 +1265,7 @@ class NotesTable(object):
|
||||
),
|
||||
self.get_nom_long(etudid),
|
||||
url_for(
|
||||
"scolar.formsemestre_edit_uecoefs",
|
||||
"notes.formsemestre_edit_uecoefs",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
formsemestre_id=self.formsemestre_id,
|
||||
err_ue_id=ue["ue_id"],
|
||||
|
@ -136,7 +136,8 @@ def formsemestre_bulletinetud_dict(formsemestre_id, etudid, version="long"):
|
||||
|
||||
prefs = sco_preferences.SemPreferences(formsemestre_id)
|
||||
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["etudid"] = etudid
|
||||
I["formsemestre_id"] = formsemestre_id
|
||||
@ -774,8 +775,8 @@ def formsemestre_bulletinetud(
|
||||
except:
|
||||
sco_etud.log_unknown_etud()
|
||||
raise ScoValueError("étudiant inconnu")
|
||||
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
# API, donc erreurs admises en ScoValueError
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id, raise_soft_exc=True)
|
||||
|
||||
bulletin = do_formsemestre_bulletinetud(
|
||||
formsemestre_id,
|
||||
|
@ -348,7 +348,7 @@ def do_moduleimpl_moyennes(nt, mod):
|
||||
if etudid in eval_rattr["notes"]:
|
||||
note = eval_rattr["notes"][etudid]["value"]
|
||||
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
|
||||
else:
|
||||
note_sur_20 = note * 20.0 / eval_rattr["note_max"]
|
||||
|
@ -48,9 +48,19 @@ import sco_version
|
||||
|
||||
|
||||
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:
|
||||
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":
|
||||
keep_numeric = True # pas de conversion des notes en strings
|
||||
else:
|
||||
@ -96,8 +106,9 @@ def get_etudids_with_debouche(start_year):
|
||||
FROM notes_formsemestre_inscription i, notes_formsemestre s, itemsuivi it
|
||||
WHERE i.etudid = it.etudid
|
||||
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]
|
||||
@ -193,15 +204,16 @@ def table_debouche_etudids(etudids, keep_numeric=True):
|
||||
return tab
|
||||
|
||||
|
||||
def report_debouche_ask_date():
|
||||
def report_debouche_ask_date(msg: str) -> str:
|
||||
"""Formulaire demande date départ"""
|
||||
return (
|
||||
html_sco_header.sco_header()
|
||||
+ """<form method="GET">
|
||||
Date de départ de la recherche: <input type="text" name="start_year" value="" size=10/>
|
||||
</form>"""
|
||||
+ html_sco_header.sco_footer()
|
||||
)
|
||||
return f"""{html_sco_header.sco_header()}
|
||||
<h2>Table des débouchés des étudiants</h2>
|
||||
<form method="GET">
|
||||
{msg}
|
||||
<input type="text" name="start_year" value="" size=10/>
|
||||
</form>
|
||||
{html_sco_header.sco_footer()}
|
||||
"""
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
|
@ -48,6 +48,7 @@ from app.scodoc import sco_users
|
||||
|
||||
def index_html(showcodes=0, showsemtable=0):
|
||||
"Page accueil département (liste des semestres)"
|
||||
showcodes = int(showcodes)
|
||||
showsemtable = int(showsemtable)
|
||||
H = []
|
||||
|
||||
@ -78,7 +79,7 @@ def index_html(showcodes=0, showsemtable=0):
|
||||
# Responsable de formation:
|
||||
sco_formsemestre.sem_set_responsable_name(sem)
|
||||
|
||||
if showcodes == "1":
|
||||
if showcodes:
|
||||
sem["tmpcode"] = "<td><tt>%s</tt></td>" % sem["formsemestre_id"]
|
||||
else:
|
||||
sem["tmpcode"] = ""
|
||||
@ -126,7 +127,7 @@ def index_html(showcodes=0, showsemtable=0):
|
||||
"""
|
||||
% sco_preferences.get_preference("DeptName")
|
||||
)
|
||||
H.append(_sem_table_gt(sems).html())
|
||||
H.append(_sem_table_gt(sems, showcodes=showcodes).html())
|
||||
H.append("</table>")
|
||||
if not showsemtable:
|
||||
H.append(
|
||||
|
@ -845,6 +845,7 @@ def ue_sharing_code(ue_code=None, ue_id=None, hide_ue_id=None):
|
||||
"""
|
||||
from app.scodoc import sco_formations
|
||||
|
||||
ue_code = str(ue_code)
|
||||
if ue_id:
|
||||
ue = do_ue_list(args={"ue_id": ue_id})[0]
|
||||
if not ue_code:
|
||||
|
@ -640,7 +640,7 @@ def view_apo_csv_delete(etape_apo="", semset_id="", dialog_confirmed=False):
|
||||
if not semset_id:
|
||||
raise ValueError("invalid null 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:
|
||||
return scu.confirm_dialog(
|
||||
"""<h2>Confirmer la suppression du fichier étape <tt>%s</tt>?</h2>
|
||||
|
@ -655,7 +655,7 @@ def log_unknown_etud():
|
||||
|
||||
|
||||
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
|
||||
ou bien cherche dans les argumenst de la requête courante:
|
||||
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
|
||||
|
||||
|
||||
# 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={}):
|
||||
"""Creation d'un étudiant. génère aussi évenement et "news".
|
||||
|
||||
|
@ -35,7 +35,7 @@ from enum import Enum
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
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 import Workbook, load_workbook
|
||||
from openpyxl.cell import WriteOnlyCell
|
||||
@ -65,10 +65,16 @@ class COLORS(Enum):
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
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):
|
||||
@ -283,10 +289,6 @@ class ScoExcelSheet:
|
||||
style -- style par défaut (dictionnaire cf. excel_make_style) de la feuille si non spécifié
|
||||
"""
|
||||
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:
|
||||
style = self.default_style
|
||||
if "font" in style:
|
||||
@ -308,6 +310,14 @@ class ScoExcelSheet:
|
||||
lines = comment.splitlines()
|
||||
cell.comment.width = 7 * max([len(line) for line in 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
|
||||
|
||||
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)
|
||||
except:
|
||||
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)
|
||||
except:
|
||||
raise ScoValueError(
|
||||
"""scolars_import_excel_file: un contenu xlsx
|
||||
semble corrompu !
|
||||
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
|
||||
"""
|
||||
"""Le fichier xlsx attendu n'est pas lisible !
|
||||
Peut-être avez-vous fourni un fichier au mauvais format (txt, xls, ...)
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
|
@ -45,10 +45,6 @@ class InvalidEtudId(NoteProcessError):
|
||||
pass
|
||||
|
||||
|
||||
class AccessDenied(ScoException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidNoteValue(ScoException):
|
||||
pass
|
||||
|
||||
@ -92,6 +88,10 @@ class ScoGenError(ScoException):
|
||||
ScoException.__init__(self, msg)
|
||||
|
||||
|
||||
class AccessDenied(ScoGenError):
|
||||
pass
|
||||
|
||||
|
||||
class ScoInvalidDateError(ScoValueError):
|
||||
pass
|
||||
|
||||
|
@ -93,16 +93,21 @@ _formsemestreEditor = ndb.EditableTable(
|
||||
)
|
||||
|
||||
|
||||
def get_formsemestre(formsemestre_id):
|
||||
def get_formsemestre(formsemestre_id, raise_soft_exc=False):
|
||||
"list ONE formsemestre"
|
||||
if formsemestre_id in g.stored_get_formsemestre:
|
||||
return g.stored_get_formsemestre[formsemestre_id]
|
||||
if not isinstance(formsemestre_id, int):
|
||||
raise ValueError("formsemestre_id must be an integer !")
|
||||
try:
|
||||
sem = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})[0]
|
||||
return sem
|
||||
except:
|
||||
sems = do_formsemestre_list(args={"formsemestre_id": formsemestre_id})
|
||||
if not sems:
|
||||
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):
|
||||
|
@ -337,7 +337,7 @@ def formsemestre_status_menubar(sem):
|
||||
submenu.append(
|
||||
{
|
||||
"title": "%s" % partition["partition_name"],
|
||||
"endpoint": "scolar.affectGroups",
|
||||
"endpoint": "scolar.affect_groups",
|
||||
"args": {"partition_id": partition["partition_id"]},
|
||||
"enabled": enabled,
|
||||
}
|
||||
@ -505,15 +505,29 @@ def formsemestre_page_title():
|
||||
|
||||
fill_formsemestre(sem)
|
||||
|
||||
H = [
|
||||
"""<div class="formsemestre_page_title">""",
|
||||
"""<div class="infos">
|
||||
<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>"""
|
||||
% sem,
|
||||
formsemestre_status_menubar(sem),
|
||||
"""</div>""",
|
||||
]
|
||||
return "\n".join(H)
|
||||
h = f"""<div class="formsemestre_page_title">
|
||||
<div class="infos">
|
||||
<span class="semtitle"><a class="stdlink" title="{sem['session_id']}"
|
||||
href="{url_for('notes.formsemestre_status',
|
||||
scodoc_dept=g.scodoc_dept, formsemestre_id=sem['formsemestre_id'])}"
|
||||
>{sem['titre']}</a><a
|
||||
title="{sem['etape_apo_str']}">{sem['num_sem']}</a>{sem['modalitestr']}</span><span
|
||||
class="dates"><a
|
||||
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):
|
||||
@ -843,7 +857,7 @@ def _make_listes_sem(sem, with_absences=True):
|
||||
H.append('<p class="help indent">Aucun groupe dans cette partition')
|
||||
if sco_groups.sco_permissions_check.can_change_groups(formsemestre_id):
|
||||
H.append(
|
||||
f""" (<a href="{url_for("scolar.affectGroups",
|
||||
f""" (<a href="{url_for("scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
partition_id=partition["partition_id"])
|
||||
}" class="stdlink">créer</a>)"""
|
||||
@ -967,7 +981,7 @@ def formsemestre_status(formsemestre_id=None):
|
||||
"""Tableau de bord semestre HTML"""
|
||||
# porté du DTML
|
||||
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(
|
||||
formsemestre_id=formsemestre_id
|
||||
)
|
||||
|
@ -492,6 +492,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
"""
|
||||
from app.scodoc import sco_formsemestre
|
||||
|
||||
cnx = ndb.GetDBConnexion()
|
||||
|
||||
t0 = time.time()
|
||||
partition = get_partition(partition_id)
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
@ -500,6 +502,7 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
nt = sco_cache.NotesTableCache.get(formsemestre_id) # > inscrdict
|
||||
etuds_set = set(nt.inscrdict)
|
||||
# Build XML:
|
||||
t1 = time.time()
|
||||
doc = Element("ajax-response")
|
||||
x_response = Element("response", type="object", id="MyUpdater")
|
||||
doc.append(x_response)
|
||||
@ -513,7 +516,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
)
|
||||
x_response.append(x_group)
|
||||
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(
|
||||
Element(
|
||||
"etud",
|
||||
@ -540,6 +544,7 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
doc.append(x_group)
|
||||
for etudid in etuds_set:
|
||||
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(
|
||||
Element(
|
||||
"etud",
|
||||
@ -550,7 +555,8 @@ def XMLgetGroupsInPartition(partition_id): # was XMLgetGroupesTD
|
||||
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:
|
||||
data = sco_xml.XML_HEADER + ElementTree.tostring(doc).decode(scu.SCO_ENCODING)
|
||||
response = make_response(data)
|
||||
@ -911,7 +917,7 @@ def editPartitionForm(formsemestre_id=None):
|
||||
H.append(", ".join(lg))
|
||||
H.append(
|
||||
f"""</td><td><a class="stdlink" href="{
|
||||
url_for("scolar.affectGroups",
|
||||
url_for("scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
partition_id=p["partition_id"])
|
||||
}">répartir</a></td>
|
||||
@ -1173,7 +1179,7 @@ def group_set_name(group_id, group_name, redirect=1):
|
||||
if redirect:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"scolar.affectGroups",
|
||||
"scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
partition_id=group["partition_id"],
|
||||
)
|
||||
@ -1216,7 +1222,7 @@ def group_rename(group_id):
|
||||
elif tf[0] == -1:
|
||||
return flask.redirect(
|
||||
url_for(
|
||||
"scolar.affectGroups",
|
||||
"scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
partition_id=group["partition_id"],
|
||||
)
|
||||
@ -1236,7 +1242,7 @@ def groups_auto_repartition(partition_id=None):
|
||||
formsemestre_id = partition["formsemestre_id"]
|
||||
# renvoie sur page édition groupes
|
||||
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):
|
||||
raise AccessDenied("Vous n'avez pas le droit d'effectuer cette opération !")
|
||||
|
@ -27,70 +27,33 @@
|
||||
|
||||
"""Formulaires gestion des groupes
|
||||
"""
|
||||
from flask import render_template
|
||||
|
||||
from app.scodoc import html_sco_header
|
||||
from app.scodoc import sco_groups
|
||||
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.
|
||||
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)
|
||||
formsemestre_id = partition["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")
|
||||
|
||||
H = [
|
||||
html_sco_header.sco_header(
|
||||
return render_template(
|
||||
"scolar/affect_groups.html",
|
||||
sco_header=html_sco_header.sco_header(
|
||||
page_title="Affectation aux groupes",
|
||||
javascripts=["js/groupmgr.js"],
|
||||
cssstyles=["css/groups.css"],
|
||||
),
|
||||
"""<h2 class="formsemestre">Affectation aux groupes de %s</h2><form id="sp">"""
|
||||
% partition["partition_name"],
|
||||
]
|
||||
|
||||
H += [
|
||||
"""</select></form>""",
|
||||
"""<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"/>
|
||||
|
||||
<input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" />
|
||||
|
||||
<input type="button" onClick="document.location = 'formsemestre_status?formsemestre_id=%s'" value="Annuler" />
|
||||
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)
|
||||
sco_footer=html_sco_header.sco_footer(),
|
||||
partition=partition,
|
||||
partitions_list=sco_groups.get_partitions_list(
|
||||
formsemestre_id, with_default=False
|
||||
),
|
||||
formsemestre_id=formsemestre_id,
|
||||
)
|
||||
|
@ -489,7 +489,7 @@ def groups_table(
|
||||
columns_ids += ["etape", "etudid", "code_nip", "code_ine"]
|
||||
if with_paiement:
|
||||
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)
|
||||
if with_archives:
|
||||
from app.scodoc import sco_archives_etud
|
||||
|
@ -25,16 +25,16 @@
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
""" Importation des etudiants à partir de fichiers CSV
|
||||
""" Importation des étudiants à partir de fichiers CSV
|
||||
"""
|
||||
|
||||
import collections
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from datetime import date
|
||||
|
||||
import flask
|
||||
from flask import g, url_for
|
||||
|
||||
import app.scodoc.sco_utils as scu
|
||||
@ -252,7 +252,7 @@ def students_import_excel(
|
||||
|
||||
|
||||
def scolars_import_excel_file(
|
||||
datafile,
|
||||
datafile: io.BytesIO,
|
||||
formsemestre_id=None,
|
||||
check_homonyms=True,
|
||||
require_ine=False,
|
||||
@ -414,16 +414,14 @@ def scolars_import_excel_file(
|
||||
if NbHomonyms:
|
||||
NbImportedHomonyms += 1
|
||||
# Insert in DB tables
|
||||
formsemestre_to_invalidate.add(
|
||||
_import_one_student(
|
||||
cnx,
|
||||
formsemestre_id,
|
||||
values,
|
||||
GroupIdInferers,
|
||||
annee_courante,
|
||||
created_etudids,
|
||||
linenum,
|
||||
)
|
||||
formsemestre_id_etud = _import_one_student(
|
||||
cnx,
|
||||
formsemestre_id,
|
||||
values,
|
||||
GroupIdInferers,
|
||||
annee_courante,
|
||||
created_etudids,
|
||||
linenum,
|
||||
)
|
||||
|
||||
# Verification proportion d'homonymes: si > 10%, abandonne
|
||||
@ -522,7 +520,7 @@ def _import_one_student(
|
||||
annee_courante,
|
||||
created_etudids,
|
||||
linenum,
|
||||
):
|
||||
) -> int:
|
||||
"""
|
||||
Import d'un étudiant et inscription dans le semestre.
|
||||
Return: id du semestre dans lequel il a été inscrit.
|
||||
@ -550,6 +548,12 @@ def _import_one_student(
|
||||
else:
|
||||
args["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:
|
||||
if formsemestre_id not in GroupIdInferers:
|
||||
GroupIdInferers[formsemestre_id] = sco_groups.GroupIdInferer(formsemestre_id)
|
||||
@ -566,7 +570,7 @@ def _import_one_student(
|
||||
)
|
||||
|
||||
do_formsemestre_inscription_with_modules(
|
||||
args["formsemestre_id"],
|
||||
int(args["formsemestre_id"]),
|
||||
etudid,
|
||||
group_ids,
|
||||
etat="I",
|
||||
|
@ -109,8 +109,11 @@ def import_excel_file(datafile):
|
||||
if not exceldata:
|
||||
raise ScoValueError("Ficher excel vide ou invalide")
|
||||
_, data = sco_excel.excel_bytes_to_list(exceldata)
|
||||
if not data: # probably a bug
|
||||
raise ScoException("import_excel_file: empty file !")
|
||||
if not data:
|
||||
raise ScoValueError(
|
||||
"""Le fichier xlsx attendu semble vide !
|
||||
"""
|
||||
)
|
||||
# 1- --- check title line
|
||||
fs = [scu.stripquotes(s).lower() for s in data[0]]
|
||||
log("excel: fs='%s'\ndata=%s" % (str(fs), str(data)))
|
||||
@ -179,11 +182,13 @@ def import_users(users):
|
||||
line = line + 1
|
||||
user_ok, msg = sco_users.check_modif_user(
|
||||
0,
|
||||
ignore_optionals=False,
|
||||
user_name=u["user_name"],
|
||||
nom=u["nom"],
|
||||
prenom=u["prenom"],
|
||||
email=u["email"],
|
||||
roles=u["roles"].split(","),
|
||||
dept=u["dept"],
|
||||
)
|
||||
if not user_ok:
|
||||
append_msg("identifiant '%s' %s" % (u["user_name"], msg))
|
||||
@ -193,39 +198,12 @@ def import_users(users):
|
||||
u["passwd"] = generate_password()
|
||||
#
|
||||
# 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():
|
||||
user_ok = False
|
||||
append_msg(
|
||||
"l'utilisateur '%s' a déjà été décrit ligne %s"
|
||||
% (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
|
||||
# roles_string (expected by User) appears as column 'roles' in excel file
|
||||
roles_list = []
|
||||
|
@ -390,7 +390,7 @@ def formsemestre_inscr_passage(
|
||||
): # il y a au moins une vraie partition
|
||||
H.append(
|
||||
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"])
|
||||
}">Répartir les groupes de {partition["partition_name"]}</a></li>
|
||||
"""
|
||||
|
@ -315,7 +315,7 @@ def _make_table_notes(
|
||||
|
||||
rows.append(
|
||||
{
|
||||
"code": code,
|
||||
"code": str(code), # INE, NIP ou etudid
|
||||
"_code_td_attrs": 'style="padding-left: 1em; padding-right: 2em;"',
|
||||
"etudid": etudid,
|
||||
"nom": etud["nom"].upper(),
|
||||
@ -374,9 +374,11 @@ def _make_table_notes(
|
||||
columns_ids.append(e["evaluation_id"])
|
||||
#
|
||||
if anonymous_listing:
|
||||
rows.sort(key=lambda x: x["code"])
|
||||
rows.sort(key=lambda x: x["code"] or "")
|
||||
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:
|
||||
if len(evals) > 1:
|
||||
|
@ -527,15 +527,15 @@ def do_etud_desinscrit_ue(etudid, formsemestre_id, ue_id):
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
cursor.execute(
|
||||
"""DELETE FROM notes_moduleimpl_inscription
|
||||
WHERE moduleimpl_inscription_id IN (
|
||||
SELECT i.moduleimpl_inscription_id FROM
|
||||
WHERE id IN (
|
||||
SELECT i.id FROM
|
||||
notes_moduleimpl mi, notes_modules mod,
|
||||
notes_formsemestre sem, notes_moduleimpl_inscription i
|
||||
WHERE sem.formsemestre_id = %(formsemestre_id)s
|
||||
AND mi.formsemestre_id = sem.formsemestre_id
|
||||
AND mod.module_id = mi.module_id
|
||||
WHERE sem.id = %(formsemestre_id)s
|
||||
AND mi.formsemestre_id = sem.id
|
||||
AND mod.id = mi.module_id
|
||||
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
|
||||
)
|
||||
""",
|
||||
|
@ -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
|
||||
):
|
||||
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
|
||||
code_ue = None
|
||||
elif valid_semestre:
|
||||
|
@ -66,7 +66,7 @@ from app.scodoc.sco_utils import (
|
||||
LOGOS_IMAGES_ALLOWED_TYPES,
|
||||
)
|
||||
from app import log
|
||||
from app.scodoc.sco_exceptions import ScoGenError
|
||||
from app.scodoc.sco_exceptions import ScoGenError, ScoValueError
|
||||
import sco_version
|
||||
|
||||
PAGE_HEIGHT = defaultPageSize[1]
|
||||
@ -121,6 +121,7 @@ def makeParas(txt, style, suppress_empty=False):
|
||||
"""Returns a list of Paragraph instances from a text
|
||||
with one or more <para> ... </para>
|
||||
"""
|
||||
result = []
|
||||
try:
|
||||
paras = _splitPara(txt)
|
||||
if suppress_empty:
|
||||
@ -133,21 +134,30 @@ def makeParas(txt, style, suppress_empty=False):
|
||||
if m.group(1): # non empty paragraph
|
||||
r.append(para)
|
||||
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:
|
||||
detail = " " + str(e)
|
||||
log(traceback.format_exc())
|
||||
log("Invalid pdf para format: %s" % txt)
|
||||
return [
|
||||
result = [
|
||||
Paragraph(
|
||||
SU(
|
||||
'<font color="red"><i>Erreur: format invalide{}</i></font>'.format(
|
||||
detail
|
||||
)
|
||||
),
|
||||
SU('<font color="red"><i>Erreur: format invalide</i></font>'),
|
||||
style,
|
||||
)
|
||||
]
|
||||
return result
|
||||
|
||||
|
||||
def bold_paras(L, tag="b", close=None):
|
||||
|
@ -88,10 +88,9 @@ def _get_group_info(evaluation_id):
|
||||
groups_tree[partition][group_name] = group_id
|
||||
if partition != TOUS:
|
||||
has_groups = True
|
||||
nb_groups = len(groups_tree)
|
||||
else:
|
||||
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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
return scu.send_file(xls, filename, scu.XLSX_SUFFIX, mime=scu.XLSX_MIMETYPE)
|
||||
# return sco_excel.send_excel_file(xls, filename)
|
||||
|
@ -271,7 +271,7 @@ def formsemestre_synchro_etuds(
|
||||
if partitions: # il y a au moins une vraie partition
|
||||
H.append(
|
||||
f"""<li><a class="stdlink" href="{
|
||||
url_for("scolar.affectGroups",
|
||||
url_for("scolar.affect_groups",
|
||||
scodoc_dept=g.scodoc_dept,
|
||||
partition_id=partitions[0]["partition_id"]
|
||||
)}">Répartir les groupes de {partitions[0]["partition_name"]}</a></li>
|
||||
|
@ -29,13 +29,14 @@
|
||||
"""
|
||||
|
||||
# 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_login import current_user
|
||||
|
||||
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 User
|
||||
@ -171,21 +172,25 @@ def list_users(
|
||||
if not can_modify:
|
||||
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"
|
||||
tab = GenTable(
|
||||
rows=r,
|
||||
columns_ids=(
|
||||
"user_name",
|
||||
"nom_fmt",
|
||||
"prenom_fmt",
|
||||
"email",
|
||||
"dept",
|
||||
"roles_string",
|
||||
"date_expiration",
|
||||
"date_modif_passwd",
|
||||
"passwd_temp",
|
||||
"status_txt",
|
||||
),
|
||||
columns_ids=columns_ids,
|
||||
titles={
|
||||
"user_name": "Login",
|
||||
"nom_fmt": "Nom",
|
||||
@ -195,6 +200,7 @@ def list_users(
|
||||
"roles_string": "Rôles",
|
||||
"date_expiration": "Expiration",
|
||||
"date_modif_passwd": "Modif. mot de passe",
|
||||
"last_seen": "Dernière cnx.",
|
||||
"passwd_temp": "Temp.",
|
||||
"status_txt": "Etat",
|
||||
},
|
||||
@ -206,7 +212,7 @@ def list_users(
|
||||
html_class="table_leftalign list_users",
|
||||
html_with_td_classes=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
|
||||
preferences=sco_preferences.SemPreferences(),
|
||||
)
|
||||
@ -379,7 +385,16 @@ def user_info_page(user_name=None):
|
||||
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)
|
||||
Cherche homonymes.
|
||||
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)
|
||||
- msg: message warning a presenter l'utilisateur
|
||||
"""
|
||||
if not user_name or not nom or not prenom:
|
||||
return False, "champ requis vide"
|
||||
if not email:
|
||||
return False, "vous devriez indiquer le mail de l'utilisateur créé !"
|
||||
MSG_OPT = """Attention: %s (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
|
||||
# ce login existe ?
|
||||
user = _user_list(user_name)
|
||||
if edit and not user: # safety net, le user_name ne devrait pas changer
|
||||
return False, "identifiant %s inexistant" % user_name
|
||||
if not edit and user:
|
||||
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 ?
|
||||
nom = nom.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)
|
||||
for x in similar_users
|
||||
]
|
||||
),
|
||||
)
|
||||
+ MSG_OPT,
|
||||
)
|
||||
# Roles ?
|
||||
if not roles:
|
||||
return False, "aucun rôle sélectionné, êtes vous sûr ?"
|
||||
# ok
|
||||
return True, ""
|
||||
|
||||
|
||||
|
@ -402,7 +402,7 @@ function GotoAnother() {
|
||||
if (groups_unsaved) {
|
||||
alert("Enregistrez ou annulez vos changement avant !");
|
||||
} 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;
|
||||
}
|
||||
|
||||
|
||||
|
@ -4,8 +4,9 @@
|
||||
{% block title %}Une erreur est survenue !{% endblock %}
|
||||
{% block body %}
|
||||
<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>
|
||||
<p><tt style="font-size:60%">{{date}}</tt></p>
|
||||
|
||||
<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
|
||||
|
19
app/templates/error_access_denied.html
Normal file
19
app/templates/error_access_denied.html
Normal 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 %}
|
@ -3,7 +3,7 @@
|
||||
{% macro render_field(field) %}
|
||||
<tr>
|
||||
<td class="wtf-field">{{ field.label }}</td>
|
||||
<td class="wtf-field">{{ field()|safe }}
|
||||
<td class="wtf-field">{{ field(**kwargs)|safe }}
|
||||
{% if field.errors %}
|
||||
<ul class=errors>
|
||||
{% for error in field.errors %}
|
||||
@ -27,7 +27,7 @@
|
||||
{{ render_field(form.nb_rangs) }}
|
||||
{{ render_field(form.etiquetage) }}
|
||||
{% 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
|
||||
{% for partition in form.groups_tree %}
|
||||
<tr>
|
||||
|
43
app/templates/scolar/affect_groups.html
Normal file
43
app/templates/scolar/affect_groups.html
Normal 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"/>
|
||||
|
||||
<input type="button" onClick="submitGroups( target='gmsg' );" value="Enregistrer ces groupes" />
|
||||
|
||||
<input type="button"
|
||||
onClick="document.location = '{{ url_for( 'notes.formsemestre_status', scodoc_dept=g.scodoc_dept, formsemestre_id=formsemestre_id) }}'"
|
||||
value="Annuler" /> É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 }}
|
@ -1,8 +1,13 @@
|
||||
# -*- coding: UTF-8 -*
|
||||
"""ScoDoc Flask views
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from flask import Blueprint
|
||||
from flask import g, current_app
|
||||
from flask_login import current_user
|
||||
|
||||
from app import db
|
||||
from app.scodoc import notesdb as ndb
|
||||
|
||||
scodoc_bp = Blueprint("scodoc", __name__)
|
||||
@ -20,7 +25,14 @@ from app.views import scodoc, notes, scolar, absences, users
|
||||
@scodoc_bp.before_app_request
|
||||
def start_scodoc_request():
|
||||
"""Affecte toutes les requêtes, de tous les blueprints"""
|
||||
# current_app.logger.info(f"start_scodoc_request")
|
||||
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
|
||||
|
@ -1064,6 +1064,10 @@ def AddBilletAbsence(
|
||||
begin et end sont au format ISO (eg "1999-01-08 04:05:06")
|
||||
"""
|
||||
t0 = time.time()
|
||||
begin = str(begin)
|
||||
end = str(end)
|
||||
code_nip = str(code_nip) if code_nip else None
|
||||
|
||||
# check etudid
|
||||
etuds = sco_etud.get_etud_info(etudid=etudid, code_nip=code_nip, filled=True)
|
||||
if not etuds:
|
||||
|
@ -274,7 +274,12 @@ def formsemestre_bulletinetud(
|
||||
xml_with_decisions=False,
|
||||
force_publishing=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(
|
||||
etudid=etudid,
|
||||
formsemestre_id=formsemestre_id,
|
||||
@ -2268,6 +2273,7 @@ sco_publish(
|
||||
"/view_apo_csv_delete",
|
||||
sco_etape_apogee_view.view_apo_csv_delete,
|
||||
Permission.ScoEditApo,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
sco_publish(
|
||||
"/view_scodoc_etuds", sco_etape_apogee_view.view_scodoc_etuds, Permission.ScoEditApo
|
||||
|
@ -61,6 +61,7 @@ from app.decorators import (
|
||||
scodoc,
|
||||
permission_required_compat_scodoc7,
|
||||
)
|
||||
from app.scodoc.sco_exceptions import AccessDenied
|
||||
from app.scodoc.sco_permissions import Permission
|
||||
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"])
|
||||
@login_required
|
||||
def table_etud_in_accessible_depts():
|
||||
|
@ -651,8 +651,8 @@ def formChangeCoordonnees(etudid):
|
||||
|
||||
# --- Gestion des groupes:
|
||||
sco_publish(
|
||||
"/affectGroups",
|
||||
sco_groups_edit.affectGroups,
|
||||
"/affect_groups",
|
||||
sco_groups_edit.affect_groups,
|
||||
Permission.ScoView,
|
||||
methods=["GET", "POST"],
|
||||
)
|
||||
@ -1589,6 +1589,7 @@ def etudident_delete(etudid, dialog_confirmed=False):
|
||||
"admissions",
|
||||
"adresse",
|
||||
"absences",
|
||||
"absences_notifications",
|
||||
"billet_absence",
|
||||
]
|
||||
cursor = cnx.cursor(cursor_factory=ndb.ScoDocCursor)
|
||||
@ -1756,6 +1757,7 @@ def check_group_apogee(group_id, etat=None, fix=False, fixmail=False):
|
||||
@scodoc7func
|
||||
def form_students_import_excel(formsemestre_id=None):
|
||||
"formulaire import xls"
|
||||
formsemestre_id = int(formsemestre_id) if formsemestre_id else None
|
||||
if formsemestre_id:
|
||||
sem = sco_formsemestre.get_formsemestre(formsemestre_id)
|
||||
dest_url = (
|
||||
@ -1888,7 +1890,7 @@ Les champs avec un astérisque (*) doivent être présents (nulls non autorisés
|
||||
else:
|
||||
return sco_import_etuds.students_import_excel(
|
||||
tf[2]["csvfile"],
|
||||
formsemestre_id=formsemestre_id,
|
||||
formsemestre_id=int(formsemestre_id) if formsemestre_id else None,
|
||||
check_homonyms=tf[2]["check_homonyms"],
|
||||
require_ine=tf[2]["require_ine"],
|
||||
)
|
||||
|
@ -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}
|
||||
# add existing user 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 = [
|
||||
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))
|
||||
return "\n".join(H) + "\n" + tf[1] + F
|
||||
|
||||
if not force:
|
||||
ok, msg = sco_users.check_modif_user(
|
||||
edit,
|
||||
user_name=user_name,
|
||||
nom=vals["nom"],
|
||||
prenom=vals["prenom"],
|
||||
email=vals["email"],
|
||||
roles=vals["roles"],
|
||||
)
|
||||
if not ok:
|
||||
H.append(
|
||||
tf_error_message(
|
||||
"""Attention: %s (vous pouvez forcer l'opération en cochant "<em>Ignorer les avertissements</em>" en bas de page)"""
|
||||
% msg
|
||||
)
|
||||
)
|
||||
ok, msg = sco_users.check_modif_user(
|
||||
edit,
|
||||
ignore_optionals=force,
|
||||
user_name=user_name,
|
||||
nom=vals["nom"],
|
||||
prenom=vals["prenom"],
|
||||
email=vals["email"],
|
||||
roles=vals["roles"],
|
||||
)
|
||||
if not ok:
|
||||
H.append(tf_error_message(msg))
|
||||
return "\n".join(H) + "\n" + tf[1] + F
|
||||
|
||||
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 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"]
|
||||
if (current_user.user_name == user_name) and "status" in vals:
|
||||
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:
|
||||
vals["active"] = vals["status"] == ""
|
||||
# traitement des roles: ne doit pas affecter les roles
|
||||
|
@ -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 ###
|
41
scodoc.py
41
scodoc.py
@ -274,6 +274,23 @@ def list_depts(depts=""): # list-dept
|
||||
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()
|
||||
@with_appcontext
|
||||
def import_scodoc7_users(): # import-scodoc7-users
|
||||
@ -329,3 +346,27 @@ def recursive_help(cmd, parent=None):
|
||||
@app.cli.command()
|
||||
def dumphelp():
|
||||
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()
|
||||
|
@ -22,6 +22,8 @@ def test_client():
|
||||
with apptest.test_client() as client:
|
||||
with apptest.app_context():
|
||||
with apptest.test_request_context():
|
||||
# initialize scodoc "g":
|
||||
g.stored_get_formsemestre = {}
|
||||
# erase and reset database:
|
||||
initialize_scodoc_database(erase=True, create_all=True)
|
||||
# Loge l'utilisateur super-admin
|
||||
|
100
tests/unit/test_notes_rattrapage.py
Normal file
100
tests/unit/test_notes_rattrapage.py
Normal 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)
|
@ -27,6 +27,7 @@ from app.scodoc import sco_codes_parcours
|
||||
from app.scodoc import sco_evaluations
|
||||
from app.scodoc import sco_formsemestre_validation
|
||||
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_utils as scu
|
||||
|
||||
@ -197,3 +198,19 @@ def run_sco_basic(verbose=False):
|
||||
assert not sco_parcours_dut.formsemestre_has_decisions(
|
||||
sem["formsemestre_id"]
|
||||
), "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"}
|
||||
|
@ -15,28 +15,46 @@ source "$SCRIPT_DIR/utils.sh"
|
||||
# Ce script doit tourner comme "root"
|
||||
check_uid_root "$0"
|
||||
|
||||
|
||||
# Usage
|
||||
if [ ! $# -eq 2 ]
|
||||
then
|
||||
echo "Usage: $0 archive dbname"
|
||||
echo "Exemple: $0 /tmp/mon-scodoc.tgz SCODOC"
|
||||
usage() {
|
||||
echo "Usage: $0 [ --keep-env ] archive"
|
||||
echo "Exemple: $0 /tmp/mon-scodoc.tgz"
|
||||
echo "OPTION"
|
||||
echo "--keep_env garde la configuration courante"
|
||||
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
|
||||
|
||||
SRC=$1
|
||||
DBNAME=$2
|
||||
|
||||
DB_DUMP="${SCODOC_VAR_DIR}"/SCODOC.dump
|
||||
|
||||
# Safety check
|
||||
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
|
||||
echo "Attention: TOUTES LES DONNEES DE CE SCODOC SERONT REMPLACEES !"
|
||||
echo "Notamment, tous les utilisateurs et departements existants seront effaces !"
|
||||
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 -n "Voulez vous poursuivre cette operation ? (y/n) [n]"
|
||||
read -r ans
|
||||
@ -47,8 +65,13 @@ then
|
||||
fi
|
||||
|
||||
# -- Stop ScoDoc
|
||||
echo "Arrêt de scodoc9..."
|
||||
systemctl stop scodoc9
|
||||
if [ $KEEP -ne 1 ]; 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
|
||||
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}"
|
||||
|
||||
# --- 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 ]
|
||||
then
|
||||
echo "Suppression de la base $DBNAME..."
|
||||
su -c "dropdb $DBNAME" "$SCODOC_USER" || die "Erreur destruction db"
|
||||
echo "Suppression de la base $DB_DEST..."
|
||||
su -c "dropdb $DB_DEST" "$SCODOC_USER" || die "Erreur destruction db"
|
||||
fi
|
||||
su -c "createdb $DBNAME" "$SCODOC_USER" || die "Erreur création db"
|
||||
|
||||
echo "Chargement de la base SQL..."
|
||||
su -c "pg_restore -d $DBNAME ${SCODOC_VAR_DIR}/SCODOC.dump" "$SCODOC_USER" || die "Erreur chargement de la base SQL"
|
||||
|
||||
# -- 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"
|
||||
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
|
||||
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
|
||||
systemctl start scodoc9
|
||||
fi
|
||||
|
||||
|
||||
echo "Terminé."
|
||||
|
@ -20,6 +20,14 @@ then
|
||||
exit 1
|
||||
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 "à un autre ScoDoc 9."
|
||||
echo "Il est vivement recommandé de mettre à jour votre ScoDoc avant."
|
||||
@ -44,8 +52,10 @@ DEST=$1
|
||||
db_name="$SCODOC_DB_PROD" # SCODOC
|
||||
|
||||
# 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"
|
||||
|
||||
echo "création du fichier d'archivage..."
|
||||
# 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"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user