feat: переименование доменной области

This commit is contained in:
Artem Tsyrulnikov
2025-12-14 12:21:35 +03:00
parent 3cb3da4dbe
commit f3e09a08eb
75 changed files with 1624 additions and 1417 deletions

View File

@@ -2,16 +2,29 @@ __all__ = (
'User',
'Workspace',
'WorkspaceUser',
'TargetChannel',
'ExternalChannel',
'WorkspaceInvite',
'WorkspaceInviteStatus',
'WorkspaceUserStatus',
'WorkspaceUserPermission',
'WorkspaceUserPermissionScope',
'PermissionKey',
'PermissionScopeType',
'Channel',
'ChannelVerificationStatus',
'Project',
'ProjectStatus',
'PurchasePlan',
'PurchasePlanChannel',
'PurchasePlanStatus',
'PurchasePlanChannelStatus',
'Creative',
'Placement',
'Subscription',
'Subscriber',
'PlacementViewsHistory',
'TelegramState',
'TelegramStateEnum',
'TargetChannelStatus',
'ChannelNotFound',
'ProjectNotFound',
'CreativeStatus',
'PlacementStatus',
'SubscriptionStatus',
@@ -24,40 +37,57 @@ __all__ = (
'LoginTokenNotFound',
'LoginTokenExpired',
'LoginTokenAlreadyUsed',
'TargetChannelNotFound',
'ProjectNotFound',
'PurchasePlanNotFound',
'PurchasePlanChannelNotFound',
'ChannelNotFound',
'ChannelAlreadyExists',
'ChannelNoAdminRights',
'ExternalChannelNotFound',
'ExternalChannelAlreadyExists',
'CreativeNotFound',
'CreativeInUse',
'PlacementNotFound',
)
from .channel import Channel, ChannelVerificationStatus
from .creative import Creative, CreativeStatus
from .error import (
ChannelAlreadyExists,
ChannelNoAdminRights,
ChannelNotFound,
CreativeInUse,
CreativeNotFound,
ExternalChannelAlreadyExists,
ExternalChannelNotFound,
LoginTokenAlreadyUsed,
LoginTokenExpired,
LoginTokenNotFound,
PlacementNotFound,
TargetChannelNotFound,
ProjectNotFound,
PurchasePlanChannelNotFound,
PurchasePlanNotFound,
UserNotFound,
WorkspaceAccessDenied,
WorkspaceNotFound,
)
from .external_channel import ExternalChannel
from .login_token import LoginToken
from .placement import InviteLinkType, Placement, PlacementStatus, PostViewsAvailability
from .placement_views_history import PlacementViewsHistory
from .subscriber import Subscriber
from .project import Project, ProjectStatus
from .purchase_plan import (
PurchasePlan,
PurchasePlanChannel,
PurchasePlanChannelStatus,
PurchasePlanStatus,
)
from .subscription import Subscription, SubscriptionStatus
from .target_channel import TargetChannel, TargetChannelStatus
from .telegram_state import TelegramState, TelegramStateEnum
from .user import User
from .workspace import Workspace, WorkspaceUser
from .workspace import (
PermissionKey,
PermissionScopeType,
Workspace,
WorkspaceInvite,
WorkspaceInviteStatus,
WorkspaceUser,
WorkspaceUserPermission,
WorkspaceUserPermissionScope,
WorkspaceUserStatus,
)

34
src/domain/channel.py Normal file
View File

@@ -0,0 +1,34 @@
import enum
import uuid
from typing import TYPE_CHECKING
from tortoise import fields
from .base import TimestampedModel
if TYPE_CHECKING:
from .workspace import Workspace
class ChannelVerificationStatus(str, enum.Enum):
UNVERIFIED = 'unverified'
VERIFIED = 'verified'
FAILED = 'failed'
class Channel(TimestampedModel):
id = fields.UUIDField(pk=True)
telegram_id = fields.BigIntField(null=True, unique=True, index=True)
username = fields.CharField(max_length=255, null=True, index=True)
title = fields.CharField(max_length=255, null=True)
verification_status = fields.CharEnumField(ChannelVerificationStatus, default=ChannelVerificationStatus.UNVERIFIED)
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
'models.Workspace', related_name='channels', on_delete=fields.CASCADE, index=True
)
if TYPE_CHECKING:
workspace_id: uuid.UUID
class Meta:
table = 'channel'

View File

@@ -7,7 +7,7 @@ from tortoise import fields
from .base import TimestampedModel
if TYPE_CHECKING:
from .target_channel import TargetChannel
from .project import Project
from .workspace import Workspace
@@ -26,13 +26,13 @@ class Creative(TimestampedModel):
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
'models.Workspace', related_name='creatives', on_delete=fields.CASCADE, index=True
)
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
'models.TargetChannel', related_name='creatives', on_delete=fields.CASCADE, index=True
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
'models.Project', related_name='creatives', on_delete=fields.CASCADE, index=True
)
if TYPE_CHECKING:
workspace_id: UUID
target_channel_id: UUID
project_id: UUID
class Meta:
table = 'creative'

