293 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			293 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| from flask_sqlalchemy import SQLAlchemy
 | |
| from datetime import datetime
 | |
| from sqlalchemy import Index, CheckConstraint
 | |
| from decimal import Decimal
 | |
| 
 | |
| 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):
 | |
|     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)  # Garder Float pour compatibilité
 | |
|     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}>'
 | |
|     
 | |
|     @property
 | |
|     def grading_progress(self):
 | |
|         """Calcule le pourcentage de progression des notes saisies pour cette évaluation.
 | |
|         Retourne un dictionnaire avec les statistiques de progression."""
 | |
|         
 | |
|         # Obtenir tous les éléments de notation pour cette évaluation
 | |
|         total_elements = 0
 | |
|         completed_elements = 0
 | |
|         total_students = len(self.class_group.students)
 | |
|         
 | |
|         if total_students == 0:
 | |
|             return {
 | |
|                 'percentage': 0,
 | |
|                 'completed': 0,
 | |
|                 'total': 0,
 | |
|                 'status': 'no_students'
 | |
|             }
 | |
|         
 | |
|         # Parcourir tous les exercices et leurs éléments de notation
 | |
|         for exercise in self.exercises:
 | |
|             for grading_element in exercise.grading_elements:
 | |
|                 total_elements += total_students
 | |
|                 
 | |
|                 # Compter les notes saisies (valeur non nulle et non vide, y compris '.')
 | |
|                 completed_for_element = db.session.query(Grade).filter(
 | |
|                     Grade.grading_element_id == grading_element.id,
 | |
|                     Grade.value.isnot(None),
 | |
|                     Grade.value != ''
 | |
|                 ).count()
 | |
|                 
 | |
|                 completed_elements += completed_for_element
 | |
|         
 | |
|         if total_elements == 0:
 | |
|             return {
 | |
|                 'percentage': 0,
 | |
|                 'completed': 0,
 | |
|                 'total': 0,
 | |
|                 'status': 'no_elements'
 | |
|             }
 | |
|         
 | |
|         percentage = round((completed_elements / total_elements) * 100)
 | |
|         
 | |
|         # Déterminer le statut
 | |
|         if percentage == 0:
 | |
|             status = 'not_started'
 | |
|         elif percentage == 100:
 | |
|             status = 'completed'
 | |
|         else:
 | |
|             status = 'in_progress'
 | |
|         
 | |
|         return {
 | |
|             'percentage': percentage,
 | |
|             'completed': completed_elements,
 | |
|             'total': total_elements,
 | |
|             'status': status,
 | |
|             'students_count': total_students
 | |
|         }
 | |
|     
 | |
|     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."""
 | |
|         from collections import defaultdict
 | |
|         import statistics
 | |
|         
 | |
|         students_scores = {}
 | |
|         exercise_scores = defaultdict(lambda: defaultdict(float))
 | |
|         
 | |
|         for student in self.class_group.students:
 | |
|             total_score = 0
 | |
|             total_max_points = 0
 | |
|             student_exercises = {}
 | |
|             
 | |
|             for exercise in self.exercises:
 | |
|                 exercise_score = 0
 | |
|                 exercise_max_points = 0
 | |
|                 
 | |
|                 for element in exercise.grading_elements:
 | |
|                     grade = Grade.query.filter_by(
 | |
|                         student_id=student.id,
 | |
|                         grading_element_id=element.id
 | |
|                     ).first()
 | |
|                     
 | |
|                     # Si une note a été saisie pour cet élément (y compris '.')
 | |
|                     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
 | |
|                             if grade.value == '.':
 | |
|                                 # '.' signifie non évalué = 0 point mais on compte le max
 | |
|                                 exercise_score += 0
 | |
|                                 exercise_max_points += (1/3) * 3 * element.max_points  # Score max de 3
 | |
|                             else:
 | |
|                                 try:
 | |
|                                     score_value = float(grade.value)
 | |
|                                     exercise_score += (1/3) * score_value * element.max_points
 | |
|                                     exercise_max_points += (1/3) * 3 * element.max_points  # Score max de 3
 | |
|                                 except ValueError:
 | |
|                                     pass
 | |
|                 
 | |
|                 student_exercises[exercise.id] = {
 | |
|                     'score': exercise_score,
 | |
|                     'max_points': exercise_max_points,
 | |
|                     'title': exercise.title
 | |
|                 }
 | |
|                 total_score += exercise_score
 | |
|                 total_max_points += exercise_max_points
 | |
|                 exercise_scores[exercise.id][student.id] = exercise_score
 | |
|             
 | |
|             students_scores[student.id] = {
 | |
|                 'student': student,
 | |
|                 'total_score': round(total_score, 2),
 | |
|                 'total_max_points': total_max_points,
 | |
|                 'exercises': student_exercises
 | |
|             }
 | |
|         
 | |
|         return students_scores, dict(exercise_scores)
 | |
|     
 | |
|     def get_assessment_statistics(self):
 | |
|         """Calcule les statistiques descriptives pour cette évaluation."""
 | |
|         students_scores, _ = self.calculate_student_scores()
 | |
|         scores = [data['total_score'] for data in students_scores.values()]
 | |
|         
 | |
|         if not scores:
 | |
|             return {
 | |
|                 'count': 0,
 | |
|                 'mean': 0,
 | |
|                 'median': 0,
 | |
|                 'min': 0,
 | |
|                 'max': 0,
 | |
|                 'std_dev': 0
 | |
|             }
 | |
|         
 | |
|         import statistics
 | |
|         import math
 | |
|         
 | |
|         return {
 | |
|             'count': len(scores),
 | |
|             'mean': round(statistics.mean(scores), 2),
 | |
|             'median': round(statistics.median(scores), 2),
 | |
|             'min': min(scores),
 | |
|             'max': max(scores),
 | |
|             'std_dev': round(statistics.stdev(scores) if len(scores) > 1 else 0, 2)
 | |
|         }
 | |
|     
 | |
|     def get_total_max_points(self):
 | |
|         """Calcule le total des points maximum pour cette évaluation."""
 | |
|         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
 | |
|         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)  # Garder Float pour compatibilité
 | |
|     grading_type = db.Column(db.String(10), nullable=False, default='points')
 | |
|     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))  # Garder l'ancien format pour compatibilité
 | |
|     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
 | |
| 
 | |
| 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)  # '0', '1', '2', '3', '.', 'd', etc.
 | |
|     label = db.Column(db.String(100), nullable=False)
 | |
|     color = db.Column(db.String(7), nullable=False)  # Format #RRGGBB
 | |
|     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)  # Format #RRGGBB
 | |
|     icon = db.Column(db.String(50), nullable=False)
 | |
|     order_index = db.Column(db.Integer, default=0)  # Pour l'ordre d'affichage
 | |
|     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}>' |