""" Modèles SQLAlchemy pour Notytex v2. IMPORTANT: Ce fichier contient UNIQUEMENT les définitions de tables. La logique métier est dans domain/services/. Ces modèles sont identiques au schéma de la v1 pour assurer la compatibilité de la base de données partagée. """ from datetime import datetime, date from typing import Optional, List from sqlalchemy import ( Column, Integer, String, Float, Date, DateTime, Text, Boolean, ForeignKey, CheckConstraint, Enum, Index, UniqueConstraint, ) from sqlalchemy.orm import relationship, DeclarativeBase, Mapped, mapped_column class Base(DeclarativeBase): """Base class for all models.""" pass class ClassGroup(Base): """Groupe de classe (6ème A, 5ème B, etc.)""" __tablename__ = "class_group" id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) description: Mapped[Optional[str]] = mapped_column(Text) year: Mapped[str] = mapped_column(String(20), nullable=False) # Relations assessments: Mapped[List["Assessment"]] = relationship( "Assessment", back_populates="class_group", lazy="selectin" ) enrollments: Mapped[List["StudentEnrollment"]] = relationship( "StudentEnrollment", back_populates="class_group", lazy="selectin" ) council_appreciations: Mapped[List["CouncilAppreciation"]] = relationship( "CouncilAppreciation", back_populates="class_group", lazy="selectin" ) def __repr__(self): return f"" class StudentEnrollment(Base): """ Historique temporel des inscriptions élève-classe. Pattern: Association temporelle avec validité temporelle. """ __tablename__ = "student_enrollments" id: Mapped[int] = mapped_column(Integer, primary_key=True) student_id: Mapped[int] = mapped_column(Integer, ForeignKey("student.id"), nullable=False) class_group_id: Mapped[int] = mapped_column( Integer, ForeignKey("class_group.id"), nullable=False ) # Période de validité enrollment_date: Mapped[date] = mapped_column(Date, nullable=False) departure_date: Mapped[Optional[date]] = mapped_column(Date, nullable=True) # Métadonnées enrollment_reason: Mapped[Optional[str]] = mapped_column(String(200)) departure_reason: Mapped[Optional[str]] = mapped_column(String(200)) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow ) # Relations student: Mapped["Student"] = relationship("Student", back_populates="enrollments") class_group: Mapped["ClassGroup"] = relationship("ClassGroup", back_populates="enrollments") __table_args__ = ( CheckConstraint( "departure_date IS NULL OR departure_date >= enrollment_date", name="check_valid_enrollment_period", ), Index( "idx_student_temporal", "student_id", "enrollment_date", "departure_date" ), Index( "idx_class_temporal", "class_group_id", "enrollment_date", "departure_date" ), ) def __repr__(self): return f"" class Student(Base): """Élève""" __tablename__ = "student" id: Mapped[int] = mapped_column(Integer, primary_key=True) last_name: Mapped[str] = mapped_column(String(100), nullable=False) first_name: Mapped[str] = mapped_column(String(100), nullable=False) email: Mapped[Optional[str]] = mapped_column(String(120), unique=True) # Relations grades: Mapped[List["Grade"]] = relationship( "Grade", back_populates="student", lazy="selectin" ) enrollments: Mapped[List["StudentEnrollment"]] = relationship( "StudentEnrollment", back_populates="student", lazy="selectin" ) council_appreciations: Mapped[List["CouncilAppreciation"]] = relationship( "CouncilAppreciation", back_populates="student", lazy="selectin" ) @property def full_name(self) -> str: """Prénom Nom""" return f"{self.first_name} {self.last_name}" @property def full_name_reversed(self) -> str: """Nom Prénom""" return f"{self.last_name} {self.first_name}" def __repr__(self): return f"" class Assessment(Base): """Évaluation""" __tablename__ = "assessment" id: Mapped[int] = mapped_column(Integer, primary_key=True) title: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text) date: Mapped[date] = mapped_column(Date, nullable=False, default=datetime.utcnow) trimester: Mapped[int] = mapped_column(Integer, nullable=False) class_group_id: Mapped[int] = mapped_column( Integer, ForeignKey("class_group.id"), nullable=False ) coefficient: Mapped[float] = mapped_column(Float, default=1.0) # Relations class_group: Mapped["ClassGroup"] = relationship( "ClassGroup", back_populates="assessments" ) exercises: Mapped[List["Exercise"]] = relationship( "Exercise", back_populates="assessment", lazy="selectin", cascade="all, delete-orphan", ) __table_args__ = ( CheckConstraint("trimester IN (1, 2, 3)", name="check_trimester_valid"), ) def __repr__(self): return f"" class Exercise(Base): """Exercice d'une évaluation""" __tablename__ = "exercise" id: Mapped[int] = mapped_column(Integer, primary_key=True) assessment_id: Mapped[int] = mapped_column( Integer, ForeignKey("assessment.id"), nullable=False ) title: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text) order: Mapped[int] = mapped_column(Integer, default=1) # Relations assessment: Mapped["Assessment"] = relationship( "Assessment", back_populates="exercises" ) grading_elements: Mapped[List["GradingElement"]] = relationship( "GradingElement", back_populates="exercise", lazy="selectin", cascade="all, delete-orphan", ) def __repr__(self): return f"" class GradingElement(Base): """Élément de notation (question, critère, etc.)""" __tablename__ = "grading_element" id: Mapped[int] = mapped_column(Integer, primary_key=True) exercise_id: Mapped[int] = mapped_column( Integer, ForeignKey("exercise.id"), nullable=False ) label: Mapped[str] = mapped_column(String(200), nullable=False) description: Mapped[Optional[str]] = mapped_column(Text) skill: Mapped[Optional[str]] = mapped_column(String(200)) max_points: Mapped[float] = mapped_column(Float, nullable=False) grading_type: Mapped[str] = mapped_column( Enum("notes", "score", name="grading_types"), nullable=False, default="notes" ) domain_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("domains.id"), nullable=True ) # Relations exercise: Mapped["Exercise"] = relationship( "Exercise", back_populates="grading_elements" ) domain: Mapped[Optional["Domain"]] = relationship( "Domain", back_populates="grading_elements" ) grades: Mapped[List["Grade"]] = relationship( "Grade", back_populates="grading_element", lazy="selectin", cascade="all, delete-orphan", ) def __repr__(self): return f"" class Grade(Base): """Note attribuée à un élève pour un élément de notation""" __tablename__ = "grade" id: Mapped[int] = mapped_column(Integer, primary_key=True) student_id: Mapped[int] = mapped_column( Integer, ForeignKey("student.id"), nullable=False ) grading_element_id: Mapped[int] = mapped_column( Integer, ForeignKey("grading_element.id"), nullable=False ) value: Mapped[Optional[str]] = mapped_column(String(10)) comment: Mapped[Optional[str]] = mapped_column(Text) # Relations student: Mapped["Student"] = relationship("Student", back_populates="grades") grading_element: Mapped["GradingElement"] = relationship( "GradingElement", back_populates="grades" ) def __repr__(self): return f"" # Configuration tables class AppConfig(Base): """Configuration simple de l'application (clé-valeur).""" __tablename__ = "app_config" key: Mapped[str] = mapped_column(String(100), primary_key=True) value: Mapped[str] = mapped_column(Text, nullable=False) description: Mapped[Optional[str]] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow ) def __repr__(self): return f"" class CompetenceScaleValue(Base): """Valeurs de l'échelle des compétences (0, 1, 2, 3, ., d, etc.).""" __tablename__ = "competence_scale_values" value: Mapped[str] = mapped_column(String(10), primary_key=True) label: Mapped[str] = mapped_column(String(100), nullable=False) color: Mapped[str] = mapped_column(String(7), nullable=False) included_in_total: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow ) def __repr__(self): return f"" class Competence(Base): """Liste des compétences (Calculer, Raisonner, etc.).""" __tablename__ = "competences" id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) color: Mapped[str] = mapped_column(String(7), nullable=False) icon: Mapped[str] = mapped_column(String(50), nullable=False) order_index: Mapped[int] = mapped_column(Integer, default=0) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow ) def __repr__(self): return f"" class Domain(Base): """Domaines/tags pour les éléments de notation.""" __tablename__ = "domains" id: Mapped[int] = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) color: Mapped[str] = mapped_column(String(7), nullable=False, default="#6B7280") description: Mapped[Optional[str]] = mapped_column(Text) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) updated_at: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow ) # Relation inverse grading_elements: Mapped[List["GradingElement"]] = relationship( "GradingElement", back_populates="domain", lazy="selectin" ) def __repr__(self): return f"" class CouncilAppreciation(Base): """Appréciations saisies lors de la préparation du conseil de classe.""" __tablename__ = "council_appreciations" id: Mapped[int] = mapped_column(Integer, primary_key=True) student_id: Mapped[int] = mapped_column( Integer, ForeignKey("student.id"), nullable=False ) class_group_id: Mapped[int] = mapped_column( Integer, ForeignKey("class_group.id"), nullable=False ) trimester: Mapped[int] = mapped_column(Integer, nullable=False) # Appréciations structurées general_appreciation: Mapped[Optional[str]] = mapped_column(Text) strengths: Mapped[Optional[str]] = mapped_column(Text) areas_for_improvement: Mapped[Optional[str]] = mapped_column(Text) # Statut et métadonnées status: Mapped[str] = mapped_column( Enum("draft", "finalized", name="appreciation_status"), nullable=False, default="draft", ) last_modified: Mapped[datetime] = mapped_column( DateTime, default=datetime.utcnow, onupdate=datetime.utcnow ) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow) # Relations student: Mapped["Student"] = relationship( "Student", back_populates="council_appreciations" ) class_group: Mapped["ClassGroup"] = relationship( "ClassGroup", back_populates="council_appreciations" ) __table_args__ = ( CheckConstraint( "trimester IN (1, 2, 3)", name="check_appreciation_trimester_valid" ), UniqueConstraint( "student_id", "class_group_id", "trimester", name="uq_student_class_trimester_appreciation", ), ) def __repr__(self): return f""