View File

@@ -21,10 +21,28 @@ def LoginTokenAlreadyUsed() -> HTTPException:
return HTTPException(status.HTTP_400_BAD_REQUEST, 'Login token has already been used')
def TargetChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException:
def ChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException:
if channel_id is None:
return HTTPException(status.HTTP_404_NOT_FOUND, 'Target channel not found')
return HTTPException(status.HTTP_404_NOT_FOUND, f'Target channel {channel_id} not found')
return HTTPException(status.HTTP_404_NOT_FOUND, 'Channel not found')
return HTTPException(status.HTTP_404_NOT_FOUND, f'Channel {channel_id} not found')
def ProjectNotFound(project_id: uuid.UUID | None = None) -> HTTPException:
if project_id is None:
return HTTPException(status.HTTP_404_NOT_FOUND, 'Project not found')
return HTTPException(status.HTTP_404_NOT_FOUND, f'Project {project_id} not found')
def PurchasePlanNotFound(plan_id: uuid.UUID | None = None) -> HTTPException:
if plan_id is None:
return HTTPException(status.HTTP_404_NOT_FOUND, 'Purchase plan not found')
return HTTPException(status.HTTP_404_NOT_FOUND, f'Purchase plan {plan_id} not found')
def PurchasePlanChannelNotFound(plan_channel_id: uuid.UUID | None = None) -> HTTPException:
if plan_channel_id is None:
return HTTPException(status.HTTP_404_NOT_FOUND, 'Purchase plan channel not found')
return HTTPException(status.HTTP_404_NOT_FOUND, f'Purchase plan channel {plan_channel_id} not found')
def ChannelAlreadyExists(telegram_id: int) -> HTTPException:
@@ -37,16 +55,6 @@ def ChannelNoAdminRights() -> HTTPException:
)
def ExternalChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException:
if channel_id is None:
return HTTPException(status.HTTP_404_NOT_FOUND, 'External channel not found')
return HTTPException(status.HTTP_404_NOT_FOUND, f'External channel {channel_id} not found')
def ExternalChannelAlreadyExists(telegram_id: int) -> HTTPException:
return HTTPException(status.HTTP_409_CONFLICT, f'External channel {telegram_id} already exists')
def CreativeNotFound(creative_id: uuid.UUID | None = None) -> HTTPException:
if creative_id is None:
return HTTPException(status.HTTP_404_NOT_FOUND, 'Creative not found')

View File

@@ -1,30 +0,0 @@
from typing import TYPE_CHECKING
from uuid import UUID
from tortoise import fields
from .base import TimestampedModel
if TYPE_CHECKING:
from .target_channel import TargetChannel
from .workspace import Workspace
class ExternalChannel(TimestampedModel):
id = fields.UUIDField(pk=True)
telegram_id = fields.BigIntField(index=True)
title = fields.CharField(max_length=255)
username = fields.CharField(max_length=255, null=True, index=True)
description = fields.TextField(null=True)
subscribers_count = fields.IntField(null=True)
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
'models.Workspace', related_name='external_channels', on_delete=fields.CASCADE, index=True
)
if TYPE_CHECKING:
target_channels: fields.ManyToManyRelation['TargetChannel']
workspace_id: UUID
class Meta:
table = 'external_channel'

View File

@@ -7,9 +7,9 @@ from tortoise import fields
from .base import TimestampedModel
if TYPE_CHECKING:
from .channel import Channel
from .creative import Creative
from .external_channel import ExternalChannel
from .target_channel import TargetChannel
from .project import Project
from .workspace import Workspace
@@ -50,11 +50,11 @@ class Placement(TimestampedModel):
views_availability = fields.CharEnumField(PostViewsAvailability, default=PostViewsAvailability.UNKNOWN)
last_views_fetch_at = fields.DatetimeField(null=True)
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
'models.TargetChannel', related_name='placements', on_delete=fields.CASCADE, index=True
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
'models.Project', related_name='placements', on_delete=fields.CASCADE, index=True
)
external_channel: fields.ForeignKeyRelation['ExternalChannel'] = fields.ForeignKeyField(
'models.ExternalChannel', related_name='placements', on_delete=fields.CASCADE, index=True
placement_channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
'models.Channel', related_name='placements', on_delete=fields.CASCADE, index=True
)
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True
@@ -64,8 +64,8 @@ class Placement(TimestampedModel):
)
if TYPE_CHECKING:
target_channel_id: UUID
external_channel_id: UUID
project_id: UUID
placement_channel_id: UUID
creative_id: UUID
workspace_id: UUID

