first commit

This commit is contained in:
Space
2026-01-17 13:37:57 +01:00
commit 3e34d84a29
49 changed files with 8579 additions and 0 deletions

265
backend/models.py Normal file
View File

@@ -0,0 +1,265 @@
from pydantic import BaseModel, EmailStr, Field, field_validator
from typing import Optional, List, Dict
from datetime import datetime
from bson import ObjectId
class PyObjectId(ObjectId):
"""Custom ObjectId type for Pydantic"""
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v):
if not ObjectId.is_valid(v):
raise ValueError("Invalid ObjectId")
return ObjectId(v)
@classmethod
def __get_pydantic_json_schema__(cls, field_schema):
field_schema.update(type="string")
# User Models
class UserBase(BaseModel):
email: EmailStr
username: str = Field(..., min_length=3, max_length=50)
class UserCreate(UserBase):
password: str = Field(..., min_length=6)
class UserLogin(BaseModel):
email: EmailStr
password: str
class UserInDB(UserBase):
id: str = Field(default_factory=lambda: str(ObjectId()), alias="_id")
hashed_password: str
created_at: datetime = Field(default_factory=datetime.utcnow)
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
class UserResponse(UserBase):
id: str = Field(..., alias="_id")
created_at: datetime
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
user: UserResponse
# Grading Category Model (flexible, not German-specific)
class GradingCategory(BaseModel):
name: str = Field(..., min_length=1, max_length=100) # e.g., "Written", "Oral", "Homework"
weight: float = Field(..., ge=0, le=100) # Percentage weight (0-100)
color: Optional[str] = Field(default="#3b82f6") # Hex color for UI
# Subject Model
class SubjectBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
grading_categories: List[GradingCategory] = Field(..., min_items=1)
color: Optional[str] = Field(default="#3b82f6")
teacher: Optional[str] = None
grade_system: Optional[str] = Field(default="percentage") # "percentage", "german", "us-letter"
target_grade: Optional[float] = None
target_grade_max: Optional[float] = None
@field_validator("grading_categories")
@classmethod
def validate_weights(cls, categories: List[GradingCategory]) -> List[GradingCategory]:
total_weight = sum(cat.weight for cat in categories)
if abs(total_weight - 100.0) > 0.01: # Allow small floating point errors
raise ValueError(f"Grading category weights must sum to 100%, got {total_weight}%")
return categories
class SubjectCreate(SubjectBase):
pass
class SubjectUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
grading_categories: Optional[List[GradingCategory]] = None
color: Optional[str] = None
teacher: Optional[str] = None
grade_system: Optional[str] = None
target_grade: Optional[float] = None
target_grade_max: Optional[float] = None
@field_validator("grading_categories")
@classmethod
def validate_weights(cls, categories: Optional[List[GradingCategory]]) -> Optional[List[GradingCategory]]:
if categories is None:
return None
total_weight = sum(cat.weight for cat in categories)
if abs(total_weight - 100.0) > 0.01:
raise ValueError(f"Grading category weights must sum to 100%, got {total_weight}%")
return categories
class SubjectInDB(SubjectBase):
id: str = Field(default_factory=lambda: str(ObjectId()), alias="_id")
user_id: str
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
class SubjectResponse(SubjectBase):
id: str = Field(..., alias="_id")
created_at: datetime
updated_at: datetime
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
# Test/Grade Model
class GradeBase(BaseModel):
subject_id: str
category_name: str # Must match one of the subject's grading categories
grade: float = Field(..., ge=0) # Grade value (e.g., 1.0 to 6.0 for German, 0-100 for percentage)
max_grade: float = Field(..., ge=0) # Maximum possible grade (e.g., 6.0 or 100)
weight_in_category: float = Field(default=1.0, ge=0) # Weight within the category (for averaging multiple tests)
name: Optional[str] = Field(None, max_length=200) # Test name/description
date: datetime = Field(default_factory=datetime.utcnow)
notes: Optional[str] = None
class GradeCreate(GradeBase):
pass
class GradeUpdate(BaseModel):
category_name: Optional[str] = None
grade: Optional[float] = Field(None, ge=0)
max_grade: Optional[float] = Field(None, ge=0)
weight_in_category: Optional[float] = Field(None, ge=0)
name: Optional[str] = Field(None, max_length=200)
date: Optional[datetime] = None
notes: Optional[str] = None
class GradeInDB(GradeBase):
id: str = Field(default_factory=lambda: str(ObjectId()), alias="_id")
user_id: str
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
class GradeResponse(GradeBase):
id: str = Field(..., alias="_id")
created_at: datetime
updated_at: datetime
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
# Report Period Model
class ReportPeriodBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100) # e.g., "First Half-Year 2024/2025"
start_date: datetime
end_date: datetime
@field_validator("end_date")
@classmethod
def validate_dates(cls, end_date: datetime, info) -> datetime:
start_date = info.data.get("start_date")
if start_date and end_date <= start_date:
raise ValueError("end_date must be after start_date")
return end_date
class ReportPeriodCreate(ReportPeriodBase):
pass
class ReportPeriodUpdate(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=100)
start_date: Optional[datetime] = None
end_date: Optional[datetime] = None
class ReportPeriodInDB(ReportPeriodBase):
id: str = Field(default_factory=lambda: str(ObjectId()), alias="_id")
user_id: str
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
class ReportPeriodResponse(ReportPeriodBase):
id: str = Field(..., alias="_id")
created_at: datetime
updated_at: datetime
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
# Teacher Grade Override Model (manual grades from teachers)
class TeacherGradeBase(BaseModel):
subject_id: str
period_id: str
grade: float = Field(..., ge=0) # The grade value (e.g., 1.0-6.0 for German, 0-100 for percentage)
max_grade: float = Field(..., ge=0) # Maximum possible grade (e.g., 6.0 or 100)
notes: Optional[str] = Field(None, max_length=500)
class TeacherGradeCreate(TeacherGradeBase):
pass
class TeacherGradeUpdate(BaseModel):
grade: Optional[float] = Field(None, ge=0)
max_grade: Optional[float] = Field(None, ge=0)
notes: Optional[str] = Field(None, max_length=500)
class TeacherGradeInDB(TeacherGradeBase):
id: str = Field(default_factory=lambda: str(ObjectId()), alias="_id")
user_id: str
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}
class TeacherGradeResponse(TeacherGradeBase):
id: str = Field(..., alias="_id")
created_at: datetime
updated_at: datetime
class Config:
populate_by_name = True
json_encoders = {ObjectId: str}