267 lines
10 KiB
Python
267 lines
10 KiB
Python
"""
|
|
Version refactorisée des modèles après application des principes SOLID.
|
|
|
|
Cette version montre comment la classe Assessment devient plus simple
|
|
après extraction des services métier.
|
|
"""
|
|
from flask_sqlalchemy import SQLAlchemy
|
|
from datetime import datetime
|
|
from sqlalchemy import Index, CheckConstraint, Enum
|
|
from decimal import Decimal
|
|
from typing import Optional, Dict, Any
|
|
from flask import current_app
|
|
|
|
# Import des services pour délégation
|
|
from services.assessment_services import ProgressResult, StudentScore, StatisticsResult
|
|
from providers.concrete_providers import AssessmentServicesFactory
|
|
|
|
db = SQLAlchemy()
|
|
|
|
|
|
class ClassGroup(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
name = db.Column(db.String(100), nullable=False, unique=True)
|
|
description = db.Column(db.Text)
|
|
year = db.Column(db.String(20), nullable=False)
|
|
students = db.relationship('Student', backref='class_group', lazy=True)
|
|
assessments = db.relationship('Assessment', backref='class_group', lazy=True)
|
|
|
|
def __repr__(self):
|
|
return f'<ClassGroup {self.name}>'
|
|
|
|
|
|
class Student(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
last_name = db.Column(db.String(100), nullable=False)
|
|
first_name = db.Column(db.String(100), nullable=False)
|
|
email = db.Column(db.String(120), unique=True)
|
|
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
|
|
grades = db.relationship('Grade', backref='student', lazy=True)
|
|
|
|
def __repr__(self):
|
|
return f'<Student {self.first_name} {self.last_name}>'
|
|
|
|
@property
|
|
def full_name(self):
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
|
|
class Assessment(db.Model):
|
|
"""
|
|
Modèle Assessment refactorisé selon les principes SOLID.
|
|
|
|
AVANT: 267 lignes avec 4 responsabilités
|
|
APRÈS: ~80 lignes avec 1 responsabilité (modèle de données)
|
|
|
|
Les responsabilités métier ont été extraites vers:
|
|
- AssessmentProgressService (progression)
|
|
- StudentScoreCalculator (scores étudiants)
|
|
- AssessmentStatisticsService (statistiques)
|
|
"""
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
title = db.Column(db.String(200), nullable=False)
|
|
description = db.Column(db.Text)
|
|
date = db.Column(db.Date, nullable=False, default=datetime.utcnow)
|
|
trimester = db.Column(db.Integer, nullable=False) # 1, 2, ou 3
|
|
class_group_id = db.Column(db.Integer, db.ForeignKey('class_group.id'), nullable=False)
|
|
coefficient = db.Column(db.Float, default=1.0)
|
|
exercises = db.relationship('Exercise', backref='assessment', lazy=True, cascade='all, delete-orphan')
|
|
|
|
__table_args__ = (
|
|
CheckConstraint('trimester IN (1, 2, 3)', name='check_trimester_valid'),
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f'<Assessment {self.title}>'
|
|
|
|
# =============== DELEGATION VERS LES SERVICES ===============
|
|
|
|
@property
|
|
def grading_progress(self) -> Dict[str, Any]:
|
|
"""
|
|
Délègue le calcul de progression au service dédié.
|
|
Plus d'import circulaire, pas de logique métier dans le modèle.
|
|
"""
|
|
services = AssessmentServicesFactory.create_facade()
|
|
progress: ProgressResult = services.get_grading_progress(self)
|
|
|
|
# Conversion en dict pour compatibilité avec l'API existante
|
|
return {
|
|
'percentage': progress.percentage,
|
|
'completed': progress.completed,
|
|
'total': progress.total,
|
|
'status': progress.status,
|
|
'students_count': progress.students_count
|
|
}
|
|
|
|
def calculate_student_scores(self):
|
|
"""
|
|
Délègue le calcul des scores au service dédié.
|
|
Plus de requêtes N+1, logique optimisée dans le service.
|
|
"""
|
|
services = AssessmentServicesFactory.create_facade()
|
|
students_scores, exercise_scores = services.calculate_student_scores(self)
|
|
|
|
# Conversion pour compatibilité avec l'API existante
|
|
converted_students = {}
|
|
for student_id, score in students_scores.items():
|
|
converted_students[student_id] = {
|
|
'student': next(s for s in self.class_group.students if s.id == student_id),
|
|
'total_score': score.total_score,
|
|
'total_max_points': score.total_max_points,
|
|
'exercises': score.exercises
|
|
}
|
|
|
|
return converted_students, exercise_scores
|
|
|
|
def get_assessment_statistics(self) -> Dict[str, float]:
|
|
"""
|
|
Délègue les calculs statistiques au service dédié.
|
|
Logique métier externalisée, modèle simplifié.
|
|
"""
|
|
services = AssessmentServicesFactory.create_facade()
|
|
stats: StatisticsResult = services.get_statistics(self)
|
|
|
|
# Conversion en dict pour compatibilité
|
|
return {
|
|
'count': stats.count,
|
|
'mean': stats.mean,
|
|
'median': stats.median,
|
|
'min': stats.min,
|
|
'max': stats.max,
|
|
'std_dev': stats.std_dev
|
|
}
|
|
|
|
def get_total_max_points(self) -> float:
|
|
"""
|
|
Calcule le total des points maximum.
|
|
Seule logique métier simple gardée dans le modèle.
|
|
"""
|
|
total = 0
|
|
for exercise in self.exercises:
|
|
for element in exercise.grading_elements:
|
|
total += element.max_points
|
|
return total
|
|
|
|
|
|
class Exercise(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
assessment_id = db.Column(db.Integer, db.ForeignKey('assessment.id'), nullable=False)
|
|
title = db.Column(db.String(200), nullable=False)
|
|
description = db.Column(db.Text)
|
|
order = db.Column(db.Integer, default=1)
|
|
grading_elements = db.relationship('GradingElement', backref='exercise', lazy=True, cascade='all, delete-orphan')
|
|
|
|
def __repr__(self):
|
|
return f'<Exercise {self.title}>'
|
|
|
|
|
|
class GradingElement(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
exercise_id = db.Column(db.Integer, db.ForeignKey('exercise.id'), nullable=False)
|
|
label = db.Column(db.String(200), nullable=False)
|
|
description = db.Column(db.Text)
|
|
skill = db.Column(db.String(200))
|
|
max_points = db.Column(db.Float, nullable=False)
|
|
grading_type = db.Column(Enum('notes', 'score', name='grading_types'), nullable=False, default='notes')
|
|
domain_id = db.Column(db.Integer, db.ForeignKey('domains.id'), nullable=True)
|
|
grades = db.relationship('Grade', backref='grading_element', lazy=True, cascade='all, delete-orphan')
|
|
|
|
def __repr__(self):
|
|
return f'<GradingElement {self.label}>'
|
|
|
|
|
|
class Grade(db.Model):
|
|
id = db.Column(db.Integer, primary_key=True)
|
|
student_id = db.Column(db.Integer, db.ForeignKey('student.id'), nullable=False)
|
|
grading_element_id = db.Column(db.Integer, db.ForeignKey('grading_element.id'), nullable=False)
|
|
value = db.Column(db.String(10))
|
|
comment = db.Column(db.Text)
|
|
|
|
def __repr__(self):
|
|
return f'<Grade {self.value} for {self.student.first_name if self.student else "Unknown"}>'
|
|
|
|
|
|
# Configuration tables (inchangées)
|
|
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)
|
|
label = db.Column(db.String(100), nullable=False)
|
|
color = db.Column(db.String(7), nullable=False)
|
|
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)
|
|
icon = db.Column(db.String(50), nullable=False)
|
|
order_index = db.Column(db.Integer, default=0)
|
|
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}>'
|
|
|
|
|
|
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')
|
|
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'<Domain {self.name}>'
|
|
|
|
|
|
# =============== CLASSE POUR RETROCOMPATIBILITÉ ===============
|
|
|
|
class GradingCalculator:
|
|
"""
|
|
Classe pour rétrocompatibilité. Délègue vers les nouveaux services.
|
|
À supprimer progressivement au profit de l'injection de dépendances.
|
|
"""
|
|
|
|
@staticmethod
|
|
def calculate_score(grade_value: str, grading_type: str, max_points: float) -> Optional[float]:
|
|
"""Délègue vers le nouveau service unifié."""
|
|
services = AssessmentServicesFactory.create_facade()
|
|
return services.grading_calculator.calculate_score(grade_value, grading_type, max_points)
|
|
|
|
@staticmethod
|
|
def is_counted_in_total(grade_value: str, grading_type: str) -> bool:
|
|
"""Délègue vers le nouveau service unifié."""
|
|
services = AssessmentServicesFactory.create_facade()
|
|
return services.grading_calculator.is_counted_in_total(grade_value) |