first commit
This commit is contained in:
265
backend/models.py
Normal file
265
backend/models.py
Normal 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}
|
||||
Reference in New Issue
Block a user