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:
19
geo-service/models/__init__.py
Normal file
19
geo-service/models/__init__.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""
|
||||
GeoEdu Service - Pydantic Models
|
||||
"""
|
||||
from .aoi import AOIRequest, AOIResponse, AOIStatus, AOIManifest
|
||||
from .learning_node import LearningNode, LearningNodeRequest, LearningTheme, NodeType
|
||||
from .attribution import Attribution, AttributionSource
|
||||
|
||||
__all__ = [
|
||||
"AOIRequest",
|
||||
"AOIResponse",
|
||||
"AOIStatus",
|
||||
"AOIManifest",
|
||||
"LearningNode",
|
||||
"LearningNodeRequest",
|
||||
"LearningTheme",
|
||||
"NodeType",
|
||||
"Attribution",
|
||||
"AttributionSource",
|
||||
]
|
||||
162
geo-service/models/aoi.py
Normal file
162
geo-service/models/aoi.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
AOI (Area of Interest) Models
|
||||
Pydantic models for AOI requests and responses
|
||||
"""
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AOIStatus(str, Enum):
|
||||
"""Status of an AOI processing job."""
|
||||
QUEUED = "queued"
|
||||
PROCESSING = "processing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class AOIQuality(str, Enum):
|
||||
"""Quality level for AOI bundle generation."""
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
|
||||
|
||||
class AOITheme(str, Enum):
|
||||
"""Learning theme for AOI."""
|
||||
TOPOGRAPHIE = "topographie"
|
||||
LANDNUTZUNG = "landnutzung"
|
||||
ORIENTIERUNG = "orientierung"
|
||||
GEOLOGIE = "geologie"
|
||||
HYDROLOGIE = "hydrologie"
|
||||
VEGETATION = "vegetation"
|
||||
|
||||
|
||||
class GeoJSONPolygon(BaseModel):
|
||||
"""GeoJSON Polygon geometry."""
|
||||
type: str = Field("Polygon", const=True)
|
||||
coordinates: list[list[list[float]]] = Field(
|
||||
...,
|
||||
description="Polygon coordinates as [[[lon, lat], ...]]",
|
||||
)
|
||||
|
||||
|
||||
class AOIRequest(BaseModel):
|
||||
"""Request model for creating an AOI."""
|
||||
polygon: dict = Field(
|
||||
...,
|
||||
description="GeoJSON Polygon geometry",
|
||||
example={
|
||||
"type": "Polygon",
|
||||
"coordinates": [[[9.19, 47.71], [9.20, 47.71], [9.20, 47.70], [9.19, 47.70], [9.19, 47.71]]]
|
||||
},
|
||||
)
|
||||
theme: str = Field(
|
||||
"topographie",
|
||||
description="Learning theme for the AOI",
|
||||
)
|
||||
quality: str = Field(
|
||||
"medium",
|
||||
pattern="^(low|medium|high)$",
|
||||
description="Bundle quality level",
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"polygon": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [[[9.1875, 47.7055], [9.1975, 47.7055], [9.1975, 47.7115], [9.1875, 47.7115], [9.1875, 47.7055]]]
|
||||
},
|
||||
"theme": "topographie",
|
||||
"quality": "medium",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AOIResponse(BaseModel):
|
||||
"""Response model for AOI operations."""
|
||||
aoi_id: str = Field(..., description="Unique AOI identifier")
|
||||
status: AOIStatus = Field(..., description="Current processing status")
|
||||
area_km2: float = Field(0, description="Area in square kilometers")
|
||||
estimated_size_mb: float = Field(0, description="Estimated bundle size in MB")
|
||||
message: Optional[str] = Field(None, description="Status message")
|
||||
download_url: Optional[str] = Field(None, description="Bundle download URL")
|
||||
manifest_url: Optional[str] = Field(None, description="Manifest URL")
|
||||
created_at: Optional[str] = Field(None, description="Creation timestamp")
|
||||
completed_at: Optional[str] = Field(None, description="Completion timestamp")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"aoi_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"status": "completed",
|
||||
"area_km2": 0.45,
|
||||
"estimated_size_mb": 25.5,
|
||||
"message": "AOI processing complete",
|
||||
"download_url": "/api/v1/aoi/550e8400-e29b-41d4-a716-446655440000/bundle.zip",
|
||||
"manifest_url": "/api/v1/aoi/550e8400-e29b-41d4-a716-446655440000/manifest.json",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AOIBounds(BaseModel):
|
||||
"""Geographic bounding box."""
|
||||
west: float = Field(..., description="Western longitude")
|
||||
south: float = Field(..., description="Southern latitude")
|
||||
east: float = Field(..., description="Eastern longitude")
|
||||
north: float = Field(..., description="Northern latitude")
|
||||
|
||||
|
||||
class AOICenter(BaseModel):
|
||||
"""Geographic center point."""
|
||||
longitude: float
|
||||
latitude: float
|
||||
|
||||
|
||||
class AOIManifest(BaseModel):
|
||||
"""Unity bundle manifest for an AOI."""
|
||||
version: str = Field("1.0.0", description="Manifest version")
|
||||
aoi_id: str = Field(..., description="AOI identifier")
|
||||
created_at: str = Field(..., description="Creation timestamp")
|
||||
bounds: AOIBounds = Field(..., description="Geographic bounds")
|
||||
center: AOICenter = Field(..., description="Geographic center")
|
||||
area_km2: float = Field(..., description="Area in km²")
|
||||
theme: str = Field(..., description="Learning theme")
|
||||
quality: str = Field(..., description="Quality level")
|
||||
assets: dict = Field(..., description="Asset file references")
|
||||
unity: dict = Field(..., description="Unity-specific configuration")
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"version": "1.0.0",
|
||||
"aoi_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"created_at": "2024-01-15T12:00:00Z",
|
||||
"bounds": {
|
||||
"west": 9.1875,
|
||||
"south": 47.7055,
|
||||
"east": 9.1975,
|
||||
"north": 47.7115,
|
||||
},
|
||||
"center": {
|
||||
"longitude": 9.1925,
|
||||
"latitude": 47.7085,
|
||||
},
|
||||
"area_km2": 0.45,
|
||||
"theme": "topographie",
|
||||
"quality": "medium",
|
||||
"assets": {
|
||||
"terrain": {"file": "terrain.heightmap.png", "config": "terrain.json"},
|
||||
"osm_features": {"file": "osm_features.json"},
|
||||
"learning_positions": {"file": "learning_positions.json"},
|
||||
"attribution": {"file": "attribution.json"},
|
||||
},
|
||||
"unity": {
|
||||
"coordinate_system": "Unity (Y-up, left-handed)",
|
||||
"scale": 1.0,
|
||||
"terrain_resolution": 256,
|
||||
},
|
||||
}
|
||||
}
|
||||
97
geo-service/models/attribution.py
Normal file
97
geo-service/models/attribution.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Attribution Models
|
||||
Models for license and attribution tracking
|
||||
"""
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class AttributionSource(BaseModel):
|
||||
"""
|
||||
Attribution information for a data source.
|
||||
|
||||
All geographic data requires proper attribution per their licenses.
|
||||
"""
|
||||
name: str = Field(..., description="Source name")
|
||||
license: str = Field(..., description="License name")
|
||||
url: str = Field(..., description="License or source URL")
|
||||
attribution: str = Field(..., description="Required attribution text")
|
||||
required: bool = Field(True, description="Whether attribution is legally required")
|
||||
logo_url: Optional[str] = Field(None, description="Optional logo URL")
|
||||
|
||||
|
||||
class Attribution(BaseModel):
|
||||
"""
|
||||
Complete attribution information for an AOI bundle.
|
||||
|
||||
Ensures DSGVO/GDPR compliance and proper data source attribution.
|
||||
"""
|
||||
sources: list[AttributionSource] = Field(
|
||||
...,
|
||||
description="List of data sources requiring attribution",
|
||||
)
|
||||
generated_at: str = Field(..., description="Timestamp when attribution was generated")
|
||||
notice: str = Field(
|
||||
"This data must be attributed according to the licenses above when used publicly.",
|
||||
description="General attribution notice",
|
||||
)
|
||||
|
||||
class Config:
|
||||
json_schema_extra = {
|
||||
"example": {
|
||||
"sources": [
|
||||
{
|
||||
"name": "OpenStreetMap",
|
||||
"license": "Open Database License (ODbL) v1.0",
|
||||
"url": "https://www.openstreetmap.org/copyright",
|
||||
"attribution": "© OpenStreetMap contributors",
|
||||
"required": True,
|
||||
},
|
||||
{
|
||||
"name": "Copernicus DEM",
|
||||
"license": "Copernicus Data License",
|
||||
"url": "https://spacedata.copernicus.eu/",
|
||||
"attribution": "© Copernicus Service Information 2024",
|
||||
"required": True,
|
||||
},
|
||||
],
|
||||
"generated_at": "2024-01-15T12:00:00Z",
|
||||
"notice": "This data must be attributed according to the licenses above when used publicly.",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Predefined attribution sources
|
||||
OSM_ATTRIBUTION = AttributionSource(
|
||||
name="OpenStreetMap",
|
||||
license="Open Database License (ODbL) v1.0",
|
||||
url="https://www.openstreetmap.org/copyright",
|
||||
attribution="© OpenStreetMap contributors",
|
||||
required=True,
|
||||
)
|
||||
|
||||
COPERNICUS_ATTRIBUTION = AttributionSource(
|
||||
name="Copernicus DEM",
|
||||
license="Copernicus Data License",
|
||||
url="https://spacedata.copernicus.eu/",
|
||||
attribution="© Copernicus Service Information 2024",
|
||||
required=True,
|
||||
)
|
||||
|
||||
OPENAERIAL_ATTRIBUTION = AttributionSource(
|
||||
name="OpenAerialMap",
|
||||
license="CC-BY 4.0",
|
||||
url="https://openaerialmap.org/",
|
||||
attribution="© OpenAerialMap contributors",
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
def get_default_attribution() -> Attribution:
|
||||
"""Get default attribution with standard sources."""
|
||||
from datetime import datetime
|
||||
|
||||
return Attribution(
|
||||
sources=[OSM_ATTRIBUTION, COPERNICUS_ATTRIBUTION],
|
||||
generated_at=datetime.utcnow().isoformat(),
|
||||
)
|
||||
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