Merge remote-tracking branch 'origin/master' into clean

This commit is contained in:
Jean-Marie Place 2021-09-05 14:52:58 +02:00
commit a4d0205cc7
21 changed files with 288 additions and 86 deletions

View File

@ -303,8 +303,9 @@ def clear_scodoc_cache():
# --------- Logging
def log(msg: str, silent_test=True):
"""log a message.
If Flask app, use configured logger, else stderr."""
if silent_test and current_app.config["TESTING"]:
If Flask app, use configured logger, else stderr.
"""
if silent_test and current_app and current_app.config["TESTING"]:
return
try:
dept = getattr(g, "scodoc_dept", "")

View File

@ -63,4 +63,4 @@ from app.models.notes import (
NotesNotes,
NotesNotesLog,
)
from app.models.preferences import ScoPreference
from app.models.preferences import ScoPreference, ScoDocSiteConfig

View File

@ -2,11 +2,12 @@
"""Model : preferences
"""
from app import db
from app import db, log
from app.scodoc import bonus_sport
class ScoPreference(db.Model):
"""ScoDoc preferences"""
"""ScoDoc preferences (par département)"""
__tablename__ = "sco_prefs"
id = db.Column(db.Integer, primary_key=True)
@ -17,3 +18,84 @@ class ScoPreference(db.Model):
name = db.Column(db.String(128), nullable=False, index=True)
value = db.Column(db.Text())
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id"))
class ScoDocSiteConfig(db.Model):
"""Config. d'un site
Nouveau en ScoDoc 9: va regrouper les paramètres qui dans les versions
antérieures étaient dans scodoc_config.py
"""
__tablename__ = "scodoc_site_config"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(128), nullable=False, index=True)
value = db.Column(db.Text())
BONUS_SPORT = "bonus_sport_func_name"
def __init__(self, name, value):
self.name = name
self.value = value
def __repr__(self):
return f"<{self.__class__.__name__}('{self.name}', '{self.value}')>"
@classmethod
def set_bonus_sport_func(cls, func_name):
"""Record bonus_sport config.
If func_name not defined, raise NameError
"""
if func_name not in cls.get_bonus_sport_func_names():
raise NameError("invalid function name for bonus_sport")
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c:
log("setting to " + func_name)
c.value = func_name
else:
c = ScoDocSiteConfig(cls.BONUS_SPORT, func_name)
db.session.add(c)
db.session.commit()
@classmethod
def get_bonus_sport_func_name(cls):
"""Get configured bonus function name, or None if None."""
f = cls.get_bonus_sport_func_from_name()
if f is None:
return ""
else:
return f.__name__
@classmethod
def get_bonus_sport_func(cls):
"""Get configured bonus function, or None if None."""
return cls.get_bonus_sport_func_from_name()
@classmethod
def get_bonus_sport_func_from_name(cls, func_name=None):
"""returns bonus func with specified name.
If name not specified, return the configured function.
None if no bonus function configured.
Raises NameError if func_name not found in module bonus_sport.
"""
if func_name is None:
c = ScoDocSiteConfig.query.filter_by(name=cls.BONUS_SPORT).first()
if c is None:
return None
func_name = c.value
if func_name == "": # pas de bonus défini
return None
return getattr(bonus_sport, func_name)
@classmethod
def get_bonus_sport_func_names(cls):
"""List available functions names
(starting with empty string to represent "no bonus function").
"""
return [""] + sorted(
[
getattr(bonus_sport, name).__name__
for name in dir(bonus_sport)
if name.startswith("bonus_")
]
)

View File

@ -35,6 +35,7 @@ from operator import itemgetter
from flask import g, url_for
from app.models import ScoDocSiteConfig
import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb
from app import log
@ -922,9 +923,13 @@ class NotesTable(object):
if len(coefs_bonus_gen) == 1:
coefs_bonus_gen = [1.0] # irrelevant, may be zero
bonus = scu.CONFIG.compute_bonus(
notes_bonus_gen, coefs_bonus_gen, infos=infos
)
bonus_func = ScoDocSiteConfig.get_bonus_sport_func()
if bonus_func:
bonus = bonus_func(
notes_bonus_gen, coefs_bonus_gen, infos=infos
)
else:
bonus = 0.0
self.bonus[etudid] = bonus
infos["moy"] += bonus
infos["moy"] = min(infos["moy"], 20.0) # clip bogus bonus

View File

@ -1,9 +1,9 @@
# -*- mode: python -*-
# -*- coding: utf-8 -*-
"""Configuration de ScoDoc (version 2020)
"""Configuration de ScoDoc (version ScoDOc 9)
NE PAS MODIFIER localement ce fichier !
mais éditer /opt/scodoc/var/scodoc/config/scodoc_local.py
mais éditer /opt/scodoc-data/config/scodoc_local.py
"""
from app.scodoc import bonus_sport
@ -20,10 +20,6 @@ CONFIG = AttrDict()
# set to 1 if you want to require INE:
CONFIG.always_require_ine = 0
# The base URL, use only if you are behind a proxy
# eg "https://scodoc.example.net/ScoDoc"
CONFIG.ABSOLUTE_URL = ""
# -----------------------------------------------------
# -------------- Documents PDF
# -----------------------------------------------------
@ -52,12 +48,6 @@ CONFIG.LOGO_HEADER_HEIGHT = 28
# scodoc_name: le nom du logiciel (ScoDoc actuellement, voir sco_version.py)
CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s"
#
# ------------- Calcul bonus modules optionnels (sport, culture...) -------------
#
CONFIG.compute_bonus = bonus_sport.bonus_iutv
# Mettre "bonus_demo" pour logguer des informations utiles au developpement...
# ------------- Capitalisation des UEs -------------
# Deux écoles:

