morozovka-portal/app/models.py

323 lines
14 KiB
Python

from sqlmodel import SQLModel, Field
from typing import Optional
from datetime import datetime, timezone
class User(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
gitea_id: Optional[int] = Field(default=None, unique=True, index=True)
username: str = Field(unique=True, index=True)
password_hash: Optional[str] = None
full_name: Optional[str] = None
email: Optional[str] = Field(default=None, unique=True)
organization: Optional[str] = None
role: str = Field(default="student")
is_active: bool = Field(default=True)
is_verified: bool = Field(default=False)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class UserSession(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
session_token: str = Field(unique=True, index=True)
user_id: int = Field(foreign_key="user.id")
ip_address: Optional[str] = None
user_agent: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class Event(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str
event_type: str = Field(default="lecture")
start_time: datetime
end_time: datetime
location: Optional[str] = None
description: Optional[str] = None
status: str = Field(default="scheduled")
is_public: bool = Field(default=True)
google_event_id: Optional[str] = Field(default=None, unique=True, index=True)
manually_edited: bool = Field(default=False)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class GlobalStatus(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
status: str = Field(default="free")
delay_minutes: int = Field(default=0)
message: Optional[str] = None
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class Project(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str
description: Optional[str] = None
status: str = Field(default="idea")
visibility: str = Field(default="public")
gitea_url: Optional[str] = None
gitea_repo: Optional[str] = None
owner_id: int = Field(foreign_key="user.id")
tags: Optional[str] = None
start_date: Optional[str] = None
end_date: Optional[str] = None
contact_person: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class ProjectMember(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
project_id: int = Field(foreign_key="project.id")
user_id: int = Field(foreign_key="user.id")
role: str = Field(default="viewer")
can_view_files: bool = Field(default=True)
can_edit_files: bool = Field(default=False)
can_view_repo: bool = Field(default=True)
can_post_feed: bool = Field(default=False)
can_edit_own_posts: bool = Field(default=True)
can_edit_any_post: bool = Field(default=False)
can_manage_members: bool = Field(default=False)
joined_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class ProjectTask(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
project_id: int = Field(foreign_key="project.id")
title: str
description: Optional[str] = None
status: str = Field(default="backlog")
priority: str = Field(default="medium")
assignee_id: Optional[int] = Field(default=None, foreign_key="user.id")
created_by: int = Field(foreign_key="user.id")
due_date: Optional[str] = None
position: int = Field(default=0)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class ProjectPost(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
project_id: int = Field(foreign_key="project.id")
author_id: int = Field(foreign_key="user.id")
title: str
body: Optional[str] = None
is_pinned: bool = Field(default=False)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class ProjectPostAttachment(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
post_id: int = Field(foreign_key="projectpost.id")
file_id: int = Field(foreign_key="portalfile.id")
class ProjectSubscription(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
project_id: int = Field(foreign_key="project.id")
user_id: int = Field(foreign_key="user.id")
notify_email: bool = Field(default=True)
notify_inapp: bool = Field(default=True)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class PortalFile(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
original_name: str
stored_name: str
size_bytes: int
uploaded_by: int = Field(foreign_key="user.id")
project_id: Optional[int] = Field(default=None, foreign_key="project.id")
uploaded_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
# ── v2.0 ──────────────────────────────────────────────────────────
space_id: Optional[int] = Field(default=None, foreign_key="filespace.id")
folder_id: Optional[int] = Field(default=None, foreign_key="filefolder.id")
description: Optional[str] = None
tags: Optional[str] = None
version: int = Field(default=1)
current_version_id: Optional[int] = Field(default=None)
class EventSubscriber(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
email: str = Field(index=True)
user_id: Optional[int] = Field(default=None, foreign_key="user.id")
unsubscribe_token: str = Field(unique=True, index=True)
is_confirmed: bool = Field(default=False)
confirm_token: Optional[str] = Field(default=None, unique=True, index=True)
notify_status: bool = Field(default=True)
notify_events: bool = Field(default=True)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class Workshop(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
title: str
description: Optional[str] = None
program: Optional[str] = None
lead_id: int = Field(foreign_key="user.id")
category: str = Field(default="other")
difficulty: str = Field(default="beginner")
partner: Optional[str] = None
status: str = Field(default="draft")
visibility: str = Field(default="public")
location: Optional[str] = None
materials_required: Optional[str] = None
outcome: Optional[str] = None
has_certificate: bool = Field(default=False)
recurrence: Optional[str] = None
max_participants: Optional[int] = None
cover_url: Optional[str] = None
tags: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class WorkshopAssistant(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
workshop_id: int = Field(foreign_key="workshop.id")
user_id: int = Field(foreign_key="user.id")
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class WorkshopSession(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
workshop_id: int = Field(foreign_key="workshop.id")
start_time: datetime
end_time: datetime
location_override: Optional[str] = None
note: Optional[str] = None
class WorkshopRegistration(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
workshop_id: int = Field(foreign_key="workshop.id")
user_id: int = Field(foreign_key="user.id")
status: str = Field(default="registered")
notes: Optional[str] = None
registered_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class WorkshopMaterial(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
workshop_id: int = Field(foreign_key="workshop.id")
title: str
description: Optional[str] = None
file_id: Optional[int] = Field(default=None, foreign_key="portalfile.id")
external_url: Optional[str] = None
visible_to: str = Field(default="registered")
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class WorkshopPost(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
workshop_id: int = Field(foreign_key="workshop.id")
author_id: int = Field(foreign_key="user.id")
title: str
body: Optional[str] = None
is_pinned: bool = Field(default=False)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class WorkshopPostAttachment(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
post_id: int = Field(foreign_key="workshoppost.id")
file_id: int = Field(foreign_key="portalfile.id")
class WorkshopSubscription(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
workshop_id: int = Field(foreign_key="workshop.id")
user_id: int = Field(foreign_key="user.id")
notify_email: bool = Field(default=True)
notify_inapp: bool = Field(default=True)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class WorkshopFeedback(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
workshop_id: int = Field(foreign_key="workshop.id")
user_id: int = Field(foreign_key="user.id")
rating: int = Field(default=5)
comment: Optional[str] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class Notification(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id", index=True)
title: str
body: Optional[str] = None
link_url: Optional[str] = None
kind: str = Field(default="info")
is_read: bool = Field(default=False, index=True)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class UserOnboarding(SQLModel, table=True):
"""Прогресс прохождения обучающего тура по платформе."""
id: Optional[int] = Field(default=None, primary_key=True)
user_id: int = Field(foreign_key="user.id", unique=True, index=True)
visited_schedule: bool = Field(default=False)
visited_projects: bool = Field(default=False)
visited_workshops: bool = Field(default=False)
visited_files: bool = Field(default=False)
visited_feed: bool = Field(default=False)
visited_profile: bool = Field(default=False)
completed_at: Optional[datetime] = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class FileSpace(SQLModel, table=True):
"""Пространство файлов: по проекту, по направлению, личное."""
id: Optional[int] = Field(default=None, primary_key=True)
name: str
slug: str = Field(unique=True, index=True)
description: Optional[str] = None
owner_id: int = Field(foreign_key="user.id")
project_id: Optional[int] = Field(default=None, foreign_key="project.id")
visibility: str = Field(default="internal") # public | internal | private
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class FileFolder(SQLModel, table=True):
"""Папка внутри пространства. parent_id=null — корень пространства."""
id: Optional[int] = Field(default=None, primary_key=True)
space_id: int = Field(foreign_key="filespace.id")
parent_id: Optional[int] = Field(default=None, foreign_key="filefolder.id")
name: str
description: Optional[str] = None
created_by: int = Field(foreign_key="user.id")
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class FileSpaceACL(SQLModel, table=True):
"""Права пользователя на пространство."""
id: Optional[int] = Field(default=None, primary_key=True)
space_id: int = Field(foreign_key="filespace.id")
user_id: int = Field(foreign_key="user.id")
role: str = Field(default="viewer") # viewer | editor | manager
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class FileFolderACL(SQLModel, table=True):
"""Права пользователя на папку (перекрывают права пространства)."""
id: Optional[int] = Field(default=None, primary_key=True)
folder_id: int = Field(foreign_key="filefolder.id")
user_id: int = Field(foreign_key="user.id")
can_view: bool = Field(default=True)
can_upload: bool = Field(default=False)
can_delete: bool = Field(default=False)
can_manage: bool = Field(default=False)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class FileVersion(SQLModel, table=True):
"""Версия файла — каждая имеет свой stored_name."""
id: Optional[int] = Field(default=None, primary_key=True)
file_id: int = Field(foreign_key="portalfile.id")
version_num: int = Field(default=1)
stored_name: str
size_bytes: int
uploaded_by: int = Field(foreign_key="user.id")
uploaded_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
comment: Optional[str] = None