feat: uniform competence management
This commit is contained in:
149
models.py
149
models.py
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user