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>
165 lines
5.1 KiB
Python
165 lines
5.1 KiB
Python
"""
|
|
BreakPilot Content Service - Database Models
|
|
Educational Content Management mit Creative Commons Lizenzen
|
|
"""
|
|
from sqlalchemy import Column, String, Integer, Float, DateTime, JSON, ForeignKey, Enum, Table
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.orm import relationship
|
|
from datetime import datetime
|
|
import enum
|
|
import uuid
|
|
|
|
Base = declarative_base()
|
|
|
|
# Content Type Enum
|
|
class ContentType(str, enum.Enum):
|
|
VIDEO = "video"
|
|
PDF = "pdf"
|
|
IMAGE_GALLERY = "image_gallery"
|
|
MARKDOWN = "markdown"
|
|
AUDIO = "audio"
|
|
H5P = "h5p"
|
|
|
|
# CC License Enum
|
|
class CCLicense(str, enum.Enum):
|
|
CC_BY = "CC-BY-4.0"
|
|
CC_BY_SA = "CC-BY-SA-4.0"
|
|
CC_BY_NC = "CC-BY-NC-4.0"
|
|
CC_BY_NC_SA = "CC-BY-NC-SA-4.0"
|
|
CC0 = "CC0-1.0"
|
|
|
|
# Category Enum
|
|
class ContentCategory(str, enum.Enum):
|
|
MOVEMENT = "movement"
|
|
MATH = "math"
|
|
STEAM = "steam"
|
|
LANGUAGE = "language"
|
|
ARTS = "arts"
|
|
SOCIAL = "social"
|
|
MINDFULNESS = "mindfulness"
|
|
|
|
# Content Status
|
|
class ContentStatus(str, enum.Enum):
|
|
DRAFT = "draft"
|
|
REVIEW = "review"
|
|
PUBLISHED = "published"
|
|
ARCHIVED = "archived"
|
|
|
|
# Many-to-Many: Content <-> Tags
|
|
content_tags = Table(
|
|
'content_tags',
|
|
Base.metadata,
|
|
Column('content_id', String, ForeignKey('contents.id')),
|
|
Column('tag_id', String, ForeignKey('tags.id'))
|
|
)
|
|
|
|
class Content(Base):
|
|
"""Educational Content Model"""
|
|
__tablename__ = 'contents'
|
|
|
|
# Primary Key
|
|
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
|
|
# Creator Info
|
|
creator_id = Column(String, nullable=False, index=True) # User ID from consent-service
|
|
creator_name = Column(String, nullable=False)
|
|
creator_email = Column(String)
|
|
|
|
# Content Metadata
|
|
title = Column(String(500), nullable=False, index=True)
|
|
description = Column(String(5000))
|
|
content_type = Column(Enum(ContentType), nullable=False)
|
|
category = Column(Enum(ContentCategory), nullable=False, index=True)
|
|
|
|
# License
|
|
license = Column(Enum(CCLicense), nullable=False, default=CCLicense.CC_BY_SA)
|
|
|
|
# Age Range
|
|
age_min = Column(Integer, default=6)
|
|
age_max = Column(Integer, default=18)
|
|
|
|
# Files & URLs
|
|
files = Column(JSON, default=list) # List of file URLs in MinIO
|
|
thumbnail_url = Column(String)
|
|
embed_url = Column(String) # For YouTube, Vimeo, etc.
|
|
|
|
# H5P Specific
|
|
h5p_content_id = Column(String) # H5P content ID if type=h5p
|
|
|
|
# Matrix Integration
|
|
matrix_room_id = Column(String) # Associated Matrix room for discussion
|
|
matrix_event_id = Column(String) # Matrix message event ID
|
|
|
|
# Status
|
|
status = Column(Enum(ContentStatus), default=ContentStatus.DRAFT, index=True)
|
|
|
|
# Analytics
|
|
downloads = Column(Integer, default=0)
|
|
views = Column(Integer, default=0)
|
|
avg_rating = Column(Float, default=0.0)
|
|
rating_count = Column(Integer, default=0)
|
|
impact_score = Column(Float, default=0.0) # Future: Impact-Scoring
|
|
|
|
# Timestamps
|
|
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
published_at = Column(DateTime)
|
|
|
|
# Relationships
|
|
ratings = relationship("Rating", back_populates="content", cascade="all, delete-orphan")
|
|
tags = relationship("Tag", secondary=content_tags, back_populates="contents")
|
|
|
|
def __repr__(self):
|
|
return f"<Content {self.title} ({self.content_type})>"
|
|
|
|
|
|
class Rating(Base):
|
|
"""Content Ratings by Teachers"""
|
|
__tablename__ = 'ratings'
|
|
|
|
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
content_id = Column(String, ForeignKey('contents.id'), nullable=False)
|
|
user_id = Column(String, nullable=False, index=True) # Teacher ID
|
|
user_name = Column(String)
|
|
|
|
stars = Column(Integer, nullable=False) # 1-5
|
|
comment = Column(String(2000))
|
|
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
# Relationship
|
|
content = relationship("Content", back_populates="ratings")
|
|
|
|
def __repr__(self):
|
|
return f"<Rating {self.stars}★ for Content {self.content_id}>"
|
|
|
|
|
|
class Tag(Base):
|
|
"""Content Tags for Search/Filter"""
|
|
__tablename__ = 'tags'
|
|
|
|
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
name = Column(String(100), unique=True, nullable=False, index=True)
|
|
category = Column(String(100)) # Optional grouping
|
|
|
|
# Relationship
|
|
contents = relationship("Content", secondary=content_tags, back_populates="tags")
|
|
|
|
def __repr__(self):
|
|
return f"<Tag {self.name}>"
|
|
|
|
|
|
class Download(Base):
|
|
"""Download Tracking (für Impact-Scoring)"""
|
|
__tablename__ = 'downloads'
|
|
|
|
id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
|
|
content_id = Column(String, ForeignKey('contents.id'), nullable=False, index=True)
|
|
user_id = Column(String, nullable=False, index=True) # Teacher ID
|
|
|
|
downloaded_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
ip_address = Column(String) # Optional, anonymisiert nach 7 Tagen
|
|
|
|
def __repr__(self):
|
|
return f"<Download {self.content_id} by {self.user_id}>"
|