feat(db): all domain models, initial Alembic migration (32 tables), seed script (8 districts, 89 regions, roles)
This commit is contained in:
1 parent
71460ab56a
commit
4a610ba94a
19 files changed
+1290
-2
No files matched your search
@@ -1,4 +1,49 @@
|
||||
"""Модели данных."""
|
||||
"""Модели данных (регистрация для Alembic)."""
|
||||
from app.models.advice import Advice
|
||||
from app.models.artifact import Artifact
|
||||
from app.models.audit import AssistantAction, AssistantConfig, AuditLog
|
||||
from app.models.broadcast import Broadcast, BroadcastRecipient, BroadcastTemplate
|
||||
from app.models.event import Event, EventReminder
|
||||
from app.models.goal import DataUpload, Goal, RegionGoalValue, Snapshot
|
||||
from app.models.history import PlatformHistory
|
||||
from app.models.knowledge import KnowledgeArticle, KnowledgeCategory
|
||||
from app.models.region import FederalDistrict, Region
|
||||
from app.models.region_card import RegionCard, RegionCardHistory, RegionLink
|
||||
from app.models.request import Request, RequestComment
|
||||
from app.models.task import Task, TaskComment
|
||||
from app.models.user import LoginToken, Role, Session, User, UserRegion, UserRole
|
||||
|
||||
__all__ = ["FederalDistrict", "Region"]
|
||||
__all__ = [
|
||||
"Advice",
|
||||
"Artifact",
|
||||
"AssistantAction",
|
||||
"AssistantConfig",
|
||||
"AuditLog",
|
||||
"Broadcast",
|
||||
"BroadcastRecipient",
|
||||
"BroadcastTemplate",
|
||||
"DataUpload",
|
||||
"Event",
|
||||
"EventReminder",
|
||||
"FederalDistrict",
|
||||
"Goal",
|
||||
"KnowledgeArticle",
|
||||
"KnowledgeCategory",
|
||||
"LoginToken",
|
||||
"PlatformHistory",
|
||||
"Region",
|
||||
"RegionCard",
|
||||
"RegionCardHistory",
|
||||
"RegionGoalValue",
|
||||
"RegionLink",
|
||||
"Request",
|
||||
"RequestComment",
|
||||
"Role",
|
||||
"Session",
|
||||
"Snapshot",
|
||||
"Task",
|
||||
"TaskComment",
|
||||
"User",
|
||||
"UserRegion",
|
||||
"UserRole",
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Модели советов от регионов."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class Advice(Base):
|
||||
__tablename__ = "advices"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
|
||||
goal_id: Mapped[int | None] = mapped_column(ForeignKey("goals.id"))
|
||||
author_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
body: Mapped[str] = mapped_column(Text)
|
||||
status: Mapped[str] = mapped_column(String(20), default="published")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Модели артефактов (файлов)."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import BigInteger, DateTime, ForeignKey, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class Artifact(Base):
|
||||
__tablename__ = "artifacts"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str | None] = mapped_column(String(300))
|
||||
file_key: Mapped[str] = mapped_column(String(500))
|
||||
file_name: Mapped[str] = mapped_column(String(300))
|
||||
mime_type: Mapped[str | None] = mapped_column(String(100))
|
||||
size_bytes: Mapped[int | None] = mapped_column(BigInteger)
|
||||
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
|
||||
goal_id: Mapped[int | None] = mapped_column(ForeignKey("goals.id"))
|
||||
task_id: Mapped[int | None] = mapped_column(ForeignKey("tasks.id"))
|
||||
uploaded_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Модели аудита и AI-ассистента."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, JSON, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.session import Base
|
||||
from app.models.enums import ActorType
|
||||
from app.models.types import enum_col
|
||||
|
||||
|
||||
class AuditLog(Base):
|
||||
__tablename__ = "audit_log"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
actor_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
actor_type: Mapped[ActorType] = mapped_column(enum_col(ActorType))
|
||||
action: Mapped[str] = mapped_column(String(100))
|
||||
entity_type: Mapped[str | None] = mapped_column(String(100))
|
||||
entity_id: Mapped[str | None] = mapped_column(String(100))
|
||||
payload: Mapped[dict | None] = mapped_column(JSON)
|
||||
ip: Mapped[str | None] = mapped_column(String(64))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class AssistantConfig(Base):
|
||||
__tablename__ = "assistant_configs"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
owner_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)
|
||||
enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
permissions: Mapped[dict | None] = mapped_column(JSON)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class AssistantAction(Base):
|
||||
__tablename__ = "assistant_actions"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
owner_user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
|
||||
action: Mapped[str] = mapped_column(String(100))
|
||||
entity_type: Mapped[str | None] = mapped_column(String(100))
|
||||
entity_id: Mapped[str | None] = mapped_column(String(100))
|
||||
payload: Mapped[dict | None] = mapped_column(JSON)
|
||||
status: Mapped[str] = mapped_column(String(20), default="ok")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,65 @@
|
||||
"""Модели рассылок."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
JSON,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.session import Base
|
||||
from app.models.enums import BroadcastStatus, BroadcastType, RecipientStatus
|
||||
from app.models.types import enum_col
|
||||
|
||||
|
||||
class BroadcastTemplate(Base):
|
||||
__tablename__ = "broadcast_templates"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
code: Mapped[str] = mapped_column(String(64), unique=True)
|
||||
name: Mapped[str] = mapped_column(String(200))
|
||||
type: Mapped[BroadcastType] = mapped_column(enum_col(BroadcastType))
|
||||
body_template: Mapped[str] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class Broadcast(Base):
|
||||
__tablename__ = "broadcasts"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
template_id: Mapped[int | None] = mapped_column(ForeignKey("broadcast_templates.id"))
|
||||
type: Mapped[BroadcastType] = mapped_column(enum_col(BroadcastType))
|
||||
subject: Mapped[str | None] = mapped_column(String(300))
|
||||
body: Mapped[str | None] = mapped_column(Text)
|
||||
audience_filter: Mapped[dict | None] = mapped_column(JSON)
|
||||
status: Mapped[BroadcastStatus] = mapped_column(
|
||||
enum_col(BroadcastStatus), default=BroadcastStatus.DRAFT
|
||||
)
|
||||
scheduled_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class BroadcastRecipient(Base):
|
||||
__tablename__ = "broadcast_recipients"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
broadcast_id: Mapped[int] = mapped_column(ForeignKey("broadcasts.id", ondelete="CASCADE"))
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||
status: Mapped[RecipientStatus] = mapped_column(
|
||||
enum_col(RecipientStatus), default=RecipientStatus.PENDING
|
||||
)
|
||||
sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
error: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
__table_args__ = (UniqueConstraint("broadcast_id", "user_id", name="uq_broadcast_user"),)
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Перечисления предметной области."""
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class MessengerType(StrEnum):
|
||||
TELEGRAM = "telegram"
|
||||
MAX = "max"
|
||||
|
||||
|
||||
class UserStatus(StrEnum):
|
||||
ACTIVE = "active"
|
||||
BLOCKED = "blocked"
|
||||
PENDING = "pending"
|
||||
|
||||
|
||||
class RoleCode(StrEnum):
|
||||
ADMIN = "admin"
|
||||
COORDINATOR = "coordinator"
|
||||
REGION_HEAD = "region_head"
|
||||
MEMBER = "member"
|
||||
DEPUTY = "deputy"
|
||||
|
||||
|
||||
class LoginTokenStatus(StrEnum):
|
||||
ACTIVE = "active"
|
||||
USED = "used"
|
||||
EXPIRED = "expired"
|
||||
REVOKED = "revoked"
|
||||
|
||||
|
||||
class SnapshotKind(StrEnum):
|
||||
POINT0 = "point0"
|
||||
CURRENT = "current"
|
||||
HISTORY = "history"
|
||||
|
||||
|
||||
class MemberType(StrEnum):
|
||||
MEMBER = "member"
|
||||
RESIDENT = "resident"
|
||||
|
||||
|
||||
class TaskStatus(StrEnum):
|
||||
NEW = "new"
|
||||
IN_PROGRESS = "in_progress"
|
||||
REVIEW = "review"
|
||||
DONE = "done"
|
||||
|
||||
|
||||
class TaskPriority(StrEnum):
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
|
||||
|
||||
class RequestType(StrEnum):
|
||||
JOIN = "join"
|
||||
ACCESS = "access"
|
||||
EVENT = "event"
|
||||
SUPPORT = "support"
|
||||
|
||||
|
||||
class RequestStatus(StrEnum):
|
||||
NEW = "new"
|
||||
IN_PROGRESS = "in_progress"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class BroadcastType(StrEnum):
|
||||
INFO = "info"
|
||||
EDUCATION = "education"
|
||||
RATING = "rating"
|
||||
|
||||
|
||||
class BroadcastStatus(StrEnum):
|
||||
DRAFT = "draft"
|
||||
SCHEDULED = "scheduled"
|
||||
SENDING = "sending"
|
||||
SENT = "sent"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class RecipientStatus(StrEnum):
|
||||
PENDING = "pending"
|
||||
SENT = "sent"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class ActorType(StrEnum):
|
||||
USER = "user"
|
||||
ASSISTANT = "assistant"
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
class LinkType(StrEnum):
|
||||
COMMUNITY = "community"
|
||||
CHAT = "chat"
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Модели календаря событий."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = "events"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(300))
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
starts_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
ends_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
location: Mapped[str | None] = mapped_column(String(300))
|
||||
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class EventReminder(Base):
|
||||
__tablename__ = "event_reminders"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
event_id: Mapped[int] = mapped_column(ForeignKey("events.id", ondelete="CASCADE"))
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||
remind_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
sent_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
@@ -0,0 +1,75 @@
|
||||
"""Модели целей, срезов и метрик."""
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Date,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Numeric,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db.session import Base
|
||||
from app.models.enums import SnapshotKind
|
||||
from app.models.types import enum_col
|
||||
|
||||
|
||||
class Goal(Base):
|
||||
__tablename__ = "goals"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(200))
|
||||
description: Mapped[str | None] = mapped_column(String(1000))
|
||||
target_value: Mapped[float | None] = mapped_column(Numeric(14, 2))
|
||||
unit: Mapped[str | None] = mapped_column(String(32))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
snapshots: Mapped[list["Snapshot"]] = relationship(back_populates="goal")
|
||||
|
||||
|
||||
class Snapshot(Base):
|
||||
__tablename__ = "snapshots"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
goal_id: Mapped[int] = mapped_column(ForeignKey("goals.id", ondelete="CASCADE"))
|
||||
snapshot_date: Mapped[date] = mapped_column(Date)
|
||||
kind: Mapped[SnapshotKind] = mapped_column(enum_col(SnapshotKind))
|
||||
source: Mapped[str | None] = mapped_column(String(32))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
goal: Mapped[Goal] = relationship(back_populates="snapshots")
|
||||
values: Mapped[list["RegionGoalValue"]] = relationship(back_populates="snapshot")
|
||||
|
||||
|
||||
class RegionGoalValue(Base):
|
||||
__tablename__ = "region_goal_values"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
snapshot_id: Mapped[int] = mapped_column(ForeignKey("snapshots.id", ondelete="CASCADE"))
|
||||
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
|
||||
value: Mapped[float] = mapped_column(Numeric(14, 2))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
__table_args__ = (UniqueConstraint("snapshot_id", "region_id", name="uq_snapshot_region"),)
|
||||
|
||||
snapshot: Mapped[Snapshot] = relationship(back_populates="values")
|
||||
|
||||
|
||||
class DataUpload(Base):
|
||||
__tablename__ = "data_uploads"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
goal_id: Mapped[int] = mapped_column(ForeignKey("goals.id"))
|
||||
file_key: Mapped[str] = mapped_column(String(500))
|
||||
uploaded_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
status: Mapped[str] = mapped_column(String(20), default="uploaded")
|
||||
processed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Модели истории платформы."""
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import Boolean, Date, DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class PlatformHistory(Base):
|
||||
__tablename__ = "platform_history"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(300))
|
||||
body: Mapped[str | None] = mapped_column(Text)
|
||||
event_date: Mapped[date | None] = mapped_column(Date)
|
||||
is_public: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Модели базы знаний."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.session import Base
|
||||
|
||||
|
||||
class KnowledgeCategory(Base):
|
||||
__tablename__ = "knowledge_categories"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(200), unique=True)
|
||||
|
||||
|
||||
class KnowledgeArticle(Base):
|
||||
__tablename__ = "knowledge_articles"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(300))
|
||||
body: Mapped[str] = mapped_column(Text)
|
||||
category_id: Mapped[int | None] = mapped_column(ForeignKey("knowledge_categories.id"))
|
||||
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Модели карточки региона."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.db.session import Base
|
||||
from app.models.enums import LinkType
|
||||
from app.models.types import enum_col
|
||||
|
||||
|
||||
class RegionCard(Base):
|
||||
__tablename__ = "region_cards"
|
||||
|
||||
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"), primary_key=True)
|
||||
population: Mapped[int | None] = mapped_column(Integer)
|
||||
head_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class RegionLink(Base):
|
||||
__tablename__ = "region_links"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
|
||||
type: Mapped[LinkType] = mapped_column(enum_col(LinkType))
|
||||
title: Mapped[str | None] = mapped_column(String(200))
|
||||
url: Mapped[str] = mapped_column(String(500))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
|
||||
class RegionCardHistory(Base):
|
||||
__tablename__ = "region_card_history"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
|
||||
field: Mapped[str] = mapped_column(String(100))
|
||||
old_value: Mapped[str | None] = mapped_column(String(1000))
|
||||
new_value: Mapped[str | None] = mapped_column(String(1000))
|
||||
changed_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
changed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Модели запросов (заявок)."""
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db.session import Base
|
||||
from app.models.enums import RequestStatus, RequestType
|
||||
from app.models.types import enum_col
|
||||
|
||||
|
||||
class Request(Base):
|
||||
__tablename__ = "requests"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
type: Mapped[RequestType] = mapped_column(enum_col(RequestType))
|
||||
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
|
||||
subject: Mapped[str | None] = mapped_column(String(300))
|
||||
body: Mapped[str | None] = mapped_column(Text)
|
||||
status: Mapped[RequestStatus] = mapped_column(
|
||||
enum_col(RequestStatus), default=RequestStatus.NEW
|
||||
)
|
||||
assigned_to: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
comments: Mapped[list["RequestComment"]] = relationship(back_populates="request")
|
||||
|
||||
|
||||
class RequestComment(Base):
|
||||
__tablename__ = "request_comments"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
request_id: Mapped[int] = mapped_column(ForeignKey("requests.id", ondelete="CASCADE"))
|
||||
author_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
body: Mapped[str] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
request: Mapped[Request] = relationship(back_populates="comments")
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Модели задач (трекера)."""
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import Date, DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db.session import Base
|
||||
from app.models.enums import RoleCode, TaskPriority, TaskStatus
|
||||
from app.models.types import enum_col
|
||||
|
||||
|
||||
class Task(Base):
|
||||
__tablename__ = "tasks"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
title: Mapped[str] = mapped_column(String(300))
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
due_date: Mapped[date | None] = mapped_column(Date)
|
||||
priority: Mapped[TaskPriority] = mapped_column(
|
||||
enum_col(TaskPriority), default=TaskPriority.MEDIUM
|
||||
)
|
||||
status: Mapped[TaskStatus] = mapped_column(
|
||||
enum_col(TaskStatus), default=TaskStatus.NEW
|
||||
)
|
||||
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
|
||||
assignee_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
assignee_role: Mapped[RoleCode | None] = mapped_column(enum_col(RoleCode))
|
||||
created_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
comments: Mapped[list["TaskComment"]] = relationship(back_populates="task")
|
||||
|
||||
|
||||
class TaskComment(Base):
|
||||
__tablename__ = "task_comments"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"))
|
||||
author_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
body: Mapped[str] = mapped_column(Text)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
task: Mapped[Task] = relationship(back_populates="comments")
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Вспомогательные типы для моделей."""
|
||||
from enum import Enum as PyEnum
|
||||
|
||||
from sqlalchemy import Enum as SAEnum
|
||||
|
||||
|
||||
def enum_col(enum_cls: type[PyEnum], length: int = 20) -> SAEnum:
|
||||
"""Enum-колонка, хранящая значения (value), а не имена членов.
|
||||
|
||||
Например, BroadcastType.INFO хранится как 'info', а не 'INFO'.
|
||||
"""
|
||||
return SAEnum(
|
||||
enum_cls,
|
||||
native_enum=False,
|
||||
length=length,
|
||||
values_callable=lambda obj: [e.value for e in obj],
|
||||
)
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Модели пользователей, ролей и доступа."""
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Date,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.db.session import Base
|
||||
from app.models.enums import LoginTokenStatus, MemberType, MessengerType, RoleCode, UserStatus
|
||||
from app.models.types import enum_col
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
full_name: Mapped[str | None] = mapped_column(String(200))
|
||||
messenger_type: Mapped[MessengerType] = mapped_column(enum_col(MessengerType))
|
||||
messenger_id: Mapped[str] = mapped_column(String(64))
|
||||
phone: Mapped[str | None] = mapped_column(String(32))
|
||||
status: Mapped[UserStatus] = mapped_column(
|
||||
enum_col(UserStatus), default=UserStatus.ACTIVE
|
||||
)
|
||||
member_type: Mapped[MemberType | None] = mapped_column(enum_col(MemberType))
|
||||
joined_at: Mapped[date | None] = mapped_column(Date)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
__table_args__ = (UniqueConstraint("messenger_type", "messenger_id", name="uq_user_messenger"),)
|
||||
|
||||
regions: Mapped[list["UserRegion"]] = relationship(back_populates="user")
|
||||
roles: Mapped[list["UserRole"]] = relationship(
|
||||
back_populates="user", foreign_keys="UserRole.user_id"
|
||||
)
|
||||
|
||||
|
||||
class UserRegion(Base):
|
||||
__tablename__ = "user_regions"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||
region_id: Mapped[int] = mapped_column(ForeignKey("regions.id", ondelete="CASCADE"))
|
||||
is_primary: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
__table_args__ = (UniqueConstraint("user_id", "region_id", name="uq_user_region"),)
|
||||
|
||||
user: Mapped[User] = relationship(back_populates="regions")
|
||||
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = "roles"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
code: Mapped[RoleCode] = mapped_column(enum_col(RoleCode), unique=True)
|
||||
name: Mapped[str] = mapped_column(String(100))
|
||||
|
||||
|
||||
class UserRole(Base):
|
||||
__tablename__ = "user_roles"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||
role_id: Mapped[int] = mapped_column(ForeignKey("roles.id"))
|
||||
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
|
||||
granted_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
granted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
__table_args__ = (UniqueConstraint("user_id", "role_id", "region_id", name="uq_user_role"),)
|
||||
|
||||
user: Mapped[User] = relationship(back_populates="roles", foreign_keys=[user_id])
|
||||
|
||||
|
||||
class LoginToken(Base):
|
||||
__tablename__ = "login_tokens"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
token: Mapped[str] = mapped_column(String(128), unique=True)
|
||||
user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
region_id: Mapped[int | None] = mapped_column(ForeignKey("regions.id"))
|
||||
issued_by: Mapped[int | None] = mapped_column(ForeignKey("users.id"))
|
||||
status: Mapped[LoginTokenStatus] = mapped_column(
|
||||
enum_col(LoginTokenStatus), default=LoginTokenStatus.ACTIVE
|
||||
)
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
used_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
|
||||
class Session(Base):
|
||||
__tablename__ = "sessions"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||
token_hash: Mapped[str] = mapped_column(String(128), unique=True)
|
||||
messenger_type: Mapped[MessengerType] = mapped_column(enum_col(MessengerType))
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
Reference in new issue
Block a user