37
src/domain/project.py Normal file
View File

@@ -0,0 +1,37 @@
import enum
from typing import TYPE_CHECKING
from uuid import UUID
from tortoise import fields
from .base import TimestampedModel
if TYPE_CHECKING:
from .channel import Channel
from .workspace import Workspace
class ProjectStatus(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
ARCHIVED = 'archived'
class Project(TimestampedModel):
id = fields.UUIDField(pk=True)
status = fields.CharEnumField(ProjectStatus, default=ProjectStatus.ACTIVE)
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
'models.Workspace', related_name='projects', on_delete=fields.CASCADE, index=True
)
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
'models.Channel', related_name='projects', on_delete=fields.CASCADE, index=True
)
if TYPE_CHECKING:
workspace_id: UUID
channel_id: UUID
class Meta:
table = 'project'
unique_together = (('workspace_id', 'channel_id'),)

View File

@@ -0,0 +1,63 @@
import enum
import uuid
from typing import TYPE_CHECKING
from tortoise import fields
from .base import TimestampedModel
if TYPE_CHECKING:
from .channel import Channel
from .project import Project
from .workspace import Workspace
class PurchasePlanStatus(str, enum.Enum):
ACTIVE = 'active'
ARCHIVED = 'archived'
class PurchasePlanChannelStatus(str, enum.Enum):
PLANNED = 'planned'
APPROVED = 'approved'
REJECTED = 'rejected'
IN_PROGRESS = 'in_progress'
COMPLETED = 'completed'
class PurchasePlan(TimestampedModel):
id = fields.UUIDField(pk=True)
status = fields.CharEnumField(PurchasePlanStatus, default=PurchasePlanStatus.ACTIVE)
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
'models.Workspace', related_name='purchase_plans', on_delete=fields.CASCADE, index=True
)
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
'models.Project', related_name='purchase_plans', on_delete=fields.CASCADE, index=True
)
class Meta:
table = 'purchase_plan'
indexes = (('project', 'status'),)
class PurchasePlanChannel(TimestampedModel):
id = fields.UUIDField(pk=True)
status = fields.CharEnumField(PurchasePlanChannelStatus, default=PurchasePlanChannelStatus.PLANNED)
planned_cost = fields.FloatField(null=True)
comment = fields.TextField(null=True)
purchase_plan: fields.ForeignKeyRelation['PurchasePlan'] = fields.ForeignKeyField(
'models.PurchasePlan', related_name='channels', on_delete=fields.CASCADE, index=True
)
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
'models.Channel', related_name='purchase_plan_channels', on_delete=fields.CASCADE, index=True
)
if TYPE_CHECKING:
purchase_plan_id: uuid.UUID
channel_id: uuid.UUID
class Meta:
table = 'purchase_plan_channel'
unique_together = (('purchase_plan_id', 'channel_id'),)

View File

@@ -1,14 +0,0 @@
from tortoise import fields
from .base import TimestampedModel
class Subscriber(TimestampedModel):
id = fields.UUIDField(pk=True)
telegram_id = fields.BigIntField(unique=True, index=True)
username = fields.CharField(max_length=255, null=True)
first_name = fields.CharField(max_length=255, null=True)
last_name = fields.CharField(max_length=255, null=True)
class Meta:
table = 'subscriber'

View File

@@ -8,8 +8,7 @@ from .base import TimestampedModel
if TYPE_CHECKING:
from .placement import Placement
from .subscriber import Subscriber
from .target_channel import TargetChannel
from .user import User
class SubscriptionStatus(str, enum.Enum):
@@ -26,17 +25,13 @@ class Subscription(TimestampedModel):
placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField(
'models.Placement', related_name='subscriptions', on_delete=fields.CASCADE, index=True
)
subscriber: fields.ForeignKeyRelation['Subscriber'] = fields.ForeignKeyField(
'models.Subscriber', related_name='subscriptions', on_delete=fields.CASCADE, index=True
)
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
'models.TargetChannel', related_name='subscriptions', on_delete=fields.CASCADE, index=True
subscriber: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
'models.User', related_name='subscriptions', on_delete=fields.CASCADE, index=True
)
if TYPE_CHECKING:
placement_id: UUID
subscriber_id: UUID
target_channel_id: UUID
class Meta:
table = 'subscription'

View File

