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:
2025-11-25 21:09:47 +01:00
parent 60c60c1605
commit 2b08eb534a
4125 changed files with 303 additions and 453271 deletions

View 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}>"