Ajout configuration gloable via formualaire: bonus_sport. Modif migration fichier config. A suivre

This commit is contained in:
Emmanuel Viennet 2021-09-05 12:30:11 +02:00
parent dccae56fe7
commit abd6d53510
17 changed files with 268 additions and 56 deletions

View File

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

View File

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

View File

@ -2,11 +2,12 @@
"""Model : preferences """Model : preferences
""" """
from app import db from app import db, log
from app.scodoc import bonus_sport
class ScoPreference(db.Model): class ScoPreference(db.Model):
"""ScoDoc preferences""" """ScoDoc preferences (par département)"""
__tablename__ = "sco_prefs" __tablename__ = "sco_prefs"
id = db.Column(db.Integer, primary_key=True) 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) name = db.Column(db.String(128), nullable=False, index=True)
value = db.Column(db.Text()) value = db.Column(db.Text())
formsemestre_id = db.Column(db.Integer, db.ForeignKey("notes_formsemestre.id")) 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 flask import g, url_for
from app.models import ScoDocSiteConfig
import app.scodoc.sco_utils as scu import app.scodoc.sco_utils as scu
import app.scodoc.notesdb as ndb import app.scodoc.notesdb as ndb
from app import log from app import log
@ -922,9 +923,13 @@ class NotesTable(object):
if len(coefs_bonus_gen) == 1: if len(coefs_bonus_gen) == 1:
coefs_bonus_gen = [1.0] # irrelevant, may be zero coefs_bonus_gen = [1.0] # irrelevant, may be zero
bonus = scu.CONFIG.compute_bonus( bonus_func = ScoDocSiteConfig.get_bonus_sport_func()
if bonus_func:
bonus = bonus_func(
notes_bonus_gen, coefs_bonus_gen, infos=infos notes_bonus_gen, coefs_bonus_gen, infos=infos
) )
else:
bonus = 0.0
self.bonus[etudid] = bonus self.bonus[etudid] = bonus
infos["moy"] += bonus infos["moy"] += bonus
infos["moy"] = min(infos["moy"], 20.0) # clip bogus bonus infos["moy"] = min(infos["moy"], 20.0) # clip bogus bonus

View File

