feat: add config page
This commit is contained in:
11
app.py
11
app.py
@@ -4,11 +4,13 @@ from flask import Flask, render_template
|
||||
from models import db, Assessment, Student, ClassGroup
|
||||
from commands import init_db
|
||||
from config import config
|
||||
from app_config import config_manager
|
||||
|
||||
# Import blueprints
|
||||
from routes.assessments import bp as assessments_bp
|
||||
from routes.exercises import bp as exercises_bp
|
||||
from routes.grading import bp as grading_bp
|
||||
from routes.config import bp as config_bp
|
||||
|
||||
def create_app(config_name=None):
|
||||
if config_name is None:
|
||||
@@ -17,6 +19,9 @@ def create_app(config_name=None):
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(config[config_name])
|
||||
|
||||
# Initialiser la configuration de l'application
|
||||
app.app_config = config_manager
|
||||
|
||||
# Configuration du logging
|
||||
if not app.debug and not app.testing:
|
||||
if not os.path.exists('logs'):
|
||||
@@ -33,11 +38,17 @@ def create_app(config_name=None):
|
||||
|
||||
# Initialize extensions
|
||||
db.init_app(app)
|
||||
|
||||
# Initialiser la configuration par défaut après l'initialisation de la DB
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
config_manager.initialize_default_config()
|
||||
|
||||
# Register blueprints
|
||||
app.register_blueprint(assessments_bp)
|
||||
app.register_blueprint(exercises_bp)
|
||||
app.register_blueprint(grading_bp)
|
||||
app.register_blueprint(config_bp)
|
||||
|
||||
# Register CLI commands
|
||||
app.cli.add_command(init_db)
|
||||
|
||||
326
app_config.py
Normal file
326
app_config.py
Normal file
@@ -0,0 +1,326 @@
|
||||
from typing import Dict, Any, Optional, List
|
||||
from flask import current_app
|
||||
from models import db, AppConfig, CompetenceScaleValue, Competence
|
||||
|
||||
class ConfigManager:
|
||||
"""Gestionnaire de configuration de l'application Notytex utilisant SQLAlchemy."""
|
||||
|
||||
@property
|
||||
def default_config(self) -> Dict[str, Any]:
|
||||
"""Configuration par défaut de l'application."""
|
||||
return {
|
||||
'context': {
|
||||
'school_year': '2025-2026'
|
||||
},
|
||||
'evaluations': {
|
||||
'default_grading_system': 'competences',
|
||||
'competence_scale': {
|
||||
'values': {
|
||||
'0': {
|
||||
'label': 'A revoir',
|
||||
'color': '#ef4444',
|
||||
'included_in_total': True
|
||||
},
|
||||
'1': {
|
||||
'label': 'Des choses justes',
|
||||
'color': '#f6d32d',
|
||||
'included_in_total': True
|
||||
},
|
||||
'2': {
|
||||
'label': 'Globalement ok',
|
||||
'color': '#8ff0a4',
|
||||
'included_in_total': True
|
||||
},
|
||||
'3': {
|
||||
'label': 'Parfait',
|
||||
'color': '#008000',
|
||||
'included_in_total': True
|
||||
},
|
||||
'.': {
|
||||
'label': 'Pas de réponse',
|
||||
'color': '#6b7280',
|
||||
'included_in_total': True
|
||||
},
|
||||
'd': {
|
||||
'label': 'Dispensé',
|
||||
'color': '#c0bfbc',
|
||||
'included_in_total': False
|
||||
}
|
||||
}
|
||||
},
|
||||
'competences': [
|
||||
{
|
||||
'name': 'Calculer',
|
||||
'color': '#3b82f6',
|
||||
'icon': 'calculator'
|
||||
},
|
||||
{
|
||||
'name': 'Raisonner',
|
||||
'color': '#8b5cf6',
|
||||
'icon': 'lightbulb'
|
||||
},
|
||||
{
|
||||
'name': 'Communiquer',
|
||||
'color': '#06b6d4',
|
||||
'icon': 'chat-bubble-left-right'
|
||||
},
|
||||
{
|
||||
'name': 'Modéliser',
|
||||
'color': '#10b981',
|
||||
'icon': 'cube'
|
||||
},
|
||||
{
|
||||
'name': 'Représenter',
|
||||
'color': '#f59e0b',
|
||||
'icon': 'chart-bar'
|
||||
},
|
||||
{
|
||||
'name': 'Chercher',
|
||||
'color': '#ef4444',
|
||||
'icon': 'magnifying-glass'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def initialize_default_config(self) -> None:
|
||||
"""Initialise la configuration par défaut en base de données si elle n'existe pas."""
|
||||
# Configuration simple
|
||||
for section, values in self.default_config.items():
|
||||
if section == 'context':
|
||||
for key, value in values.items():
|
||||
config_key = f"{section}.{key}"
|
||||
if not AppConfig.query.filter_by(key=config_key).first():
|
||||
config = AppConfig(key=config_key, value=str(value))
|
||||
db.session.add(config)
|
||||
elif section == 'evaluations':
|
||||
# Système de notation par défaut
|
||||
if 'default_grading_system' in values:
|
||||
config_key = f"{section}.default_grading_system"
|
||||
if not AppConfig.query.filter_by(key=config_key).first():
|
||||
config = AppConfig(key=config_key, value=values['default_grading_system'])
|
||||
db.session.add(config)
|
||||
|
||||
# Échelle des compétences
|
||||
if CompetenceScaleValue.query.count() == 0:
|
||||
scale_values = self.default_config['evaluations']['competence_scale']['values']
|
||||
for value, config in scale_values.items():
|
||||
scale_value = CompetenceScaleValue(
|
||||
value=value,
|
||||
label=config['label'],
|
||||
color=config['color'],
|
||||
included_in_total=config['included_in_total']
|
||||
)
|
||||
db.session.add(scale_value)
|
||||
|
||||
# Compétences
|
||||
if Competence.query.count() == 0:
|
||||
competences = self.default_config['evaluations']['competences']
|
||||
for i, comp in enumerate(competences):
|
||||
competence = Competence(
|
||||
name=comp['name'],
|
||||
color=comp['color'],
|
||||
icon=comp['icon'],
|
||||
order_index=i
|
||||
)
|
||||
db.session.add(competence)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
def get(self, key_path: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Récupère une valeur de configuration.
|
||||
|
||||
Args:
|
||||
key_path: Chemin vers la valeur (ex: 'context.school_year')
|
||||
default: Valeur par défaut si la clé n'existe pas
|
||||
|
||||
Returns:
|
||||
La valeur de configuration ou la valeur par défaut
|
||||
"""
|
||||
# Configuration simple stockée dans AppConfig
|
||||
config = AppConfig.query.filter_by(key=key_path).first()
|
||||
if config:
|
||||
return config.value
|
||||
|
||||
# Valeurs par défaut pour les cas non trouvés
|
||||
return self._get_default_value(key_path, default)
|
||||
|
||||
def _get_default_value(self, key_path: str, fallback: Any) -> Any:
|
||||
"""Récupère une valeur par défaut depuis la configuration par défaut."""
|
||||
if fallback is not None:
|
||||
return fallback
|
||||
|
||||
keys = key_path.split('.')
|
||||
value = self.default_config
|
||||
|
||||
try:
|
||||
for key in keys:
|
||||
value = value[key]
|
||||
return value
|
||||
except (KeyError, TypeError):
|
||||
return None
|
||||
|
||||
def set(self, key_path: str, value: Any) -> None:
|
||||
"""
|
||||
Définit une valeur de configuration.
|
||||
|
||||
Args:
|
||||
key_path: Chemin vers la valeur (ex: 'context.school_year')
|
||||
value: Nouvelle valeur
|
||||
"""
|
||||
config = AppConfig.query.filter_by(key=key_path).first()
|
||||
if config:
|
||||
config.value = str(value)
|
||||
else:
|
||||
config = AppConfig(key=key_path, value=str(value))
|
||||
db.session.add(config)
|
||||
|
||||
def save(self) -> bool:
|
||||
"""Sauvegarde la configuration en base de données."""
|
||||
try:
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Erreur lors de la sauvegarde de la configuration: {e}")
|
||||
return False
|
||||
|
||||
def get_competence_scale_values(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Récupère les valeurs de l'échelle des compétences."""
|
||||
scale_values = CompetenceScaleValue.query.all()
|
||||
result = {}
|
||||
for scale_value in scale_values:
|
||||
# Convertir en int si c'est un nombre, sinon garder string
|
||||
key = int(scale_value.value) if scale_value.value.isdigit() else scale_value.value
|
||||
result[key] = {
|
||||
'label': scale_value.label,
|
||||
'color': scale_value.color,
|
||||
'included_in_total': scale_value.included_in_total
|
||||
}
|
||||
return result
|
||||
|
||||
def get_competences_list(self) -> List[Dict[str, Any]]:
|
||||
"""Récupère la liste des compétences configurées."""
|
||||
competences = Competence.query.order_by(Competence.order_index).all()
|
||||
return [
|
||||
{
|
||||
'name': comp.name,
|
||||
'color': comp.color,
|
||||
'icon': comp.icon
|
||||
}
|
||||
for comp in competences
|
||||
]
|
||||
|
||||
def get_school_year(self) -> str:
|
||||
"""Récupère l'année scolaire courante."""
|
||||
return self.get('context.school_year', '2025-2026')
|
||||
|
||||
def get_default_grading_system(self) -> str:
|
||||
"""Récupère le système de notation par défaut."""
|
||||
return self.get('evaluations.default_grading_system', 'competences')
|
||||
|
||||
# Méthodes spécifiques pour la gestion des compétences
|
||||
|
||||
def add_competence(self, name: str, color: str, icon: str) -> bool:
|
||||
"""Ajoute une nouvelle compétence."""
|
||||
try:
|
||||
# Obtenir le prochain index d'ordre
|
||||
max_order = db.session.query(db.func.max(Competence.order_index)).scalar() or 0
|
||||
|
||||
competence = Competence(
|
||||
name=name,
|
||||
color=color,
|
||||
icon=icon,
|
||||
order_index=max_order + 1
|
||||
)
|
||||
db.session.add(competence)
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Erreur lors de l'ajout de la compétence: {e}")
|
||||
return False
|
||||
|
||||
def update_competence(self, competence_id: int, name: str, color: str, icon: str) -> bool:
|
||||
"""Met à jour une compétence."""
|
||||
try:
|
||||
competence = Competence.query.get(competence_id)
|
||||
if competence:
|
||||
competence.name = name
|
||||
competence.color = color
|
||||
competence.icon = icon
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Erreur lors de la mise à jour de la compétence: {e}")
|
||||
return False
|
||||
|
||||
def delete_competence(self, competence_id: int) -> bool:
|
||||
"""Supprime une compétence."""
|
||||
try:
|
||||
competence = Competence.query.get(competence_id)
|
||||
if competence:
|
||||
db.session.delete(competence)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Erreur lors de la suppression de la compétence: {e}")
|
||||
return False
|
||||
|
||||
# Méthodes spécifiques pour la gestion de l'échelle
|
||||
|
||||
def add_scale_value(self, value: str, label: str, color: str, included_in_total: bool = True) -> bool:
|
||||
"""Ajoute une nouvelle valeur à l'échelle."""
|
||||
try:
|
||||
scale_value = CompetenceScaleValue(
|
||||
value=value,
|
||||
label=label,
|
||||
color=color,
|
||||
included_in_total=included_in_total
|
||||
)
|
||||
db.session.add(scale_value)
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Erreur lors de l'ajout de la valeur d'échelle: {e}")
|
||||
return False
|
||||
|
||||
def update_scale_value(self, value: str, label: str, color: str, included_in_total: bool) -> bool:
|
||||
"""Met à jour une valeur de l'échelle."""
|
||||
try:
|
||||
scale_value = CompetenceScaleValue.query.get(value)
|
||||
if scale_value:
|
||||
scale_value.label = label
|
||||
scale_value.color = color
|
||||
scale_value.included_in_total = included_in_total
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Erreur lors de la mise à jour de la valeur d'échelle: {e}")
|
||||
return False
|
||||
|
||||
def delete_scale_value(self, value: str) -> bool:
|
||||
"""Supprime une valeur de l'échelle."""
|
||||
try:
|
||||
scale_value = CompetenceScaleValue.query.get(value)
|
||||
if scale_value:
|
||||
db.session.delete(scale_value)
|
||||
db.session.commit()
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
print(f"Erreur lors de la suppression de la valeur d'échelle: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Instance globale de configuration
|
||||
config_manager = ConfigManager()
|
||||
46
models.py
46
models.py
@@ -246,4 +246,48 @@ class Grade(db.Model):
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Grade {self.value} for {self.student.first_name if self.student else "Unknown"}>'
|
||||
return f'<Grade {self.value} for {self.student.first_name if self.student else "Unknown"}>'
|
||||
|
||||
# Configuration tables
|
||||
|
||||
class AppConfig(db.Model):
|
||||
"""Configuration simple de l'application (clé-valeur)."""
|
||||
__tablename__ = 'app_config'
|
||||
|
||||
key = db.Column(db.String(100), primary_key=True)
|
||||
value = db.Column(db.Text, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<AppConfig {self.key}={self.value}>'
|
||||
|
||||
class CompetenceScaleValue(db.Model):
|
||||
"""Valeurs de l'échelle des compétences (0, 1, 2, 3, ., d, etc.)."""
|
||||
__tablename__ = 'competence_scale_values'
|
||||
|
||||
value = db.Column(db.String(10), primary_key=True) # '0', '1', '2', '3', '.', 'd', etc.
|
||||
label = db.Column(db.String(100), nullable=False)
|
||||
color = db.Column(db.String(7), nullable=False) # Format #RRGGBB
|
||||
included_in_total = db.Column(db.Boolean, default=True, nullable=False)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<CompetenceScaleValue {self.value}: {self.label}>'
|
||||
|
||||
class Competence(db.Model):
|
||||
"""Liste des compétences (Calculer, Raisonner, etc.)."""
|
||||
__tablename__ = 'competences'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), unique=True, nullable=False)
|
||||
color = db.Column(db.String(7), nullable=False) # Format #RRGGBB
|
||||
icon = db.Column(db.String(50), nullable=False)
|
||||
order_index = db.Column(db.Integer, default=0) # Pour l'ordre d'affichage
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Competence {self.name}>'
|
||||
@@ -5,7 +5,7 @@ description = "Application web de gestion scolaire"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"Flask>=2.3.3",
|
||||
"Flask-SQLAlchemy>=3.0.5",
|
||||
"Flask-SQLAlchemy>=3.0.5",
|
||||
"Flask-WTF>=1.1.1",
|
||||
"WTForms>=3.0.1",
|
||||
]
|
||||
@@ -22,4 +22,4 @@ dev-dependencies = [
|
||||
"pytest>=7.4.0",
|
||||
"pytest-flask>=1.2.0",
|
||||
"pytest-cov>=4.1.0",
|
||||
]
|
||||
]
|
||||
|
||||
404
routes/config.py
Normal file
404
routes/config.py
Normal file
@@ -0,0 +1,404 @@
|
||||
from flask import Blueprint, render_template, request, flash, redirect, url_for, jsonify
|
||||
from app_config import config_manager
|
||||
from models import db, AppConfig, CompetenceScaleValue, Competence
|
||||
from utils import handle_error
|
||||
import logging
|
||||
|
||||
bp = Blueprint('config', __name__, url_prefix='/config')
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
"""Page principale de configuration."""
|
||||
try:
|
||||
return render_template('config/index.html', app_config=config_manager)
|
||||
except Exception as e:
|
||||
return handle_error(e, "Erreur lors du chargement de la configuration")
|
||||
|
||||
@bp.route('/api/get')
|
||||
def api_get_config():
|
||||
"""API pour récupérer la configuration actuelle."""
|
||||
try:
|
||||
config_data = {
|
||||
'context': {
|
||||
'school_year': config_manager.get('context.school_year')
|
||||
},
|
||||
'evaluations': {
|
||||
'default_grading_system': config_manager.get('evaluations.default_grading_system'),
|
||||
'competence_scale': config_manager.get_competence_scale_values(),
|
||||
'competences': config_manager.get_competences_list()
|
||||
}
|
||||
}
|
||||
return jsonify(config_data)
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur API configuration: {e}")
|
||||
return jsonify({'error': 'Erreur lors de la récupération de la configuration'}), 500
|
||||
|
||||
@bp.route('/api/update', methods=['POST'])
|
||||
def api_update_config():
|
||||
"""API pour mettre à jour la configuration."""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
if not data:
|
||||
return jsonify({'error': 'Données manquantes'}), 400
|
||||
|
||||
# Mettre à jour la configuration
|
||||
for section, values in data.items():
|
||||
if isinstance(values, dict):
|
||||
for key, value in values.items():
|
||||
config_manager.set(f"{section}.{key}", value)
|
||||
else:
|
||||
config_manager.set(section, values)
|
||||
|
||||
# Sauvegarder
|
||||
if config_manager.save():
|
||||
flash('Configuration mise à jour avec succès', 'success')
|
||||
return jsonify({'success': True, 'message': 'Configuration sauvegardée'})
|
||||
else:
|
||||
return jsonify({'error': 'Erreur lors de la sauvegarde'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur mise à jour configuration: {e}")
|
||||
return jsonify({'error': 'Erreur lors de la mise à jour'}), 500
|
||||
|
||||
@bp.route('/competences')
|
||||
def competences():
|
||||
"""Page de configuration des compétences."""
|
||||
try:
|
||||
competences_list = config_manager.get_competences_list()
|
||||
competence_scale = config_manager.get_competence_scale_values()
|
||||
return render_template('config/competences.html',
|
||||
competences=competences_list,
|
||||
competence_scale=competence_scale)
|
||||
except Exception as e:
|
||||
return handle_error(e, "Erreur lors du chargement des compétences")
|
||||
|
||||
@bp.route('/competences/add', methods=['POST'])
|
||||
def add_competence():
|
||||
"""Ajouter une nouvelle compétence."""
|
||||
try:
|
||||
name = request.form.get('name')
|
||||
color = request.form.get('color', '#3b82f6')
|
||||
icon = request.form.get('icon', 'star')
|
||||
|
||||
if not name:
|
||||
flash('Le nom de la compétence est requis', 'error')
|
||||
return redirect(url_for('config.competences'))
|
||||
|
||||
# Validation de la couleur hexadécimale
|
||||
import re
|
||||
if not re.match(r'^#[0-9a-fA-F]{6}$', color):
|
||||
flash('Format de couleur invalide', 'error')
|
||||
return redirect(url_for('config.competences'))
|
||||
|
||||
# Vérifier si le nom existe déjà
|
||||
if Competence.query.filter_by(name=name).first():
|
||||
flash(f'Une compétence "{name}" existe déjà', 'error')
|
||||
return redirect(url_for('config.competences'))
|
||||
|
||||
if config_manager.add_competence(name, color, icon):
|
||||
flash(f'Compétence "{name}" ajoutée avec succès', 'success')
|
||||
else:
|
||||
flash('Erreur lors de la sauvegarde', 'error')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur ajout compétence: {e}")
|
||||
flash('Erreur lors de l\'ajout de la compétence', 'error')
|
||||
|
||||
return redirect(url_for('config.competences'))
|
||||
|
||||
@bp.route('/competences/update', methods=['POST'])
|
||||
def update_competence():
|
||||
"""Modifier une compétence existante."""
|
||||
try:
|
||||
edit_index = request.form.get('edit_index')
|
||||
name = request.form.get('name')
|
||||
color = request.form.get('color', '#3b82f6')
|
||||
icon = request.form.get('icon', 'star')
|
||||
|
||||
if not edit_index or not name:
|
||||
flash('Données manquantes pour la modification', 'error')
|
||||
return redirect(url_for('config.competences'))
|
||||
|
||||
index = int(edit_index)
|
||||
competences = Competence.query.order_by(Competence.order_index).all()
|
||||
|
||||
if 0 <= index < len(competences):
|
||||
# Validation de la couleur hexadécimale
|
||||
import re
|
||||
if not re.match(r'^#[0-9a-fA-F]{6}$', color):
|
||||
flash('Format de couleur invalide', 'error')
|
||||
return redirect(url_for('config.competences'))
|
||||
|
||||
competence = competences[index]
|
||||
|
||||
# Vérifier si le nom existe déjà (sauf pour l'élément en cours d'édition)
|
||||
existing = Competence.query.filter(Competence.name == name, Competence.id != competence.id).first()
|
||||
if existing:
|
||||
flash(f'Une compétence "{name}" existe déjà', 'error')
|
||||
return redirect(url_for('config.competences'))
|
||||
|
||||
old_name = competence.name
|
||||
|
||||
if config_manager.update_competence(competence.id, name, color, icon):
|
||||
flash(f'Compétence "{old_name}" modifiée en "{name}"', 'success')
|
||||
else:
|
||||
flash('Erreur lors de la sauvegarde', 'error')
|
||||
else:
|
||||
flash('Compétence introuvable', 'error')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur modification compétence: {e}")
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
|
||||
return redirect(url_for('config.competences'))
|
||||
|
||||
@bp.route('/competences/delete/<int:index>', methods=['POST'])
|
||||
def delete_competence(index):
|
||||
"""Supprimer une compétence."""
|
||||
try:
|
||||
competences = Competence.query.order_by(Competence.order_index).all()
|
||||
|
||||
if 0 <= index < len(competences):
|
||||
competence = competences[index]
|
||||
deleted_name = competence.name
|
||||
|
||||
if config_manager.delete_competence(competence.id):
|
||||
flash(f'Compétence "{deleted_name}" supprimée', 'success')
|
||||
else:
|
||||
flash('Erreur lors de la sauvegarde', 'error')
|
||||
else:
|
||||
flash('Compétence introuvable', 'error')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur suppression compétence: {e}")
|
||||
flash('Erreur lors de la suppression', 'error')
|
||||
|
||||
return redirect(url_for('config.competences'))
|
||||
|
||||
@bp.route('/scale')
|
||||
def scale():
|
||||
"""Page de configuration de l'échelle des compétences."""
|
||||
try:
|
||||
competence_scale = config_manager.get_competence_scale_values()
|
||||
return render_template('config/scale.html', competence_scale=competence_scale)
|
||||
except Exception as e:
|
||||
return handle_error(e, "Erreur lors du chargement de l'échelle")
|
||||
|
||||
@bp.route('/scale/update', methods=['POST'])
|
||||
def update_scale():
|
||||
"""Mettre à jour l'échelle des compétences."""
|
||||
try:
|
||||
scale_data = {}
|
||||
|
||||
# Récupérer tous les champs du formulaire
|
||||
for key in request.form.keys():
|
||||
if key.startswith('scale_'):
|
||||
parts = key.split('_', 2) # scale_VALUE_FIELD
|
||||
if len(parts) == 3:
|
||||
value, field = parts[1], parts[2]
|
||||
|
||||
if value not in scale_data:
|
||||
scale_data[value] = {}
|
||||
|
||||
form_value = request.form[key]
|
||||
if field == 'included_in_total':
|
||||
form_value = form_value.lower() == 'true'
|
||||
|
||||
scale_data[value][field] = form_value
|
||||
|
||||
# Validation des données
|
||||
import re
|
||||
for value, config in scale_data.items():
|
||||
# Vérifier que tous les champs requis sont présents
|
||||
if not all(field in config for field in ['label', 'color', 'included_in_total']):
|
||||
flash(f'Données manquantes pour la valeur {value}', 'error')
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
# Valider le format de couleur
|
||||
if not re.match(r'^#[0-9a-fA-F]{6}$', config['color']):
|
||||
flash(f'Format de couleur invalide pour la valeur {value}', 'error')
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
# Valider que le libellé n'est pas vide
|
||||
if not config['label'].strip():
|
||||
flash(f'Le libellé ne peut pas être vide pour la valeur {value}', 'error')
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
# Mettre à jour chaque valeur
|
||||
success_count = 0
|
||||
for value, config in scale_data.items():
|
||||
if config_manager.update_scale_value(
|
||||
value,
|
||||
config['label'],
|
||||
config['color'],
|
||||
config['included_in_total']
|
||||
):
|
||||
success_count += 1
|
||||
|
||||
if success_count == len(scale_data):
|
||||
flash('Échelle des compétences mise à jour avec succès', 'success')
|
||||
else:
|
||||
flash(f'Mise à jour partielle : {success_count}/{len(scale_data)} valeurs mises à jour', 'warning')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur mise à jour échelle: {e}")
|
||||
flash('Erreur lors de la mise à jour', 'error')
|
||||
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
@bp.route('/scale/add', methods=['POST'])
|
||||
def add_scale_value():
|
||||
"""Ajouter une nouvelle valeur à l'échelle."""
|
||||
try:
|
||||
value = request.form.get('value')
|
||||
label = request.form.get('label')
|
||||
color = request.form.get('color', '#6b7280')
|
||||
included_in_total = request.form.get('included_in_total', 'true').lower() == 'true'
|
||||
|
||||
if not value or not label:
|
||||
flash('La valeur et le libellé sont requis', 'error')
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
# Validation de la couleur hexadécimale
|
||||
import re
|
||||
if not re.match(r'^#[0-9a-fA-F]{6}$', color):
|
||||
flash('Format de couleur invalide', 'error')
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
# Vérifier si la valeur existe déjà
|
||||
if CompetenceScaleValue.query.filter_by(value=value).first():
|
||||
flash(f'La valeur "{value}" existe déjà dans l\'échelle', 'error')
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
if config_manager.add_scale_value(value, label.strip(), color, included_in_total):
|
||||
flash(f'Valeur "{value}" ajoutée à l\'échelle avec succès', 'success')
|
||||
else:
|
||||
flash('Erreur lors de la sauvegarde', 'error')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur ajout valeur échelle: {e}")
|
||||
flash('Erreur lors de l\'ajout de la valeur', 'error')
|
||||
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
@bp.route('/scale/delete/<path:value>', methods=['POST'])
|
||||
def delete_scale_value(value):
|
||||
"""Supprimer une valeur de l'échelle."""
|
||||
try:
|
||||
# Vérifier si la valeur existe
|
||||
scale_value = CompetenceScaleValue.query.filter_by(value=value).first()
|
||||
if not scale_value:
|
||||
flash(f'La valeur "{value}" n\'existe pas dans l\'échelle', 'error')
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
# Empêcher la suppression des valeurs de base (0, 1, 2, 3, .)
|
||||
base_values = ['0', '1', '2', '3', '.']
|
||||
if value in base_values:
|
||||
flash(f'La valeur "{value}" est une valeur de base et ne peut pas être supprimée', 'error')
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
label = scale_value.label
|
||||
|
||||
if config_manager.delete_scale_value(value):
|
||||
flash(f'Valeur "{value}" ({label}) supprimée de l\'échelle', 'success')
|
||||
else:
|
||||
flash('Erreur lors de la sauvegarde', 'error')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur suppression valeur échelle: {e}")
|
||||
flash('Erreur lors de la suppression', 'error')
|
||||
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
@bp.route('/scale/reset', methods=['POST'])
|
||||
def reset_scale():
|
||||
"""Réinitialiser l'échelle aux valeurs par défaut."""
|
||||
try:
|
||||
# Supprimer toutes les valeurs existantes
|
||||
CompetenceScaleValue.query.delete()
|
||||
|
||||
# Réajouter les valeurs par défaut
|
||||
default_scale = config_manager.default_config['evaluations']['competence_scale']['values']
|
||||
for value, config in default_scale.items():
|
||||
scale_value = CompetenceScaleValue(
|
||||
value=value,
|
||||
label=config['label'],
|
||||
color=config['color'],
|
||||
included_in_total=config['included_in_total']
|
||||
)
|
||||
db.session.add(scale_value)
|
||||
|
||||
db.session.commit()
|
||||
flash('Échelle réinitialisée aux valeurs par défaut', 'success')
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logging.error(f"Erreur réinitialisation échelle: {e}")
|
||||
flash('Erreur lors de la réinitialisation', 'error')
|
||||
|
||||
return redirect(url_for('config.scale'))
|
||||
|
||||
@bp.route('/general')
|
||||
def general():
|
||||
"""Page de configuration générale."""
|
||||
try:
|
||||
return render_template('config/general.html',
|
||||
app_config=config_manager,
|
||||
school_year=config_manager.get_school_year(),
|
||||
default_grading_system=config_manager.get_default_grading_system())
|
||||
except Exception as e:
|
||||
return handle_error(e, "Erreur lors du chargement de la configuration générale")
|
||||
|
||||
@bp.route('/general/update', methods=['POST'])
|
||||
def update_general():
|
||||
"""Mettre à jour la configuration générale."""
|
||||
try:
|
||||
school_year = request.form.get('school_year')
|
||||
default_grading_system = request.form.get('default_grading_system')
|
||||
|
||||
if not school_year:
|
||||
flash('L\'année scolaire est requise', 'error')
|
||||
return redirect(url_for('config.general'))
|
||||
|
||||
# Validation de l'année scolaire (format YYYY-YYYY)
|
||||
import re
|
||||
if not re.match(r'^\d{4}-\d{4}$', school_year):
|
||||
flash('Format d\'année scolaire invalide (attendu: YYYY-YYYY)', 'error')
|
||||
return redirect(url_for('config.general'))
|
||||
|
||||
# Mettre à jour la configuration
|
||||
config_manager.set('context.school_year', school_year)
|
||||
config_manager.set('evaluations.default_grading_system', default_grading_system)
|
||||
|
||||
if config_manager.save():
|
||||
flash('Configuration générale mise à jour avec succès', 'success')
|
||||
else:
|
||||
flash('Erreur lors de la sauvegarde', 'error')
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur mise à jour config générale: {e}")
|
||||
flash('Erreur lors de la mise à jour', 'error')
|
||||
|
||||
return redirect(url_for('config.general'))
|
||||
|
||||
@bp.route('/reset', methods=['POST'])
|
||||
def reset_config():
|
||||
"""Réinitialise la configuration aux valeurs par défaut."""
|
||||
try:
|
||||
# Supprimer toute la configuration existante
|
||||
AppConfig.query.delete()
|
||||
CompetenceScaleValue.query.delete()
|
||||
Competence.query.delete()
|
||||
|
||||
# Réinitialiser avec les valeurs par défaut
|
||||
config_manager.initialize_default_config()
|
||||
|
||||
flash('Configuration réinitialisée aux valeurs par défaut', 'success')
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logging.error(f"Erreur réinitialisation: {e}")
|
||||
flash('Erreur lors de la réinitialisation', 'error')
|
||||
|
||||
return redirect(url_for('config.index'))
|
||||
@@ -36,6 +36,12 @@
|
||||
<a href="{{ url_for('classes') }}" class="hover:text-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600 rounded-sm px-2 py-1" role="menuitem">Classes</a>
|
||||
<a href="{{ url_for('students') }}" class="hover:text-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600 rounded-sm px-2 py-1" role="menuitem">Élèves</a>
|
||||
<a href="{{ url_for('assessments.list') }}" class="hover:text-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600 rounded-sm px-2 py-1" role="menuitem">Évaluations</a>
|
||||
<a href="{{ url_for('config.index') }}" class="hover:text-blue-200 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-300 focus:ring-offset-2 focus:ring-offset-blue-600 rounded-sm px-2 py-1" role="menuitem">
|
||||
<svg class="w-4 h-4 inline-block mr-1" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Configuration
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
297
templates/config/competences.html
Normal file
297
templates/config/competences.html
Normal file
@@ -0,0 +1,297 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Configuration des compétences - Notytex{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-4">
|
||||
<li>
|
||||
<a href="{{ url_for('config.index') }}" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="sr-only">Configuration</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="ml-4 text-sm font-medium text-gray-500">Compétences</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- En-tête -->
|
||||
<div class="bg-gradient-to-r from-purple-600 to-pink-600 text-white rounded-xl p-8 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-2">⭐ Gestion des compétences</h1>
|
||||
<p class="text-xl opacity-90">Configurez les compétences avec leurs couleurs et icônes</p>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<div class="w-20 h-20 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<svg class="w-10 h-10" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Liste des compétences existantes -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="bg-white rounded-lg shadow-md">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">📋 Compétences configurées</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ competences|length }} compétence(s) définie(s)</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
{% if competences %}
|
||||
<div class="space-y-4">
|
||||
{% for competence in competences %}
|
||||
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors">
|
||||
<div class="flex items-center space-x-3">
|
||||
<!-- Couleur -->
|
||||
<div
|
||||
class="w-6 h-6 rounded-full border-2 border-white shadow-sm"
|
||||
style="background-color: {{ competence.color }}"
|
||||
></div>
|
||||
|
||||
<!-- Icône -->
|
||||
<div class="w-8 h-8 flex items-center justify-center">
|
||||
{% if competence.icon == 'calculator' %}
|
||||
<svg class="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V4a2 2 0 00-2-2H6zM6 4h8v2H6V4zm0 4h8v2H6V8zm0 4h8v2H6v-2z"/>
|
||||
</svg>
|
||||
{% elif competence.icon == 'lightbulb' %}
|
||||
<svg class="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M11 3a1 1 0 10-2 0v1a1 1 0 102 0V3zM15.657 5.757a1 1 0 00-1.414-1.414l-.707.707a1 1 0 001.414 1.414l.707-.707zM18 10a1 1 0 01-1 1h-1a1 1 0 110-2h1a1 1 0 011 1zM5.05 6.464A1 1 0 106.464 5.05l-.707-.707a1 1 0 00-1.414 1.414l.707.707zM5 10a1 1 0 01-1 1H3a1 1 0 110-2h1a1 1 0 011 1zM8 16v-1h4v1a2 2 0 11-4 0zM12 14c.015-.34.208-.646.477-.859a4 4 0 10-4.954 0c.27.213.462.519.477.859h4z"/>
|
||||
</svg>
|
||||
{% elif competence.icon == 'chat-bubble-left-right' %}
|
||||
<svg class="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% elif competence.icon == 'cube' %}
|
||||
<svg class="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M11 17a1 1 0 001.447.894l4-2A1 1 0 0017 15V9.236a1 1 0 00-1.447-.894l-4 2a1 1 0 00-.553.894V17zM15.211 6.276a1 1 0 000-1.788l-4.764-2.382a1 1 0 00-.894 0L4.789 4.488a1 1 0 000 1.788l4.764 2.382a1 1 0 00.894 0l4.764-2.382zM4.447 8.342A1 1 0 003 9.236V15a1 1 0 00.553.894l4 2A1 1 0 009 17v-5.764a1 1 0 00-.553-.894l-4-2z"/>
|
||||
</svg>
|
||||
{% elif competence.icon == 'chart-bar' %}
|
||||
<svg class="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"/>
|
||||
</svg>
|
||||
{% elif competence.icon == 'magnifying-glass' %}
|
||||
<svg class="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Nom -->
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900">{{ competence.name }}</h4>
|
||||
<p class="text-xs text-gray-500">Icône: {{ competence.icon }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<button
|
||||
onclick="editCompetence({{ loop.index0 }}, '{{ competence.name }}', '{{ competence.color }}', '{{ competence.icon }}')"
|
||||
class="p-2 text-gray-400 hover:text-blue-600 transition-colors"
|
||||
title="Modifier"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<form method="POST" action="{{ url_for('config.delete_competence', index=loop.index0) }}" class="inline">
|
||||
<button
|
||||
type="submit"
|
||||
onclick="return confirm('Supprimer la compétence « {{ competence.name }} » ?')"
|
||||
class="p-2 text-gray-400 hover:text-red-600 transition-colors"
|
||||
title="Supprimer"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<svg class="w-12 h-12 text-gray-400 mx-auto mb-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">Aucune compétence définie</h3>
|
||||
<p class="text-gray-500">Commencez par ajouter votre première compétence</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'ajout/modification -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="bg-white rounded-lg shadow-md sticky top-8">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900" id="form-title">➕ Ajouter une compétence</h3>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('config.add_competence') }}" class="p-6 space-y-4" id="competence-form">
|
||||
<input type="hidden" id="edit-index" name="edit_index" value="">
|
||||
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nom de la compétence
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Ex: Calculer, Raisonner..."
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="color" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Couleur
|
||||
</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input
|
||||
type="color"
|
||||
id="color"
|
||||
name="color"
|
||||
value="#3b82f6"
|
||||
class="w-12 h-10 border border-gray-300 rounded-md cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="color-text"
|
||||
placeholder="#3b82f6"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
pattern="#[0-9a-fA-F]{6}"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="icon" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Icône
|
||||
</label>
|
||||
<select
|
||||
id="icon"
|
||||
name="icon"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||
>
|
||||
<option value="calculator">🧮 Calculer</option>
|
||||
<option value="lightbulb">💡 Raisonner</option>
|
||||
<option value="chat-bubble-left-right">💬 Communiquer</option>
|
||||
<option value="cube">📦 Modéliser</option>
|
||||
<option value="chart-bar">📊 Représenter</option>
|
||||
<option value="magnifying-glass">🔍 Chercher</option>
|
||||
<option value="star">⭐ Autre</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onclick="resetForm()"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
|
||||
id="cancel-btn"
|
||||
style="display: none;"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="flex-1 px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
|
||||
id="submit-btn"
|
||||
>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-between items-center">
|
||||
<a
|
||||
href="{{ url_for('config.index') }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.707 14.707a1 1 0 01-1.414 0L2.586 11H13a1 1 0 110 2H2.586l3.707 3.707a1 1 0 01-1.414 1.414l-5-5a1 1 0 010-1.414l5-5a1 1 0 011.414 1.414L2.586 9H13a1 1 0 110 2H7.707z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Retour à la configuration
|
||||
</a>
|
||||
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ competences|length }} compétence(s) configurée(s)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Synchroniser les champs de couleur
|
||||
document.getElementById('color').addEventListener('input', function() {
|
||||
document.getElementById('color-text').value = this.value;
|
||||
});
|
||||
|
||||
document.getElementById('color-text').addEventListener('input', function() {
|
||||
if (this.value.match(/^#[0-9a-fA-F]{6}$/)) {
|
||||
document.getElementById('color').value = this.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Fonction pour éditer une compétence
|
||||
function editCompetence(index, name, color, icon) {
|
||||
document.getElementById('form-title').textContent = '✏️ Modifier la compétence';
|
||||
document.getElementById('edit-index').value = index;
|
||||
document.getElementById('name').value = name;
|
||||
document.getElementById('color').value = color;
|
||||
document.getElementById('color-text').value = color;
|
||||
document.getElementById('icon').value = icon;
|
||||
document.getElementById('submit-btn').textContent = 'Modifier';
|
||||
document.getElementById('cancel-btn').style.display = 'block';
|
||||
|
||||
// Changer l'action du formulaire
|
||||
document.getElementById('competence-form').action = '{{ url_for("config.update_competence") }}';
|
||||
}
|
||||
|
||||
// Fonction pour réinitialiser le formulaire
|
||||
function resetForm() {
|
||||
document.getElementById('form-title').textContent = '➕ Ajouter une compétence';
|
||||
document.getElementById('edit-index').value = '';
|
||||
document.getElementById('name').value = '';
|
||||
document.getElementById('color').value = '#3b82f6';
|
||||
document.getElementById('color-text').value = '#3b82f6';
|
||||
document.getElementById('icon').value = 'calculator';
|
||||
document.getElementById('submit-btn').textContent = 'Ajouter';
|
||||
document.getElementById('cancel-btn').style.display = 'none';
|
||||
|
||||
// Remettre l'action d'origine
|
||||
document.getElementById('competence-form').action = '{{ url_for("config.add_competence") }}';
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
169
templates/config/general.html
Normal file
169
templates/config/general.html
Normal file
@@ -0,0 +1,169 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Configuration générale - Notytex{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-4">
|
||||
<li>
|
||||
<a href="{{ url_for('config.index') }}" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="sr-only">Configuration</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="ml-4 text-sm font-medium text-gray-500">Configuration générale</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- En-tête -->
|
||||
<div class="bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl p-8 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-2">🏫 Configuration générale</h1>
|
||||
<p class="text-xl opacity-90">Paramètres système et contexte de l'application</p>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<div class="w-20 h-20 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<svg class="w-10 h-10" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire de configuration -->
|
||||
<div class="bg-white rounded-lg shadow-md">
|
||||
<form method="POST" action="{{ url_for('config.update_general') }}" class="p-8 space-y-6">
|
||||
|
||||
<!-- Section Contexte -->
|
||||
<div class="border-b border-gray-200 pb-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">📅 Contexte scolaire</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="school_year" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Année scolaire
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="school_year"
|
||||
name="school_year"
|
||||
value="{{ school_year }}"
|
||||
pattern="^\d{4}-\d{4}$"
|
||||
placeholder="2024-2025"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
>
|
||||
<p class="mt-1 text-xs text-gray-500">Format attendu : YYYY-YYYY</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Section Évaluations -->
|
||||
<div class="pb-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">🎯 Configuration des évaluations</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="default_grading_system" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Système de notation par défaut
|
||||
</label>
|
||||
<select
|
||||
id="default_grading_system"
|
||||
name="default_grading_system"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="points" {% if default_grading_system == 'points' %}selected{% endif %}>
|
||||
Points (notation classique)
|
||||
</option>
|
||||
<option value="competences" {% if default_grading_system == 'competences' %}selected{% endif %}>
|
||||
Compétences (échelle 0-3)
|
||||
</option>
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Système utilisé par défaut lors de la création d'exercices
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
|
||||
<a
|
||||
href="{{ url_for('config.index') }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.707 14.707a1 1 0 01-1.414 0L2.586 11H13a1 1 0 110 2H2.586l3.707 3.707a1 1 0 01-1.414 1.414l-5-5a1 1 0 010-1.414l5-5a1 1 0 011.414 1.414L2.586 9H13a1 1 0 110 2H7.707z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Retour
|
||||
</a>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick="resetForm()"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Réinitialiser
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center px-6 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Informations système -->
|
||||
<div class="bg-gray-50 rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">ℹ️ Informations système</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Version :</span>
|
||||
<span class="text-gray-900">Notytex 1.0</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Type de base :</span>
|
||||
<span class="text-gray-900">SQLite</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-medium text-gray-600">Configuration :</span>
|
||||
<span class="text-gray-900">Base de données SQLite</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function resetForm() {
|
||||
if (confirm('Êtes-vous sûr de vouloir réinitialiser le formulaire ?')) {
|
||||
document.getElementById('school_year').value = '2025-2026';
|
||||
document.getElementById('default_grading_system').value = 'competences';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
157
templates/config/index.html
Normal file
157
templates/config/index.html
Normal file
@@ -0,0 +1,157 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Configuration - Notytex{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- En-tête -->
|
||||
<div class="bg-gradient-to-r from-purple-600 to-blue-600 text-white rounded-xl p-8 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold mb-2">⚙️ Configuration</h1>
|
||||
<p class="text-xl opacity-90">Personnalisez votre application Notytex</p>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<div class="w-24 h-24 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sections de configuration -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Configuration générale -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Configuration générale</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-4">Base de données, année scolaire et paramètres système</p>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-500">Année scolaire :</span>
|
||||
<span class="text-sm font-medium text-gray-900" id="school-year">{{ app_config.get_school_year() }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-500">Base de données :</span>
|
||||
<span class="text-sm font-medium text-gray-900">SQLite</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('config.general') }}" class="mt-4 block w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors text-center">
|
||||
Modifier
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Configuration des compétences -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 text-purple-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Compétences</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-4">Gérez la liste des compétences et leurs couleurs</p>
|
||||
<div class="space-y-2 mb-4">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
|
||||
<span class="text-sm">Calculer</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-3 h-3 rounded-full bg-purple-500"></div>
|
||||
<span class="text-sm">Raisonner</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">+4 autres compétences</div>
|
||||
</div>
|
||||
<a href="{{ url_for('config.competences') }}" class="block w-full bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 transition-colors text-center">
|
||||
Gérer les compétences
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Configuration de l'échelle -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mr-4">
|
||||
<svg class="w-6 h-6 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2a2 2 0 002-2V5a1 1 0 100-2H3zm6 2a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1zm1 5a1 1 0 100 2h6a1 1 0 100-2h-6zm0 4a1 1 0 100 2h6a1 1 0 100-2h-6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900">Échelle des compétences</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 mb-4">Personnalisez les valeurs et libellés de l'échelle</p>
|
||||
<div class="space-y-2 mb-4">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm">0 - Non acquis</span>
|
||||
<div class="w-3 h-3 rounded bg-red-500"></div>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm">3 - Expert</span>
|
||||
<div class="w-3 h-3 rounded bg-blue-500"></div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">5 valeurs configurées</div>
|
||||
</div>
|
||||
<a href="{{ url_for('config.scale') }}" class="block w-full bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 transition-colors text-center">
|
||||
Configurer l'échelle
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions globales -->
|
||||
<div class="bg-white rounded-lg shadow-md p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Actions système</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<button onclick="exportConfig()" class="bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition-colors">
|
||||
📤 Exporter la configuration
|
||||
</button>
|
||||
<button onclick="importConfig()" class="bg-yellow-600 text-white py-2 px-4 rounded-md hover:bg-yellow-700 transition-colors">
|
||||
📥 Importer une configuration
|
||||
</button>
|
||||
<button onclick="resetConfig()" class="bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700 transition-colors">
|
||||
🔄 Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modals et JavaScript -->
|
||||
<script>
|
||||
function editGeneralConfig() {
|
||||
// TODO: Implémenter modal d'édition
|
||||
alert('Fonctionnalité en cours de développement');
|
||||
}
|
||||
|
||||
function exportConfig() {
|
||||
// TODO: Implémenter export
|
||||
alert('Fonctionnalité en cours de développement');
|
||||
}
|
||||
|
||||
function importConfig() {
|
||||
// TODO: Implémenter import
|
||||
alert('Fonctionnalité en cours de développement');
|
||||
}
|
||||
|
||||
function resetConfig() {
|
||||
if (confirm('Êtes-vous sûr de vouloir réinitialiser toute la configuration ?')) {
|
||||
fetch('{{ url_for("config.reset_config") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
464
templates/config/scale.html
Normal file
464
templates/config/scale.html
Normal file
@@ -0,0 +1,464 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Configuration de l'échelle - Notytex{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="flex" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center space-x-4">
|
||||
<li>
|
||||
<a href="{{ url_for('config.index') }}" class="text-gray-400 hover:text-gray-500">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="sr-only">Configuration</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="ml-4 text-sm font-medium text-gray-500">Échelle des compétences</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- En-tête -->
|
||||
<div class="bg-gradient-to-r from-green-600 to-teal-600 text-white rounded-xl p-8 shadow-lg">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-2">📏 Échelle des compétences</h1>
|
||||
<p class="text-xl opacity-90">Configurez l'échelle numérique et les valeurs spéciales</p>
|
||||
</div>
|
||||
<div class="hidden md:block">
|
||||
<div class="w-20 h-20 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<svg class="w-10 h-10" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2a2 2 0 002-2V5a1 1 0 100-2H3zm6 2a1 1 0 011-1h6a1 1 0 110 2h-6a1 1 0 01-1-1zm1 5a1 1 0 100 2h6a1 1 0 100-2h-6zm0 4a1 1 0 100 2h6a1 1 0 100-2h-6z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Information sur l'échelle actuelle -->
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<div class="flex items-start">
|
||||
<svg class="w-6 h-6 text-blue-400 mt-0.5 mr-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-blue-800">Organisation de l'échelle</h3>
|
||||
<ul class="text-sm text-blue-700 mt-2 space-y-1">
|
||||
<li><strong>Échelle numérique :</strong> Valeurs de 0 à N avec progression continue</li>
|
||||
<li><strong>Valeurs spéciales :</strong> "." pour non évalué et autres valeurs personnalisées</li>
|
||||
<li><strong>Réglage de l'étendue :</strong> Vous pouvez étendre l'échelle jusqu'à 10</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire de configuration -->
|
||||
<div class="bg-white rounded-lg shadow-md">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">🎚️ Configuration de l'échelle</h3>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ competence_scale|length }} valeur(s) configurée(s)</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('config.update_scale') }}" class="p-6">
|
||||
|
||||
<!-- Échelle numérique -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-lg font-medium text-gray-900">🔢 Échelle numérique</h4>
|
||||
|
||||
<!-- Contrôle d'étendue -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<label for="scale_max" class="text-sm font-medium text-gray-700">
|
||||
Étendue :
|
||||
</label>
|
||||
<select id="scale_max" onchange="updateScaleRange()" class="px-3 py-1 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500">
|
||||
{% set current_max = 3 %}
|
||||
{% for value in competence_scale.keys() %}
|
||||
{% if value in [0, 1, 2, 3, 4, 5] %}
|
||||
{% if value > current_max %}
|
||||
{% set current_max = value %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for i in range(2, 6) %}
|
||||
<option value="{{ i }}" {% if i == current_max %}selected{% endif %}>0 à {{ i }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-4">
|
||||
<p class="text-sm text-gray-600">
|
||||
Configurez chaque niveau de l'échelle numérique. L'étendue actuelle va de 0 à {{ current_max }}.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div id="numeric-scale" class="space-y-4">
|
||||
{% for value, config in competence_scale.items() %}
|
||||
{% if value in [0, 1, 2, 3, 4, 5] %}
|
||||
<div class="border border-gray-200 rounded-lg p-4" data-numeric-value="{{ value }}">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full border-2 border-white shadow-sm flex items-center justify-center text-white font-bold text-sm"
|
||||
style="background-color: {{ config.color }}"
|
||||
>
|
||||
{{ value }}
|
||||
</div>
|
||||
<h5 class="font-medium text-gray-900">
|
||||
{% if value == '0' %}
|
||||
Niveau {{ value }} (minimum)
|
||||
{% else %}
|
||||
Niveau {{ value }}
|
||||
{% endif %}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {% if config.included_in_total %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
|
||||
{% if config.included_in_total %}✅ Inclus{% else %}❌ Exclu{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="scale_{{ value }}_label" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Libellé
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="scale_{{ value }}_label"
|
||||
name="scale_{{ value }}_label"
|
||||
value="{{ config.label }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="scale_{{ value }}_color" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Couleur
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="color"
|
||||
id="scale_{{ value }}_color"
|
||||
name="scale_{{ value }}_color"
|
||||
value="{{ config.color }}"
|
||||
class="w-10 h-10 border border-gray-300 rounded-md cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="scale_{{ value }}_color_text"
|
||||
value="{{ config.color }}"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
pattern="#[0-9a-fA-F]{6}"
|
||||
onchange="syncColor('{{ value }}')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="scale_{{ value }}_included_in_total" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Inclusion dans le total
|
||||
</label>
|
||||
<select
|
||||
id="scale_{{ value }}_included_in_total"
|
||||
name="scale_{{ value }}_included_in_total"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
>
|
||||
<option value="true" {% if config.included_in_total %}selected{% endif %}>✅ Inclus dans le calcul</option>
|
||||
<option value="false" {% if not config.included_in_total %}selected{% endif %}>❌ Exclu du calcul</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Valeurs spéciales -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h4 class="text-lg font-medium text-gray-900">🔤 Valeurs spéciales</h4>
|
||||
<button
|
||||
type="button"
|
||||
onclick="showAddSpecialValue()"
|
||||
class="inline-flex items-center px-3 py-1 text-sm border border-gray-300 rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{% for value, config in competence_scale.items() %}
|
||||
{% if value not in [0, 1, 2, 3, 4, 5] %}
|
||||
<div class="border border-gray-200 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div
|
||||
class="w-8 h-8 rounded-full border-2 border-white shadow-sm flex items-center justify-center text-white font-bold text-sm"
|
||||
style="background-color: {{ config.color }}"
|
||||
>
|
||||
{% if value == '.' %}
|
||||
•
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<h5 class="font-medium text-gray-900">
|
||||
{% if value == '.' %}
|
||||
Valeur "." (Non évalué)
|
||||
{% else %}
|
||||
Valeur "{{ value }}"
|
||||
{% endif %}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {% if config.included_in_total %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
|
||||
{% if config.included_in_total %}✅ Inclus{% else %}❌ Exclu{% endif %}
|
||||
</span>
|
||||
|
||||
{% if value != '.' %}
|
||||
<button
|
||||
type="button"
|
||||
onclick="deleteSpecialValue('{{ value }}', '{{ config.label }}')"
|
||||
class="p-1 text-gray-400 hover:text-red-600 transition-colors"
|
||||
title="Supprimer cette valeur"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label for="scale_{{ value }}_label" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Libellé
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="scale_{{ value }}_label"
|
||||
name="scale_{{ value }}_label"
|
||||
value="{{ config.label }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="scale_{{ value }}_color" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Couleur
|
||||
</label>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input
|
||||
type="color"
|
||||
id="scale_{{ value }}_color"
|
||||
name="scale_{{ value }}_color"
|
||||
value="{{ config.color }}"
|
||||
class="w-10 h-10 border border-gray-300 rounded-md cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
id="scale_{{ value }}_color_text"
|
||||
value="{{ config.color }}"
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
pattern="#[0-9a-fA-F]{6}"
|
||||
onchange="syncColor('{{ value }}')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="scale_{{ value }}_included_in_total" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Inclusion dans le total
|
||||
</label>
|
||||
<select
|
||||
id="scale_{{ value }}_included_in_total"
|
||||
name="scale_{{ value }}_included_in_total"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500"
|
||||
>
|
||||
<option value="true" {% if config.included_in_total %}selected{% endif %}>✅ Inclus dans le calcul</option>
|
||||
<option value="false" {% if not config.included_in_total %}selected{% endif %}>❌ Exclu du calcul</option>
|
||||
</select>
|
||||
{% if value == '.' %}
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Généralement inclus car compte dans le total possible
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-between items-center pt-8 border-t border-gray-200 mt-8">
|
||||
<a
|
||||
href="{{ url_for('config.index') }}"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M7.707 14.707a1 1 0 01-1.414 0L2.586 11H13a1 1 0 110 2H2.586l3.707 3.707a1 1 0 01-1.414 1.414l-5-5a1 1 0 010-1.414l5-5a1 1 0 011.414 1.414L2.586 9H13a1 1 0 110 2H7.707z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Retour
|
||||
</a>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onclick="resetToDefaults()"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Valeurs par défaut
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center px-6 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
Enregistrer l'échelle
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Synchroniser les champs de couleur
|
||||
function syncColor(value) {
|
||||
const colorPicker = document.getElementById(`scale_${value}_color`);
|
||||
const colorText = document.getElementById(`scale_${value}_color_text`);
|
||||
|
||||
if (colorText.value.match(/^#[0-9a-fA-F]{6}$/)) {
|
||||
colorPicker.value = colorText.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Synchroniser tous les color pickers avec leurs champs texte
|
||||
{% for value, config in competence_scale.items() %}
|
||||
document.getElementById('scale_{{ value }}_color').addEventListener('input', function() {
|
||||
document.getElementById('scale_{{ value }}_color_text').value = this.value;
|
||||
});
|
||||
{% endfor %}
|
||||
|
||||
// Mettre à jour l'étendue de l'échelle
|
||||
function updateScaleRange() {
|
||||
const newMax = parseInt(document.getElementById('scale_max').value);
|
||||
const currentElements = document.querySelectorAll('#numeric-scale [data-numeric-value]');
|
||||
|
||||
// Masquer les éléments au-dessus du nouveau maximum
|
||||
currentElements.forEach(element => {
|
||||
const value = parseInt(element.getAttribute('data-numeric-value'));
|
||||
if (value > newMax) {
|
||||
element.style.display = 'none';
|
||||
} else {
|
||||
element.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Cette fonction pourrait être étendue pour créer dynamiquement de nouveaux éléments
|
||||
if (confirm(`Changer l'étendue à 0-${newMax} ? Cela nécessite une sauvegarde pour créer les nouveaux niveaux.`)) {
|
||||
// Vous pourriez ajouter ici une requête AJAX pour créer les nouveaux niveaux
|
||||
alert('Fonctionnalité à implémenter : création dynamique des nouveaux niveaux');
|
||||
}
|
||||
}
|
||||
|
||||
// Réinitialiser aux valeurs par défaut
|
||||
function resetToDefaults() {
|
||||
if (confirm('Êtes-vous sûr de vouloir restaurer l\'échelle par défaut ? Toutes vos modifications seront perdues.')) {
|
||||
fetch('{{ url_for("config.reset_scale") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter une valeur spéciale
|
||||
function showAddSpecialValue() {
|
||||
const value = prompt('Entrez la valeur spéciale (ex: NA, X, etc.) :');
|
||||
if (value && value.trim()) {
|
||||
const label = prompt('Entrez le libellé pour cette valeur :');
|
||||
if (label && label.trim()) {
|
||||
const color = prompt('Entrez la couleur (hex, ex: #ff0000) :', '#6b7280');
|
||||
const included = confirm('Cette valeur doit-elle être incluse dans le calcul du total ?');
|
||||
|
||||
// Créer un formulaire temporaire pour envoyer les données
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '{{ url_for("config.add_scale_value") }}';
|
||||
|
||||
const valueInput = document.createElement('input');
|
||||
valueInput.type = 'hidden';
|
||||
valueInput.name = 'value';
|
||||
valueInput.value = value.trim();
|
||||
|
||||
const labelInput = document.createElement('input');
|
||||
labelInput.type = 'hidden';
|
||||
labelInput.name = 'label';
|
||||
labelInput.value = label.trim();
|
||||
|
||||
const colorInput = document.createElement('input');
|
||||
colorInput.type = 'hidden';
|
||||
colorInput.name = 'color';
|
||||
colorInput.value = color || '#6b7280';
|
||||
|
||||
const includedInput = document.createElement('input');
|
||||
includedInput.type = 'hidden';
|
||||
includedInput.name = 'included_in_total';
|
||||
includedInput.value = included ? 'true' : 'false';
|
||||
|
||||
form.appendChild(valueInput);
|
||||
form.appendChild(labelInput);
|
||||
form.appendChild(colorInput);
|
||||
form.appendChild(includedInput);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Supprimer une valeur spéciale
|
||||
function deleteSpecialValue(value, label) {
|
||||
if (confirm(`Êtes-vous sûr de vouloir supprimer la valeur "${value}" (${label}) ?`)) {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = `{{ url_for("config.delete_scale_value", value="PLACEHOLDER") }}`.replace('PLACEHOLDER', encodeURIComponent(value));
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
8
utils.py
8
utils.py
@@ -91,4 +91,10 @@ def log_user_action(action, details=None):
|
||||
|
||||
class ValidationError(Exception):
|
||||
"""Exception personnalisée pour les erreurs de validation"""
|
||||
pass
|
||||
pass
|
||||
|
||||
def handle_error(exception, default_message="Une erreur s'est produite"):
|
||||
"""Gestionnaire d'erreur générique"""
|
||||
current_app.logger.error(f"Erreur: {exception}")
|
||||
flash(default_message, 'error')
|
||||
return render_template('error.html', error=default_message), 500
|
||||
Reference in New Issue
Block a user