feat: uniform competence management

This commit is contained in:
2025-08-05 20:44:54 +02:00
parent 4a2d8a73e1
commit 91eb04ca01
26 changed files with 3801 additions and 167 deletions

149
models.py
View File

@@ -1,10 +1,74 @@
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from sqlalchemy import Index, CheckConstraint
from sqlalchemy import Index, CheckConstraint, Enum
from decimal import Decimal
from typing import Optional, Dict, Any
from flask import current_app
db = SQLAlchemy()
class GradingCalculator:
"""Calculateur unifié pour tous types de notation."""
@staticmethod
def calculate_score(grade_value: str, grading_type: str, max_points: float) -> Optional[float]:
"""
UN seul point d'entrée pour tous les calculs de score.
Args:
grade_value: Valeur de la note (ex: '15.5', '2', '.', 'd')
grading_type: Type de notation ('notes' ou 'score')
max_points: Points maximum de l'élément de notation
Returns:
Score calculé ou None pour les valeurs dispensées
"""
# Éviter les imports circulaires en important à l'utilisation
from app_config import config_manager
# Valeurs spéciales en premier
if config_manager.is_special_value(grade_value):
special_config = config_manager.get_special_values()[grade_value]
special_value = special_config['value']
if special_value is None: # Dispensé
return None
return float(special_value) # 0 pour '.', 'a'
# Calcul selon type
try:
if grading_type == 'notes':
return float(grade_value)
elif grading_type == 'score':
# Score 0-3 converti en proportion du max_points
score_int = int(grade_value)
if 0 <= score_int <= 3:
return (score_int / 3) * max_points
return 0.0
except (ValueError, TypeError):
return 0.0
return 0.0
@staticmethod
def is_counted_in_total(grade_value: str, grading_type: str) -> bool:
"""
Détermine si une note doit être comptée dans le total.
Returns:
True si la note compte dans le total, False sinon (ex: dispensé)
"""
from app_config import config_manager
# Valeurs spéciales
if config_manager.is_special_value(grade_value):
special_config = config_manager.get_special_values()[grade_value]
return special_config['counts']
# Toutes les autres valeurs comptent
return True
class ClassGroup(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
@@ -109,13 +173,9 @@ class Assessment(db.Model):
def calculate_student_scores(self):
"""Calcule les scores de tous les élèves pour cette évaluation.
Retourne un dictionnaire avec les scores par élève et par exercice."""
Retourne un dictionnaire avec les scores par élève et par exercice.
Logique de calcul simplifiée avec 2 types seulement."""
from collections import defaultdict
import statistics
from app_config import config_manager
# Récupérer l'échelle des compétences configurée
competence_scale = config_manager.get_competence_scale_values()
students_scores = {}
exercise_scores = defaultdict(lambda: defaultdict(float))
@@ -135,59 +195,21 @@ class Assessment(db.Model):
grading_element_id=element.id
).first()
# Si une note a été saisie pour cet élément (y compris '.')
# Si une note a été saisie pour cet élément (y compris valeurs spéciales)
if grade and grade.value and grade.value != '':
if element.grading_type == 'points':
if grade.value == '.':
# '.' signifie non répondu = 0 point mais on compte le max
exercise_score += 0
exercise_max_points += element.max_points
else:
try:
exercise_score += float(grade.value)
exercise_max_points += element.max_points
except ValueError:
pass
else: # compétences - utiliser la nouvelle échelle
grade_value = grade.value.strip()
# Gérer les valeurs numériques et string
scale_key = int(grade_value) if grade_value.isdigit() else grade_value
if scale_key in competence_scale:
scale_config = competence_scale[scale_key]
if scale_config['included_in_total']:
# Calculer le score selon l'échelle configurée
if grade_value == '.':
# Non évalué = 0 point
exercise_score += 0
else:
# Calculer le score proportionnel
# Trouver la valeur maximale de l'échelle
max_scale_value = max([
int(k) if str(k).isdigit() else 0
for k in competence_scale.keys()
if competence_scale[k]['included_in_total'] and k != '.'
])
if max_scale_value > 0:
if grade_value.isdigit():
score_ratio = int(grade_value) / max_scale_value
exercise_score += score_ratio * element.max_points
# Compter les points maximum (sauf pour '.')
if grade_value != '.':
exercise_max_points += element.max_points
# Si not included_in_total, on ne compte ni score ni max
else:
# Valeur non reconnue, utiliser l'ancienne logique par défaut
try:
score_value = float(grade_value)
exercise_score += (1/3) * score_value * element.max_points
exercise_max_points += element.max_points
except ValueError:
pass
# Utiliser la nouvelle logique unifiée
calculated_score = GradingCalculator.calculate_score(
grade.value.strip(),
element.grading_type,
element.max_points
)
# Vérifier si cette note compte dans le total
if GradingCalculator.is_counted_in_total(grade.value.strip(), element.grading_type):
if calculated_score is not None: # Pas dispensé
exercise_score += calculated_score
exercise_max_points += element.max_points
# Si pas compté ou dispensé, on ignore complètement
student_exercises[exercise.id] = {
'score': exercise_score,
@@ -239,10 +261,8 @@ class Assessment(db.Model):
total = 0
for exercise in self.exercises:
for element in exercise.grading_elements:
if element.grading_type == 'points':
total += element.max_points
else: # compétences
total += (1/3) * 3 * element.max_points # Score max de 3
# Logique simplifiée avec 2 types : notes et score
total += element.max_points
return total
class Exercise(db.Model):
@@ -263,7 +283,8 @@ class GradingElement(db.Model):
description = db.Column(db.Text)
skill = db.Column(db.String(200))
max_points = db.Column(db.Float, nullable=False) # Garder Float pour compatibilité
grading_type = db.Column(db.String(10), nullable=False, default='points')
# NOUVEAU : Types enum directement
grading_type = db.Column(Enum('notes', 'score', name='grading_types'), nullable=False, default='notes')
grades = db.relationship('Grade', backref='grading_element', lazy=True, cascade='all, delete-orphan')
def __repr__(self):