fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
120
geo-service/models/learning_node.py
Normal file
120
geo-service/models/learning_node.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user