View File

@ -17,27 +17,26 @@ def load_local_configuration(scodoc_cfg_dir):
"""Load local configuration file (if exists)
and merge it with CONFIG.
"""
# this path should be synced with upgrade.sh
LOCAL_CONFIG_FILENAME = os.path.join(scodoc_cfg_dir, "scodoc_local.py")
LOCAL_CONFIG = None
if os.path.exists(LOCAL_CONFIG_FILENAME):
local_config_filename = os.path.join(scodoc_cfg_dir, "scodoc_local.py")
local_config = None
if os.path.exists(local_config_filename):
if not scodoc_cfg_dir in sys.path:
sys.path.insert(1, scodoc_cfg_dir)
sys.path.insert(0, scodoc_cfg_dir)
try:
from scodoc_local import CONFIG as LOCAL_CONFIG
from scodoc_local import CONFIG as local_config
log("imported %s" % LOCAL_CONFIG_FILENAME)
log("imported %s" % local_config_filename)
except ImportError:
log("Error: can't import %s" % LOCAL_CONFIG_FILENAME)
del sys.path[1]
if LOCAL_CONFIG is None:
log("Error: can't import %s" % local_config_filename)
del sys.path[0]
if local_config is None:
return
# Now merges local config in our CONFIG
for x in [x for x in dir(LOCAL_CONFIG) if x[0] != "_"]:
v = getattr(LOCAL_CONFIG, x)
if not v in sco_config.CONFIG:
log("Warning: local config setting unused parameter %s (skipped)" % x)
for x in [x for x in local_config if x[0] != "_"]:
v = local_config.get(x)
if not x in sco_config.CONFIG:
log(f"Warning: local config setting unused parameter {x} (skipped)")
else:
if v != sco_config.CONFIG[x]:
log("Setting parameter %s from %s" % (x, LOCAL_CONFIG_FILENAME))
log(f"Setting parameter {x} from {local_config_filename}")
sco_config.CONFIG[x] = v

View File

