Files
notytex/models_refactored.py

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)