@@ -1,43 +0,0 @@
import enum
from typing import TYPE_CHECKING
from uuid import UUID
from tortoise import fields
from .base import TimestampedModel
if TYPE_CHECKING:
from .external_channel import ExternalChannel
from .workspace import Workspace
class TargetChannelStatus(str, enum.Enum):
ACTIVE = 'active'
INACTIVE = 'inactive'
ARCHIVED = 'archived'
class TargetChannel(TimestampedModel):
id = fields.UUIDField(pk=True)
telegram_id = fields.BigIntField(unique=True, index=True)
title = fields.CharField(max_length=255)
username = fields.CharField(max_length=255, null=True, index=True)
status = fields.CharEnumField(TargetChannelStatus, default=TargetChannelStatus.ACTIVE)
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
'models.Workspace', related_name='target_channels', on_delete=fields.CASCADE, index=True
)
external_channels: fields.ManyToManyRelation['ExternalChannel'] = fields.ManyToManyField(
'models.ExternalChannel',
related_name='target_channels',
through='target_external_channel',
forward_key='external_channel_id',
backward_key='target_channel_id',
)
if TYPE_CHECKING:
workspace_id: UUID
class Meta:
table = 'target_channel'

View File

@@ -10,7 +10,7 @@ class TelegramStateEnum(str, enum.Enum):
CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel'
CREATIVE_WAITING_NAME = 'creative_waiting_name'
CREATIVE_WAITING_TEXT = 'creative_waiting_text'
TARGET_CHANNEL_WAITING_WORKSPACE = 'target_channel_waiting_workspace'
PROJECT_WAITING_WORKSPACE = 'project_waiting_workspace'
class TelegramState(TimestampedModel):

View File

@@ -7,6 +7,8 @@ class User(TimestampedModel):
id = fields.UUIDField(pk=True)
telegram_id = fields.BigIntField(unique=True, index=True)
username = fields.CharField(max_length=255, null=True)
first_name = fields.CharField(max_length=255, null=True)
last_name = fields.CharField(max_length=255, null=True)
class Meta:
table = 'user'

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import enum
import uuid
from typing import TYPE_CHECKING
@@ -19,8 +20,15 @@ class Workspace(TimestampedModel):
table = 'workspace'
class WorkspaceUserStatus(str, enum.Enum):
ACTIVE = 'active'
INVITED = 'invited'
REMOVED = 'removed'
class WorkspaceUser(TimestampedModel):
id = fields.UUIDField(pk=True)
status = fields.CharEnumField(WorkspaceUserStatus, default=WorkspaceUserStatus.ACTIVE)
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
'models.Workspace', related_name='workspace_users', on_delete=fields.CASCADE, index=True
@@ -36,3 +44,65 @@ class WorkspaceUser(TimestampedModel):
class Meta:
table = 'workspace_user'
unique_together = (('workspace_id', 'user_id'),)
class WorkspaceInviteStatus(str, enum.Enum):
PENDING = 'pending'
ACCEPTED = 'accepted'
REVOKED = 'revoked'
class WorkspaceInvite(TimestampedModel):
id = fields.UUIDField(pk=True)
status = fields.CharEnumField(WorkspaceInviteStatus, default=WorkspaceInviteStatus.PENDING)
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
'models.Workspace', related_name='invites', on_delete=fields.CASCADE, index=True
)
invited_by: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
'models.User', related_name='sent_invites', on_delete=fields.CASCADE, index=True
)
user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
'models.User', related_name='workspace_invites', on_delete=fields.CASCADE, index=True
)
class Meta:
table = 'workspace_invite'
unique_together = (('workspace_id', 'user_id'),)
class PermissionKey(str, enum.Enum):
MANAGE_PROJECTS = 'manage_projects'
MANAGE_PLACEMENTS = 'manage_placements'
VIEW_ANALYTICS = 'view_analytics'
class PermissionScopeType(str, enum.Enum):
WORKSPACE = 'workspace'
PROJECT = 'project'
class WorkspaceUserPermission(TimestampedModel):
workspace_user: fields.ForeignKeyRelation[WorkspaceUser] = fields.ForeignKeyField(
'models.WorkspaceUser', related_name='permissions', on_delete=fields.CASCADE, index=True
)
permission = fields.CharEnumField(PermissionKey)
class Meta:
table = 'workspace_user_permission'
unique_together = (('workspace_user_id', 'permission'),)
class WorkspaceUserPermissionScope(TimestampedModel):
id = fields.UUIDField(pk=True)
permission = fields.CharEnumField(PermissionKey)
scope_type = fields.CharEnumField(PermissionScopeType)
scope_id = fields.UUIDField()
workspace_user: fields.ForeignKeyRelation[WorkspaceUser] = fields.ForeignKeyField(
'models.WorkspaceUser', related_name='permission_scopes', on_delete=fields.CASCADE, index=True
)
class Meta:
table = 'workspace_user_permission_scope'
unique_together = (('workspace_user_id', 'permission', 'scope_type', 'scope_id'),)