@ -1031,7 +1031,7 @@ def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=
# -------------- VIEWS
def evaluation_describe(evaluation_id="", edit_in_place=True, REQUEST=None):
def evaluation_describe(evaluation_id="", edit_in_place=True):
"""HTML description of evaluation, for page headers
edit_in_place: allow in-place editing when permitted (not implemented)
"""
@ -1046,7 +1046,7 @@ def evaluation_describe(evaluation_id="", edit_in_place=True, REQUEST=None):
resp = u["prenomnom"]
nomcomplet = u["nomcomplet"]
can_edit = sco_permissions_check.can_edit_notes(
REQUEST.AUTHENTICATED_USER, moduleimpl_id, allow_ens=False
current_user, moduleimpl_id, allow_ens=False
)
link = (
@ -1223,7 +1223,7 @@ def evaluation_create_form(
if not readonly:
H = ["<h3>%svaluation en %s</h3>" % (action, mod_descr)]
else:
return evaluation_describe(evaluation_id, REQUEST=REQUEST)
return evaluation_describe(evaluation_id)
heures = ["%02dh%02d" % (h, m) for h in range(8, 19) for m in (0, 30)]
#

View File

@ -80,11 +80,7 @@ def do_evaluation_listenotes(REQUEST):
E = evals[0] # il y a au moins une evaluation
# description de l'evaluation
if mode == "eval":
H = [
sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id, REQUEST=REQUEST
)
]
H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
else:
H = []
# groupes
@ -529,9 +525,7 @@ def _make_table_notes(
eval_info = '<span class="eval_info eval_incomplete">Notes incomplètes, évaluation non prise en compte dans les moyennes</span>'
return (
sco_evaluations.evaluation_describe(
evaluation_id=E["evaluation_id"], REQUEST=REQUEST
)
sco_evaluations.evaluation_describe(evaluation_id=E["evaluation_id"])
+ eval_info
+ html_form
+ t
@ -786,9 +780,7 @@ def evaluation_check_absences_html(
html_sco_header.html_sem_header(
REQUEST, "Vérification absences à l'évaluation"
),
sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id, REQUEST=REQUEST
),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
]
else:

View File

@ -186,9 +186,7 @@ def do_placement_selectetuds():
# M = sco_moduleimpl.do_moduleimpl_list( moduleimpl_id=E["moduleimpl_id"])[0]
# description de l'evaluation
H = [
sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id, REQUEST=REQUEST
),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
"<h3>Placement et émargement des étudiants</h3>",
]
#

View File

@ -630,9 +630,7 @@ def saisie_notes_tableur(evaluation_id, group_ids=[], REQUEST=None):
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id, REQUEST=REQUEST
),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
"""<span class="eval_title">Saisie des notes par fichier</span>""",
]
@ -909,9 +907,7 @@ def saisie_notes(evaluation_id, group_ids=[], REQUEST=None):
cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True,
),
sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id, REQUEST=REQUEST
),
sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
'<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>',
]
H.append("""<div id="group-tabs"><table><tr><td>""")

View File

