diff --git a/app.py b/app.py index bf5f844..8423bbe 100644 --- a/app.py +++ b/app.py @@ -12,6 +12,7 @@ from core.logging import setup_logging from routes.assessments import bp as assessments_bp from routes.grading import bp as grading_bp from routes.config import bp as config_bp +from routes.domains import bp as domains_bp def create_app(config_name=None): if config_name is None: @@ -41,6 +42,7 @@ def create_app(config_name=None): app.register_blueprint(assessments_bp) app.register_blueprint(grading_bp) app.register_blueprint(config_bp) + app.register_blueprint(domains_bp) # Register CLI commands app.cli.add_command(init_db) diff --git a/app_config.py b/app_config.py index 6ee2cf7..b23c851 100644 --- a/app_config.py +++ b/app_config.py @@ -130,6 +130,40 @@ class ConfigManager: 'icon': 'magnifying-glass' } ] + }, + 'domains': { + 'default_domains': [ + { + 'name': 'Algèbre', + 'color': '#3b82f6', + 'description': 'Calculs algébriques, équations, expressions' + }, + { + 'name': 'Géométrie', + 'color': '#10b981', + 'description': 'Figures, mesures, constructions géométriques' + }, + { + 'name': 'Statistiques', + 'color': '#f59e0b', + 'description': 'Données, moyennes, graphiques statistiques' + }, + { + 'name': 'Fonctions', + 'color': '#8b5cf6', + 'description': 'Fonctions, graphiques, tableaux de valeurs' + }, + { + 'name': 'Problèmes', + 'color': '#ef4444', + 'description': 'Résolution de problèmes concrets' + }, + { + 'name': 'Calcul mental', + 'color': '#06b6d4', + 'description': 'Calculs rapides, estimations' + } + ] } } @@ -175,6 +209,18 @@ class ConfigManager: ) db.session.add(competence) + # Domaines par défaut + from models import Domain + if Domain.query.count() == 0: + default_domains = self.default_config['domains']['default_domains'] + for domain_data in default_domains: + domain = Domain( + name=domain_data['name'], + color=domain_data['color'], + description=domain_data.get('description', '') + ) + db.session.add(domain) + db.session.commit() def get(self, key_path: str, default: Any = None) -> Any: @@ -267,6 +313,44 @@ class ConfigManager: for comp in competences ] + def get_domains_list(self) -> List[Dict[str, Any]]: + """Récupère la liste des domaines configurés.""" + from models import Domain + domains = Domain.query.order_by(Domain.name).all() + return [ + { + 'id': domain.id, + 'name': domain.name, + 'color': domain.color, + 'description': domain.description or '' + } + for domain in domains + ] + + def add_domain(self, name: str, color: str = '#6B7280', description: str = '') -> bool: + """Ajoute un nouveau domaine.""" + try: + from models import Domain + domain = Domain(name=name, color=color, description=description) + db.session.add(domain) + db.session.commit() + return True + except Exception as e: + db.session.rollback() + from flask import current_app + current_app.logger.error(f"Erreur lors de l'ajout du domaine: {e}") + return False + + def get_or_create_domain(self, name: str, color: str = '#6B7280') -> 'Domain': + """Récupère un domaine existant ou le crée s'il n'existe pas.""" + from models import Domain + domain = Domain.query.filter_by(name=name).first() + if not domain: + domain = Domain(name=name, color=color) + db.session.add(domain) + db.session.commit() + return domain + def get_school_year(self) -> str: """Récupère l'année scolaire courante.""" return self.get('context.school_year', '2025-2026') diff --git a/commands.py b/commands.py index cf4c5e5..0ae05fb 100644 --- a/commands.py +++ b/commands.py @@ -1,6 +1,6 @@ import click from flask.cli import with_appcontext -from models import db, ClassGroup, Student, Assessment, Exercise, GradingElement +from models import db, ClassGroup, Student, Assessment, Exercise, GradingElement, Domain @click.command() @with_appcontext @@ -62,21 +62,26 @@ def init_db(): db.session.add(exercise) db.session.commit() - # Create sample grading elements + # Récupérer des domaines existants (créés automatiquement par la configuration) + domain_calcul = Domain.query.filter_by(name='Algèbre').first() + domain_methode = Domain.query.filter_by(name='Problèmes').first() + + # Create sample grading elements with domains (optionnels) elements_data = [ - ("Calcul de base", "Addition et soustraction de fractions", "Calculer", 4.0, "notes"), - ("Méthode", "Justification de la méthode utilisée", "Raisonner", 2.0, "score"), - ("Présentation", "Clarté de la présentation", "Communiquer", 2.0, "score"), + ("Calcul de base", "Addition et soustraction de fractions", "Calculer", 4.0, "notes", domain_calcul.id if domain_calcul else None), + ("Méthode", "Justification de la méthode utilisée", "Raisonner", 2.0, "score", domain_methode.id if domain_methode else None), + ("Présentation", "Clarté de la présentation", "Communiquer", 2.0, "score", None), # Pas de domaine spécifique ] - for label, description, skill, max_points, grading_type in elements_data: + for label, description, skill, max_points, grading_type, domain_id in elements_data: element = GradingElement( exercise_id=exercise.id, label=label, description=description, skill=skill, max_points=max_points, - grading_type=grading_type + grading_type=grading_type, + domain_id=domain_id ) db.session.add(element) @@ -113,6 +118,13 @@ def create_large_test_data(): db.session.commit() + # Récupérer les domaines existants (créés automatiquement par la configuration) + domain_fonctions = Domain.query.filter_by(name='Fonctions').first() + domain_calcul = Domain.query.filter_by(name='Algèbre').first() + domain_geometrie = Domain.query.filter_by(name='Géométrie').first() + domain_stats = Domain.query.filter_by(name='Statistiques').first() + domain_problemes = Domain.query.filter_by(name='Problèmes').first() + # Create a complex assessment with 4 exercises, 5 elements each assessment = Assessment( title="Contrôle de Mathématiques - Fonctions et Statistiques", @@ -135,16 +147,16 @@ def create_large_test_data(): db.session.commit() ex1_elements = [ - ("1a - Calcul image", "Calculer f(3)", "Calculer", 2.0, "notes"), - ("1b - Antécédent", "Résoudre f(x)=5", "Calculer", 3.0, "notes"), - ("1c - Graphique", "Tracer la droite", "Représenter", 3.0, "score"), - ("1d - Lecture graph", "Lire coordonnées", "Modéliser", 2.0, "notes"), - ("1e - Méthode", "Justification", "Raisonner", 2.0, "score") + ("1a - Calcul image", "Calculer f(3)", "Calculer", 2.0, "notes", domain_fonctions.id if domain_fonctions else None), + ("1b - Antécédent", "Résoudre f(x)=5", "Calculer", 3.0, "notes", domain_calcul.id if domain_calcul else None), + ("1c - Graphique", "Tracer la droite", "Représenter", 3.0, "score", domain_fonctions.id if domain_fonctions else None), + ("1d - Lecture graph", "Lire coordonnées", "Modéliser", 2.0, "notes", domain_fonctions.id if domain_fonctions else None), + ("1e - Méthode", "Justification", "Raisonner", 2.0, "score", domain_problemes.id if domain_problemes else None) ] - for label, desc, skill, points, gtype in ex1_elements: + for label, desc, skill, points, gtype, domain_id in ex1_elements: elem = GradingElement(exercise_id=ex1.id, label=label, description=desc, - skill=skill, max_points=points, grading_type=gtype) + skill=skill, max_points=points, grading_type=gtype, domain_id=domain_id) db.session.add(elem) # Exercise 2: Équations @@ -158,16 +170,16 @@ def create_large_test_data(): db.session.commit() ex2_elements = [ - ("2a - Équation simple", "Résoudre 2x+3=7", "Calculer", 2.0, "notes"), - ("2b - Avec parenthèses", "3(x-1)=2x+5", "Calculer", 4.0, "notes"), - ("2c - Vérification", "Contrôler solution", "Raisonner", 1.0, "score"), - ("2d - Méthode", "Étapes de résolution", "Communiquer", 2.0, "score"), - ("2e - Application", "Problème concret", "Modéliser", 3.0, "score") + ("2a - Équation simple", "Résoudre 2x+3=7", "Calculer", 2.0, "notes", domain_calcul.id if domain_calcul else None), + ("2b - Avec parenthèses", "3(x-1)=2x+5", "Calculer", 4.0, "notes", domain_calcul.id if domain_calcul else None), + ("2c - Vérification", "Contrôler solution", "Raisonner", 1.0, "score", domain_problemes.id if domain_problemes else None), + ("2d - Méthode", "Étapes de résolution", "Communiquer", 2.0, "score", domain_problemes.id if domain_problemes else None), + ("2e - Application", "Problème concret", "Modéliser", 3.0, "score", domain_problemes.id if domain_problemes else None) ] - for label, desc, skill, points, gtype in ex2_elements: + for label, desc, skill, points, gtype, domain_id in ex2_elements: elem = GradingElement(exercise_id=ex2.id, label=label, description=desc, - skill=skill, max_points=points, grading_type=gtype) + skill=skill, max_points=points, grading_type=gtype, domain_id=domain_id) db.session.add(elem) # Exercise 3: Statistiques @@ -181,16 +193,16 @@ def create_large_test_data(): db.session.commit() ex3_elements = [ - ("3a - Moyenne", "Calculer moyenne", "Calculer", 3.0, "notes"), - ("3b - Médiane", "Déterminer médiane", "Calculer", 2.0, "notes"), - ("3c - Quartiles", "Q1 et Q3", "Calculer", 4.0, "notes"), - ("3d - Interprétation", "Analyser résultats", "Raisonner", 3.0, "score"), - ("3e - Graphique", "Diagramme en boîte", "Représenter", 2.0, "score") + ("3a - Moyenne", "Calculer moyenne", "Calculer", 3.0, "notes", domain_stats.id if domain_stats else None), + ("3b - Médiane", "Déterminer médiane", "Calculer", 2.0, "notes", domain_stats.id if domain_stats else None), + ("3c - Quartiles", "Q1 et Q3", "Calculer", 4.0, "notes", domain_stats.id if domain_stats else None), + ("3d - Interprétation", "Analyser résultats", "Raisonner", 3.0, "score", domain_stats.id if domain_stats else None), + ("3e - Graphique", "Diagramme en boîte", "Représenter", 2.0, "score", domain_stats.id if domain_stats else None) ] - for label, desc, skill, points, gtype in ex3_elements: + for label, desc, skill, points, gtype, domain_id in ex3_elements: elem = GradingElement(exercise_id=ex3.id, label=label, description=desc, - skill=skill, max_points=points, grading_type=gtype) + skill=skill, max_points=points, grading_type=gtype, domain_id=domain_id) db.session.add(elem) # Exercise 4: Problème de synthèse @@ -204,16 +216,16 @@ def create_large_test_data(): db.session.commit() ex4_elements = [ - ("4a - Modélisation", "Mise en équation", "Modéliser", 4.0, "score"), - ("4b - Résolution", "Calculs", "Calculer", 5.0, "notes"), - ("4c - Interprétation", "Sens du résultat", "Raisonner", 3.0, "score"), - ("4d - Communication", "Rédaction", "Communiquer", 3.0, "score"), - ("4e - Démarche", "Organisation", "Raisonner", 3.0, "score") + ("4a - Modélisation", "Mise en équation", "Modéliser", 4.0, "score", domain_problemes.id if domain_problemes else None), + ("4b - Résolution", "Calculs", "Calculer", 5.0, "notes", domain_calcul.id if domain_calcul else None), + ("4c - Interprétation", "Sens du résultat", "Raisonner", 3.0, "score", domain_problemes.id if domain_problemes else None), + ("4d - Communication", "Rédaction", "Communiquer", 3.0, "score", domain_problemes.id if domain_problemes else None), + ("4e - Démarche", "Organisation", "Raisonner", 3.0, "score", domain_problemes.id if domain_problemes else None) ] - for label, desc, skill, points, gtype in ex4_elements: + for label, desc, skill, points, gtype, domain_id in ex4_elements: elem = GradingElement(exercise_id=ex4.id, label=label, description=desc, - skill=skill, max_points=points, grading_type=gtype) + skill=skill, max_points=points, grading_type=gtype, domain_id=domain_id) db.session.add(elem) db.session.commit() diff --git a/models.py b/models.py index 1d22421..e3d1f06 100644 --- a/models.py +++ b/models.py @@ -285,6 +285,8 @@ class GradingElement(db.Model): max_points = db.Column(db.Float, nullable=False) # Garder Float pour compatibilité # NOUVEAU : Types enum directement grading_type = db.Column(Enum('notes', 'score', name='grading_types'), nullable=False, default='notes') + # Ajout du champ domain_id + domain_id = db.Column(db.Integer, db.ForeignKey('domains.id'), nullable=True) # Optionnel grades = db.relationship('Grade', backref='grading_element', lazy=True, cascade='all, delete-orphan') def __repr__(self): @@ -343,4 +345,22 @@ class Competence(db.Model): updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def __repr__(self): - return f'' \ No newline at end of file + return f'' + + +class Domain(db.Model): + """Domaines/tags pour les éléments de notation.""" + __tablename__ = 'domains' + + 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, default='#6B7280') # Format #RRGGBB + 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) + + # Relation inverse + grading_elements = db.relationship('GradingElement', backref='domain', lazy=True) + + def __repr__(self): + return f'' \ No newline at end of file diff --git a/routes/assessments.py b/routes/assessments.py index 595480a..550e39e 100644 --- a/routes/assessments.py +++ b/routes/assessments.py @@ -155,14 +155,16 @@ def edit(id): 'description': element.description or '', 'skill': element.skill or '', 'max_points': float(element.max_points), - 'grading_type': element.grading_type + 'grading_type': element.grading_type, + 'domain_id': element.domain_id } exercise_data['grading_elements'].append(element_data) exercises_data.append(exercise_data) - # Récupérer les compétences configurées + # Récupérer les compétences et domaines configurées from app_config import config_manager competences = config_manager.get_competences_list() + domains = config_manager.get_domains_list() return render_template('assessment_form_unified.html', form=form, @@ -170,7 +172,8 @@ def edit(id): assessment=assessment, exercises_json=exercises_data, is_edit=True, - competences=competences) + competences=competences, + domains=domains) @bp.route('/new', methods=['GET', 'POST']) @handle_db_errors @@ -182,13 +185,15 @@ def new(): if result: return result - # Récupérer les compétences configurées + # Récupérer les compétences et domaines configurées competences = config_manager.get_competences_list() + domains = config_manager.get_domains_list() return render_template('assessment_form_unified.html', form=form, title='Nouvelle évaluation complète', - competences=competences) + competences=competences, + domains=domains) @bp.route('//results') @handle_db_errors diff --git a/routes/domains.py b/routes/domains.py new file mode 100644 index 0000000..a34d051 --- /dev/null +++ b/routes/domains.py @@ -0,0 +1,156 @@ +from flask import Blueprint, jsonify, request, current_app +from models import db, Domain +from app_config import config_manager +from utils import handle_db_errors + +bp = Blueprint('domains', __name__, url_prefix='/api/domains') + +@bp.route('/', methods=['GET']) +@handle_db_errors +def list_domains(): + """Liste tous les domaines disponibles.""" + domains = config_manager.get_domains_list() + return jsonify({'success': True, 'domains': domains}) + + +@bp.route('/', methods=['POST']) +@handle_db_errors +def create_domain(): + """Crée un nouveau domaine dynamiquement.""" + data = request.get_json() + + if not data or not data.get('name'): + return jsonify({'success': False, 'error': 'Nom du domaine requis'}), 400 + + name = data['name'].strip() + color = data.get('color', '#6B7280') + description = data.get('description', '') + + # Vérifier que le domaine n'existe pas déjà + if Domain.query.filter_by(name=name).first(): + return jsonify({'success': False, 'error': 'Un domaine avec ce nom existe déjà'}), 400 + + success = config_manager.add_domain(name, color, description) + + if success: + # Récupérer le domaine créé + domain = Domain.query.filter_by(name=name).first() + return jsonify({ + 'success': True, + 'domain': { + 'id': domain.id, + 'name': domain.name, + 'color': domain.color, + 'description': domain.description or '' + } + }) + else: + return jsonify({'success': False, 'error': 'Erreur lors de la création du domaine'}), 500 + +@bp.route('/search', methods=['GET']) +@handle_db_errors +def search_domains(): + """Recherche des domaines avec autocomplétion avancée.""" + query = request.args.get('q', '').strip() + + if not query or len(query) < 1: + return jsonify({'success': True, 'domains': []}) + + # Utiliser la recherche directe en base pour plus d'efficacité + domains_query = Domain.query.filter( + Domain.name.ilike(f'%{query}%') + ).order_by( + # Prioriser les correspondances exactes, puis celles qui commencent par la requête + db.case( + (Domain.name.ilike(query), 1), + (Domain.name.ilike(f'{query}%'), 2), + else_=3 + ), + Domain.name.asc() + ).limit(8) + + domains = domains_query.all() + + # Convertir en format JSON + domains_data = [{ + 'id': domain.id, + 'name': domain.name, + 'color': domain.color, + 'description': domain.description or '' + } for domain in domains] + + return jsonify({'success': True, 'domains': domains_data}) + +@bp.route('/', methods=['PUT']) +@handle_db_errors +def update_domain(domain_id): + """Met à jour un domaine existant.""" + data = request.get_json() + domain = Domain.query.get_or_404(domain_id) + + if data.get('name'): + domain.name = data['name'].strip() + if data.get('color'): + domain.color = data['color'] + if 'description' in data: + domain.description = data['description'] + + try: + db.session.commit() + return jsonify({'success': True}) + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Erreur lors de la mise à jour du domaine: {e}") + return jsonify({'success': False, 'error': 'Erreur lors de la sauvegarde'}), 500 + +@bp.route('/', methods=['DELETE']) +@handle_db_errors +def delete_domain(domain_id): + """Supprime un domaine (si non utilisé).""" + domain = Domain.query.get_or_404(domain_id) + + # Vérifier que le domaine n'est pas utilisé + if domain.grading_elements: + return jsonify({ + 'success': False, + 'error': f'Ce domaine est utilisé par {len(domain.grading_elements)} éléments de notation' + }), 400 + + try: + db.session.delete(domain) + db.session.commit() + return jsonify({'success': True}) + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Erreur lors de la suppression du domaine: {e}") + return jsonify({'success': False, 'error': 'Erreur lors de la suppression'}), 500 + +@bp.route('//usage', methods=['GET']) +@handle_db_errors +def domain_usage(domain_id): + """Récupère les informations d'utilisation d'un domaine.""" + domain = Domain.query.get_or_404(domain_id) + + # Compter les éléments de notation utilisant ce domaine + elements_count = len(domain.grading_elements) + + # Récupérer les évaluations concernées + assessments = set() + for element in domain.grading_elements: + assessments.add(element.exercise.assessment) + + return jsonify({ + 'success': True, + 'usage': { + 'elements_count': elements_count, + 'assessments_count': len(assessments), + 'assessments': [ + { + 'id': assessment.id, + 'title': assessment.title, + 'class_name': assessment.class_group.name + } + for assessment in assessments + ] + } + }) \ No newline at end of file diff --git a/services.py b/services.py index 982ffda..82472a6 100644 --- a/services.py +++ b/services.py @@ -137,12 +137,26 @@ class AssessmentService: except ValueError as e: raise ValidationError(str(e)) + # Gestion du domaine + domain_id = None + if 'domain_name' in elem_data and elem_data['domain_name']: + # Récupérer ou créer le domaine + from app_config import config_manager + domain = config_manager.get_or_create_domain( + elem_data['domain_name'], + elem_data.get('domain_color', '#6B7280') + ) + domain_id = domain.id + elif 'domain_id' in elem_data: + domain_id = elem_data['domain_id'] + grading_element = GradingElement( label=elem_data['label'].strip(), description=elem_data.get('description', '').strip(), skill=elem_data.get('skill', '').strip(), max_points=max_points, grading_type=grading_type, + domain_id=domain_id, exercise_id=exercise.id ) db.session.add(grading_element) \ No newline at end of file diff --git a/templates/assessment_detail.html b/templates/assessment_detail.html index 742ee4f..6984003 100644 --- a/templates/assessment_detail.html +++ b/templates/assessment_detail.html @@ -280,6 +280,12 @@
{{ element.label }}
+ {% if element.domain %} + + {{ element.domain.name }} + + {% endif %} {% if element.skill %} {{ element.skill }} diff --git a/templates/assessment_form_unified.html b/templates/assessment_form_unified.html index 1fdaf4d..8344df2 100644 --- a/templates/assessment_form_unified.html +++ b/templates/assessment_form_unified.html @@ -250,6 +250,16 @@ {% endfor %}
+
+ +
+ + + +
+
@@ -271,9 +281,427 @@
+ + + diff --git a/templates/assessment_grading.html b/templates/assessment_grading.html index 16a24c8..39bb250 100644 --- a/templates/assessment_grading.html +++ b/templates/assessment_grading.html @@ -184,6 +184,15 @@
{{ element.label }}
+ {% if element.domain %} +
+ + {{ element.domain.name }} + +
+ {% endif %} + {% if element.description %}
{{ element.description }}
{% endif %}