Migration v1 (Flask) -> v2 (FastAPI + Vue.js) complétée
✨ Changements majeurs: - Suppression complète du code Flask legacy - Migration backend FastAPI vers racine /backend - Migration frontend Vue.js vers racine /frontend - Suppression de notytex-v2/ (code monté à la racine) ✅ Validations: - Backend démarre correctement (port 8000) - API /api/v2/health répond healthy - 99/99 tests unitaires passent - Frontend configuré avec proxy Vite 📝 Documentation: - README.md réécrit pour v2 - Instructions de démarrage mises à jour - .gitignore adapté pour backend/frontend/ 🎯 Architecture finale: notytex/ ├── backend/ # FastAPI + SQLAlchemy + Pydantic ├── frontend/ # Vue 3 + Vite + TailwindCSS ├── docs/ # Documentation └── school_management.db # Base de données (inchangée) Jalon 6 complété: Application v2 prête pour utilisation!
This commit is contained in:
376
backend/infrastructure/database/models.py
Normal file
376
backend/infrastructure/database/models.py
Normal file
@@ -0,0 +1,376 @@
|
||||
"""
|
||||
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"<ClassGroup {self.name}>"
|
||||
|
||||
|
||||
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"<StudentEnrollment {self.student_id} in {self.class_group_id} from {self.enrollment_date}>"
|
||||
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Student {self.first_name} {self.last_name}>"
|
||||
|
||||
|
||||
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"<Assessment {self.title}>"
|
||||
|
||||
|
||||
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"<Exercise {self.title}>"
|
||||
|
||||
|
||||
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"<GradingElement {self.label}>"
|
||||
|
||||
|
||||
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"<Grade {self.value}>"
|
||||
|
||||
|
||||
# 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"<AppConfig {self.key}={self.value}>"
|
||||
|
||||
|
||||
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"<CompetenceScaleValue {self.value}: {self.label}>"
|
||||
|
||||
|
||||
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"<Competence {self.name}>"
|
||||
|
||||
|
||||
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"<Domain {self.name}>"
|
||||
|
||||
|
||||
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"<CouncilAppreciation Student:{self.student_id} Class:{self.class_group_id} T{self.trimester}>"
|
||||
Reference in New Issue
Block a user