@ -1062,6 +1062,14 @@ h2.formsemestre, .gtrcontent h2 {
#formnotes td.tf-fieldlabel {
border-bottom: 1px dotted #fdcaca;
}
/* Formulaires ScoDoc 9 */
form.sco-form {
margin-top: 1em;
}
div.sco-submit {
margin-top: 2em;
}
/*
.formsemestre_menubar {
border-top: 3px solid #67A7E3;

View File

@ -23,9 +23,11 @@
<a class="navbar-brand" href="{{ url_for('scodoc.index') }}">ScoDoc</a>
</div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
{% if current_user.is_administrator() %}
<ul class="nav navbar-nav">
<li><a href="{{ url_for('scodoc.index') }}">Home</a></li>
<li><a href="{{ url_for('scodoc.configuration') }}">Configuration</a></li>
</ul>
{% endif %}
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_anonymous %}
<li><a href="{{ url_for('auth.login') }}">Login</a></li>

View File

@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}
{% macro render_field(field) %}
<div>
<span class="wtf-field">{{ field.label }} :</span>
<span class="wtf-field">{{ field()|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</span>
</div>
{% endmacro %}
{% block app_content %}
<h1>Configuration générale</h1>
<p class="help">Les paramètres donnés ici s'appliquent à tout ScoDoc (tous les départements).</p>
<form class="sco-form" action="" method="post" novalidate>
{{ form.hidden_tag() }}
{{ render_field(form.bonus_sport_func_name)}}
{# <p>
{{ form.bonus_sport_func_name.label }}<br>
{{ form.bonus_sport_func_name() }}
</p> #}
<div class="sco-submit">{{ form.submit() }}</div>
</form>
{% endblock %}

View File

@ -15,7 +15,7 @@
<li>
<a class="stdlink {{'link_accessible' if current_user.has_permission(Permission.ScoView, dept=dept.acronym) else 'link_unauthorized'}}"
href="{{url_for('scolar.index_html', scodoc_dept=dept.acronym)}}">Département
{{dept.acronym}}</a>
{{dept.preferences.filter_by(name="DeptName").first().value}}</a>
</li>
{% else %}
<li>

View File

@ -31,13 +31,19 @@ Module main: page d'accueil, avec liste des départements
Emmanuel Viennet, 2021
"""
import flask
from flask import render_template
from flask import flash, url_for, redirect, render_template
from flask import request
from flask.app import Flask
from flask_login.utils import login_required
from flask_wtf import FlaskForm
from wtforms import SelectField, SubmitField
from app.models import Departement
# from wtforms.validators import DataRequired
from app.models import Departement, ScoDocSiteConfig
import sco_version
from app.scodoc import sco_find_etud
from app.decorators import admin_required
from app.scodoc.sco_permissions import Permission
from app.views import scodoc_bp as bp
@ -66,6 +72,42 @@ def table_etud_in_accessible_depts():
return sco_find_etud.table_etud_in_accessible_depts(expnom=request.form["expnom"])
# ---- CONFIGURATION
class ScoDocConfigurationForm(FlaskForm):
"Panneau de configuration général"
# très préliminaire ;-)
# On veut y mettre la fonction bonus et ensuite les logos
bonus_sport_func_name = SelectField(
label="Fonction de calcul des bonus sport&culture",
choices=[
(x, x if x else "Aucune")
for x in ScoDocSiteConfig.get_bonus_sport_func_names()
],
)
submit = SubmitField("Enregistrer")
@bp.route("/ScoDoc/configuration", methods=["GET", "POST"])
@admin_required
def configuration():
"Panneau de configuration général"
form = ScoDocConfigurationForm(
bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func_name()
)
if form.validate_on_submit():
ScoDocSiteConfig.set_bonus_sport_func(form.bonus_sport_func_name.data)
flash(f"Configuration enregistrée")
return redirect(url_for("scodoc.index"))
return render_template(
"configuration.html",
title="Configuration ScoDoc",
form=form,
# bonus_sport_func_name=ScoDocSiteConfig.get_bonus_sport_func(),
)
# essais
# @bp.route("/testlog")
# def testlog():

View File

@ -0,0 +1,35 @@
"""Table configuration site
Revision ID: f73251d1d825
Revises: f6e7d2e01be1
Create Date: 2021-09-04 23:10:39.149965
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f73251d1d825'
down_revision = 'f6e7d2e01be1'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('scodoc_site_config',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('value', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_scodoc_site_config_name'), 'scodoc_site_config', ['name'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_scodoc_site_config_name'), table_name='scodoc_site_config')
op.drop_table('scodoc_site_config')
# ### end Alembic commands ###

View File

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

View File

@ -51,18 +51,17 @@ def import_scodoc7_user_db(scodoc7_db="dbname=SCOUSERS"):
)
# Set roles:
# ScoDoc7 roles are stored as 'AdminRT,EnsRT'
# ou, dans les rares cas où le dept est en minuscules
# "Ensgeii,Admingeii"
if u7["roles"]:
roles7 = u7["roles"].split(",")
else:
roles7 = []
for role_dept in roles7:
# Cas particulier RespPeRT
m = re.match(r"^(-?RespPe)([A-Z][A-Za-z0-9]*?)$", role_dept)
# Migre les rôles RespPeX, EnsX, AdminX, SecrX et ignore les autres
m = re.match(r"^(-?Ens|-?Secr|-?ResPe|-?Admin)(.*)$", role_dept)
if not m:
# Cas général: eg EnsRT
m = re.match(r"^(-?[A-Za-z0-9]+?)([A-Z][A-Za-z0-9]*?)$", role_dept)
if not m:
msg = f"User {user_name}: invalid role '{role_dept}' (ignoring)"
msg = f"User {user_name}: role inconnu '{role_dept}' (ignoré)"
current_app.logger.warning(msg)
messages.append(msg)
else:

View File

@ -125,6 +125,29 @@ migrate_local_files() {
mkdir -p "$old_logs_dest" || die "erreur creation $old_logs_dest"
mv "${SCODOC7_HOME}"/log/* "$old_logs_dest" || die "erreur mv"
# Le fichier de customization local:
# peut être dans .../var/config/scodoc_local.py
# ou bien, sur les très anciennes installs, dans Products/ScoDoc/config/scodoc_config.py
# (si migration, copié dans SCODOC7_HOME/config/scodoc_config.py)
# en principe ScoDoc 9 est encore compatible avec cet ancien fichier.
# donc:
if [ ! -e "$SCODOC_VAR_DIR"/scodoc_local.py ]
then
echo "note: pas de fichier scodoc_local.py (ok)."
# if [ "$INPLACE" == 1 ]
# then
# scodoc_config_filename = "${SCODOC7_HOME}"/Products/ScoDoc/config/scodoc_config.py
# else
# scodoc_config_filename = "${SCODOC7_HOME}"/config/scodoc_config.py
# fi
# # Le fichier distribué avait-il été modifié ?
# if [ $(md5sum "$scodoc_config_filename" | cut -f1 -d ' ') == "378caca5cb2e3b2753f5989c0762b8cc" ]
# then
# echo "copying $scodoc_config_filename to $SCODOC_VAR_DIR/scodoc_local.py"
# cp "$scodoc_config_filename" "$SCODOC_VAR_DIR"/scodoc_local.py || die "erreur cp"
# fi
fi
# Templates locaux poursuites etudes
if [ -e "${SCODOC7_HOME}"/config/doc_poursuites_etudes/local ]
then
@ -170,8 +193,8 @@ su -c "(cd $SCODOC_DIR && source venv/bin/activate && flask import-scodoc7-users
# (ils ne sont d'ailleurs plus utilisés par ScoDoc 9)
for f in "$SCODOC_VAR_DIR"/config/depts/*.cfg
do
dept=$(basename "${f%.*}")
db_name=$(echo "SCO$dept" | tr "[:lower:]" "[:upper:]")
dept=$(basename "${f%.*}") # le nom du dept peut-être en minuscules et/ou majuscules (geii, GEII...)
db_name=$(echo "SCO$dept" | tr "[:lower:]" "[:upper:]") # nom de BD toujours en majuscule
echo
echo "----------------------------------------------"
echo "| MIGRATION DU DEPARTEMENT $dept"
@ -192,6 +215,18 @@ fi
# Précaution a priori inutile (import-scodoc7-dept efface les caches)
systemctl restart redis
# --- THE END
echo
echo "Migration terminée."
echo "Vérifiez le fichier de log /opt/scodoc-data/log/migration79.log"
echo "et:"
echo "- prévenez les utilisateurs dont le login aurait changé."
echo "- dans ScodoC, en tant qu'admin, vérifier la configuration et"
echo " notamment la fonction de calcul du bonus sport, dont le réglage"
echo " est différent en ScoDoc 9 (plus de fichier de configuration python,"
echo " passer par le formulaire de configuration.)"
echo
# Commande listant les nom des departement en DB:
# Liste des bases de données de département:

View File

@ -70,14 +70,6 @@ CONFIG.LOGO_HEADER_HEIGHT = 28 # taille verticale dans le document en millimetr
CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s"
#
# ------------- Calcul bonus modules optionnels (sport, culture...) -------------
#
from bonus_sport import *
CONFIG.compute_bonus = bonus_iutv
# Mettre "bonus_demo" pour logguer des informations utiles au developpement...
#
# ------------- Capitalisation des UEs -------------
# Deux écoles:

View File

@ -58,13 +58,6 @@ CONFIG = CFG()
# CONFIG.DEFAULT_PDF_FOOTER_TEMPLATE = "Edité par %(scodoc_name)s le %(day)s/%(month)s/%(year)s à %(hour)sh%(minute)s sur %(server_url)s"
#
# ------------- Calcul bonus modules optionnels (sport, culture...) -------------
#
# CONFIG.compute_bonus = bonus_sport.bonus_iutv
# Mettre "bonus_demo" pour logguer des informations utiles au developpement...
#
# ------------- Capitalisation des UEs -------------
# Deux écoles: