""" Learning Node Models Pydantic models for educational content nodes """ from enum import Enum from typing import Optional from pydantic import BaseModel, Field class LearningTheme(str, Enum): """Available learning themes for geographic education.""" TOPOGRAPHIE = "topographie" LANDNUTZUNG = "landnutzung" ORIENTIERUNG = "orientierung" GEOLOGIE = "geologie" HYDROLOGIE = "hydrologie" VEGETATION = "vegetation" class NodeType(str, Enum): """Type of learning node interaction.""" QUESTION = "question" # Multiple choice or open question OBSERVATION = "observation" # Guided observation task EXPLORATION = "exploration" # Free exploration with hints class Position(BaseModel): """Geographic position for a learning node.""" latitude: float = Field(..., ge=-90, le=90, description="Latitude in degrees") longitude: float = Field(..., ge=-180, le=180, description="Longitude in degrees") altitude: Optional[float] = Field(None, description="Altitude in meters") class LearningNode(BaseModel): """ A learning node (station) within a geographic area. Contains educational content tied to a specific location, including questions, hints, and explanations. """ id: str = Field(..., description="Unique node identifier") aoi_id: str = Field(..., description="Parent AOI identifier") title: str = Field(..., min_length=1, max_length=100, description="Node title") theme: LearningTheme = Field(..., description="Learning theme") position: dict = Field(..., description="Geographic position") question: str = Field(..., description="Learning question or task") hints: list[str] = Field(default_factory=list, description="Progressive hints") answer: str = Field(..., description="Correct answer or expected observation") explanation: str = Field(..., description="Didactic explanation") node_type: NodeType = Field(NodeType.QUESTION, description="Interaction type") points: int = Field(10, ge=1, le=100, description="Points awarded for completion") approved: bool = Field(False, description="Teacher-approved for student use") media: Optional[list[dict]] = Field(None, description="Associated media files") tags: Optional[list[str]] = Field(None, description="Content tags") difficulty: Optional[str] = Field(None, description="Difficulty level") grade_level: Optional[str] = Field(None, description="Target grade level") class Config: json_schema_extra = { "example": { "id": "node-001", "aoi_id": "550e8400-e29b-41d4-a716-446655440000", "title": "Höhenbestimmung", "theme": "topographie", "position": {"latitude": 47.7085, "longitude": 9.1925}, "question": "Schätze die Höhe dieses Punktes über dem Meeresspiegel.", "hints": [ "Schau dir die Vegetation an.", "Vergleiche mit dem Seespiegel des Bodensees (395m).", ], "answer": "Ca. 430 Meter über NN", "explanation": "Die Höhe lässt sich aus der Vegetation und der relativen Position zum Bodensee abschätzen.", "node_type": "question", "points": 10, "approved": True, } } class LearningNodeRequest(BaseModel): """Request model for creating a learning node manually.""" title: str = Field(..., min_length=1, max_length=100) theme: LearningTheme position: Position question: str hints: list[str] = Field(default_factory=list) answer: str explanation: str node_type: NodeType = NodeType.QUESTION points: int = Field(10, ge=1, le=100) tags: Optional[list[str]] = None difficulty: Optional[str] = Field(None, pattern="^(leicht|mittel|schwer)$") grade_level: Optional[str] = None class LearningNodeBatch(BaseModel): """Batch of learning nodes for bulk operations.""" nodes: list[LearningNode] total_points: int = 0 def calculate_total_points(self) -> int: """Calculate total points from all nodes.""" self.total_points = sum(node.points for node in self.nodes) return self.total_points class LearningProgress(BaseModel): """Student progress through learning nodes.""" student_id: str aoi_id: str completed_nodes: list[str] = Field(default_factory=list) total_points: int = 0 started_at: Optional[str] = None completed_at: Optional[str] = None @property def completion_percentage(self) -> float: """Calculate completion percentage.""" # Would need total node count for accurate calculation return len(self.completed_nodes) * 10 # Placeholder