@ -1,9 +1,9 @@
# -*- mode: python -*- # -*- mode: python -*-
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Configuration de ScoDoc (version 2020) """Configuration de ScoDoc (version ScoDOc 9)
NE PAS MODIFIER localement ce fichier ! 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 from app.scodoc import bonus_sport
@ -20,10 +20,6 @@ CONFIG = AttrDict()
# set to 1 if you want to require INE: # set to 1 if you want to require INE:
CONFIG.always_require_ine = 0 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 # -------------- Documents PDF
# ----------------------------------------------------- # -----------------------------------------------------

View File

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

View File

@ -1031,7 +1031,7 @@ def module_evaluation_renumber(moduleimpl_id, only_if_unumbered=False, redirect=
# -------------- VIEWS # -------------- 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 """HTML description of evaluation, for page headers
edit_in_place: allow in-place editing when permitted (not implemented) 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"] resp = u["prenomnom"]
nomcomplet = u["nomcomplet"] nomcomplet = u["nomcomplet"]
can_edit = sco_permissions_check.can_edit_notes( can_edit = sco_permissions_check.can_edit_notes(
REQUEST.AUTHENTICATED_USER, moduleimpl_id, allow_ens=False current_user, moduleimpl_id, allow_ens=False
) )
link = ( link = (
@ -1223,7 +1223,7 @@ def evaluation_create_form(
if not readonly: if not readonly:
H = ["<h3>%svaluation en %s</h3>" % (action, mod_descr)] H = ["<h3>%svaluation en %s</h3>" % (action, mod_descr)]
else: 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)] 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 E = evals[0] # il y a au moins une evaluation
# description de l'evaluation # description de l'evaluation
if mode == "eval": if mode == "eval":
H = [ H = [sco_evaluations.evaluation_describe(evaluation_id=evaluation_id)]
sco_evaluations.evaluation_describe(
evaluation_id=evaluation_id, REQUEST=REQUEST
)
]
else: else:
H = [] H = []
# groupes # 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>' eval_info = '<span class="eval_info eval_incomplete">Notes incomplètes, évaluation non prise en compte dans les moyennes</span>'
return ( return (
sco_evaluations.evaluation_describe( sco_evaluations.evaluation_describe(evaluation_id=E["evaluation_id"])
evaluation_id=E["evaluation_id"], REQUEST=REQUEST
)
+ eval_info + eval_info
+ html_form + html_form
+ t + t
@ -786,9 +780,7 @@ def evaluation_check_absences_html(
html_sco_header.html_sem_header( html_sco_header.html_sem_header(
REQUEST, "Vérification absences à l'évaluation" REQUEST, "Vérification absences à l'évaluation"
), ),
sco_evaluations.evaluation_describe( sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
evaluation_id=evaluation_id, REQUEST=REQUEST
),
"""<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""", """<p class="help">Vérification de la cohérence entre les notes saisies et les absences signalées.</p>""",
] ]
else: else:

View File

@ -76,9 +76,7 @@ def do_placement_selectetuds(REQUEST):
# description de l'evaluation # description de l'evaluation
H = [ H = [
sco_evaluations.evaluation_describe( sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
evaluation_id=evaluation_id, REQUEST=REQUEST
),
"<h3>Placement et émargement des étudiants</h3>", "<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, cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True, init_qtip=True,
), ),
sco_evaluations.evaluation_describe( sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
evaluation_id=evaluation_id, REQUEST=REQUEST
),
"""<span class="eval_title">Saisie des notes par fichier</span>""", """<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, cssstyles=sco_groups_view.CSSSTYLES,
init_qtip=True, init_qtip=True,
), ),
sco_evaluations.evaluation_describe( sco_evaluations.evaluation_describe(evaluation_id=evaluation_id),
evaluation_id=evaluation_id, REQUEST=REQUEST
),
'<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>', '<div id="saisie_notes"><span class="eval_title">Saisie des notes</span>',
] ]
H.append("""<div id="group-tabs"><table><tr><td>""") H.append("""<div id="group-tabs"><table><tr><td>""")

View File

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

View File

@ -23,9 +23,11 @@
<a class="navbar-brand" href="{{ url_for('scodoc.index') }}">ScoDoc</a> <a class="navbar-brand" href="{{ url_for('scodoc.index') }}">ScoDoc</a>
</div> </div>
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
{% if current_user.is_administrator() %}
<ul class="nav navbar-nav"> <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> </ul>
{% endif %}
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% if current_user.is_anonymous %} {% if current_user.is_anonymous %}
<li><a href="{{ url_for('auth.login') }}">Login</a></li> <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> <li>
<a class="stdlink {{'link_accessible' if current_user.has_permission(Permission.ScoView, dept=dept.acronym) else 'link_unauthorized'}}" <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 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> </li>
{% else %} {% else %}
<li> <li>

View File

@ -31,13 +31,19 @@ Module main: page d'accueil, avec liste des départements
Emmanuel Viennet, 2021 Emmanuel Viennet, 2021
""" """
import flask import flask
from flask import render_template from flask import flash, url_for, redirect, render_template
from flask import request from flask import request
from flask.app import Flask
from flask_login.utils import login_required 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 import sco_version
from app.scodoc import sco_find_etud from app.scodoc import sco_find_etud
from app.decorators import admin_required
from app.scodoc.sco_permissions import Permission from app.scodoc.sco_permissions import Permission
from app.views import scodoc_bp as bp from app.views import scodoc_bp as bp
@ -66,6 +72,42 @@ def table_etud_in_accessible_depts():
return sco_find_etud.table_etud_in_accessible_depts(expnom=request.form["expnom"]) 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 # essais
# @bp.route("/testlog") # @bp.route("/testlog")
# def 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

@ -125,6 +125,29 @@ migrate_local_files() {
mkdir -p "$old_logs_dest" || die "erreur creation $old_logs_dest" mkdir -p "$old_logs_dest" || die "erreur creation $old_logs_dest"
mv "${SCODOC7_HOME}"/log/* "$old_logs_dest" || die "erreur mv" 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
# 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 # Templates locaux poursuites etudes
if [ -e "${SCODOC7_HOME}"/config/doc_poursuites_etudes/local ] if [ -e "${SCODOC7_HOME}"/config/doc_poursuites_etudes/local ]
then then