feat: add workspace
This commit is contained in:
@@ -22,6 +22,7 @@ from .creative.get_creative import get_creative
|
||||
from .creative.get_creatives import get_creatives
|
||||
from .creative.handle_creative_callback import handle_creative_callback
|
||||
from .creative.handle_creative_text_input import handle_creative_text_input
|
||||
from .creative.handle_creative_workspace_callback import handle_creative_workspace_callback
|
||||
from .creative.start_creative_creation import start_creative_creation
|
||||
from .creative.update_creative import update_creative
|
||||
from .external_channel.create_external_channel import create_external_channel
|
||||
@@ -41,7 +42,10 @@ from .target_channel.disconnect_target_chan_by_tg_id import disconnect_target_ch
|
||||
from .target_channel.get_user_target_chans import get_user_target_chans
|
||||
from .target_channel.update_target_chan_permissions import update_target_chan_permissions
|
||||
from .views.get_views_history import get_views_history
|
||||
from .views.update_views_manually import update_views_manually
|
||||
from .workspace.create_workspace import create_workspace
|
||||
from .workspace.delete_workspace import delete_workspace
|
||||
from .workspace.get_workspaces import get_workspaces
|
||||
from .workspace.update_workspace import update_workspace
|
||||
|
||||
|
||||
class Database(typing.Protocol):
|
||||
@@ -51,6 +55,24 @@ class Database(typing.Protocol):
|
||||
|
||||
async def create_user(self, user: domain.User) -> None: ...
|
||||
|
||||
async def create_workspace(self, workspace: domain.Workspace) -> None: ...
|
||||
|
||||
async def update_workspace(self, workspace: domain.Workspace) -> None: ...
|
||||
|
||||
async def delete_workspace(self, workspace_id: UUID) -> None: ...
|
||||
|
||||
async def add_user_to_workspace(self, workspace_id: UUID, user_id: UUID) -> None: ...
|
||||
|
||||
async def get_user_workspaces(self, user_id: UUID) -> list[domain.WorkspaceUser]: ...
|
||||
|
||||
async def get_workspace(self, workspace_id: UUID) -> domain.Workspace | None: ...
|
||||
|
||||
async def get_workspace_for_user(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace | None: ...
|
||||
|
||||
async def get_default_workspace_for_user(self, user_id: UUID) -> domain.Workspace | None: ...
|
||||
|
||||
async def get_workspace_membership(self, workspace_id: UUID, user_id: UUID) -> domain.WorkspaceUser | None: ...
|
||||
|
||||
async def create_login_token(self, login_token: domain.LoginToken) -> None: ...
|
||||
|
||||
async def create_creative(self, creative: domain.Creative) -> None: ...
|
||||
@@ -60,24 +82,28 @@ class Database(typing.Protocol):
|
||||
async def mark_token_as_used(self, token: str) -> None: ...
|
||||
|
||||
async def get_target_channel(
|
||||
self, user_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
||||
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
||||
) -> domain.TargetChannel | None: ...
|
||||
|
||||
async def get_target_channel_for_user_by_telegram(
|
||||
self, user_id: UUID, telegram_id: int
|
||||
) -> domain.TargetChannel | None: ...
|
||||
|
||||
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None: ...
|
||||
|
||||
async def get_user_target_channels(self, user_id: UUID) -> list[domain.TargetChannel]: ...
|
||||
async def get_workspace_target_channels(self, workspace_id: UUID) -> list[domain.TargetChannel]: ...
|
||||
|
||||
async def update_target_channel(self, channel: domain.TargetChannel) -> None: ...
|
||||
|
||||
async def get_external_channel(
|
||||
self, user_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
||||
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
||||
) -> domain.ExternalChannel | None: ...
|
||||
|
||||
async def create_external_channel(self, channel: domain.ExternalChannel) -> None: ...
|
||||
|
||||
async def get_external_channels_for_target(self, target_channel_id: UUID) -> list[domain.ExternalChannel]: ...
|
||||
|
||||
async def get_user_external_channels(self, user_id: UUID) -> list[domain.ExternalChannel]: ...
|
||||
async def get_workspace_external_channels(self, workspace_id: UUID) -> list[domain.ExternalChannel]: ...
|
||||
|
||||
async def add_external_channel_to_targets(
|
||||
self, external_channel: domain.ExternalChannel, target_channel_ids: list[UUID]
|
||||
@@ -89,25 +115,25 @@ class Database(typing.Protocol):
|
||||
|
||||
async def update_external_channel(self, channel: domain.ExternalChannel) -> None: ...
|
||||
|
||||
async def get_creative(self, user_id: UUID, creative_id: UUID) -> domain.Creative | None: ...
|
||||
async def get_creative(self, workspace_id: UUID, creative_id: UUID) -> domain.Creative | None: ...
|
||||
|
||||
async def update_creative(self, creative: domain.Creative) -> None: ...
|
||||
|
||||
async def get_user_creatives(
|
||||
self, user_id: UUID, target_channel_id: UUID | None = None, include_archived: bool = False
|
||||
async def get_workspace_creatives(
|
||||
self, workspace_id: UUID, target_channel_id: UUID | None = None, include_archived: bool = False
|
||||
) -> list[domain.Creative]: ...
|
||||
|
||||
async def delete_creative(self, creative_id: UUID) -> None: ...
|
||||
|
||||
async def get_placement(self, user_id: UUID, placement_id: UUID) -> domain.Placement | None: ...
|
||||
async def get_placement(self, workspace_id: UUID, placement_id: UUID) -> domain.Placement | None: ...
|
||||
|
||||
async def create_placement(self, placement: domain.Placement) -> None: ...
|
||||
|
||||
async def update_placement(self, placement: domain.Placement) -> None: ...
|
||||
|
||||
async def get_user_placements(
|
||||
async def get_workspace_placements(
|
||||
self,
|
||||
user_id: UUID,
|
||||
workspace_id: UUID,
|
||||
target_channel_id: UUID | None = None,
|
||||
external_channel_id: UUID | None = None,
|
||||
creative_id: UUID | None = None,
|
||||
@@ -181,6 +207,13 @@ class Usecase:
|
||||
telegram_bot: TelegramBotWriter
|
||||
jwt_encoder: JWTEncoder
|
||||
|
||||
async def ensure_workspace_access(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace:
|
||||
workspace = await self.database.get_workspace_for_user(workspace_id, user_id)
|
||||
if not workspace:
|
||||
raise domain.WorkspaceNotFound(workspace_id)
|
||||
|
||||
return workspace
|
||||
|
||||
validate_login_token = validate_login_token
|
||||
telegram_login = telegram_login
|
||||
telegram_start = telegram_start
|
||||
@@ -201,6 +234,7 @@ class Usecase:
|
||||
delete_creative = delete_creative
|
||||
start_creative_creation = start_creative_creation
|
||||
handle_creative_callback = handle_creative_callback
|
||||
handle_creative_workspace_callback = handle_creative_workspace_callback
|
||||
handle_creative_text_input = handle_creative_text_input
|
||||
get_placements = get_placements
|
||||
get_placement = get_placement
|
||||
@@ -210,8 +244,11 @@ class Usecase:
|
||||
handle_subscription = handle_subscription
|
||||
handle_unsubscription = handle_unsubscription
|
||||
get_views_history = get_views_history
|
||||
update_views_manually = update_views_manually
|
||||
get_placements_analytics = get_placements_analytics
|
||||
get_creatives_analytics = get_creatives_analytics
|
||||
get_external_channels_analytics = get_external_channels_analytics
|
||||
get_spending_analytics = get_spending_analytics
|
||||
get_workspaces = get_workspaces
|
||||
create_workspace = create_workspace
|
||||
update_workspace = update_workspace
|
||||
delete_workspace = delete_workspace
|
||||
|
||||
@@ -14,13 +14,19 @@ log = logging.getLogger(__name__)
|
||||
async def get_creatives_analytics(
|
||||
self: 'Usecase', input: dto.GetCreativesAnalyticsInput
|
||||
) -> dto.GetCreativesAnalyticsOutput:
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
if input.target_channel_id:
|
||||
target_channel = await self.database.get_target_channel(input.user_id, input.target_channel_id)
|
||||
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
||||
if not target_channel:
|
||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||
|
||||
creatives = await self.database.get_user_creatives(input.user_id, input.target_channel_id, include_archived=False)
|
||||
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
|
||||
creatives = await self.database.get_workspace_creatives(
|
||||
input.workspace_id, input.target_channel_id, include_archived=False
|
||||
)
|
||||
placements = await self.database.get_workspace_placements(
|
||||
input.workspace_id, input.target_channel_id, include_archived=False
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class CreativeStats:
|
||||
|
||||
@@ -14,15 +14,19 @@ log = logging.getLogger(__name__)
|
||||
async def get_external_channels_analytics(
|
||||
self: 'Usecase', input: dto.GetExternalChannelsAnalyticsInput
|
||||
) -> dto.GetExternalChannelsAnalyticsOutput:
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
if input.target_channel_id:
|
||||
target_channel = await self.database.get_target_channel(input.user_id, input.target_channel_id)
|
||||
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
||||
if not target_channel:
|
||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||
external_channels = await self.database.get_external_channels_for_target(input.target_channel_id)
|
||||
else:
|
||||
external_channels = await self.database.get_user_external_channels(input.user_id)
|
||||
external_channels = await self.database.get_workspace_external_channels(input.workspace_id)
|
||||
|
||||
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
|
||||
placements = await self.database.get_workspace_placements(
|
||||
input.workspace_id, input.target_channel_id, include_archived=False
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class ChannelStats:
|
||||
|
||||
@@ -24,12 +24,16 @@ def _calculate_cpm(cost: float | None, views: int | None) -> float | None:
|
||||
async def get_placements_analytics(
|
||||
self: 'Usecase', input: dto.GetPlacementsAnalyticsInput
|
||||
) -> dto.GetPlacementsAnalyticsOutput:
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
if input.target_channel_id:
|
||||
target_channel = await self.database.get_target_channel(input.user_id, input.target_channel_id)
|
||||
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
||||
if not target_channel:
|
||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||
|
||||
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
|
||||
placements = await self.database.get_workspace_placements(
|
||||
input.workspace_id, input.target_channel_id, include_archived=False
|
||||
)
|
||||
|
||||
result = [
|
||||
dto.PlacementAnalyticsOutput(
|
||||
|
||||
@@ -33,12 +33,16 @@ def _format_period(dt: 'datetime.datetime', grouping: dto.DateGrouping) -> str:
|
||||
async def get_spending_analytics(
|
||||
self: 'Usecase', input: dto.GetSpendingAnalyticsInput
|
||||
) -> dto.GetSpendingAnalyticsOutput:
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
if input.target_channel_id:
|
||||
target_channel = await self.database.get_target_channel(input.user_id, input.target_channel_id)
|
||||
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
||||
if not target_channel:
|
||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||
|
||||
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
|
||||
placements = await self.database.get_workspace_placements(
|
||||
input.workspace_id, input.target_channel_id, include_archived=False
|
||||
)
|
||||
|
||||
filtered = [
|
||||
p
|
||||
|
||||
@@ -10,8 +10,12 @@ if TYPE_CHECKING:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_creative(self: 'Usecase', input: dto.CreateCreativeInput, user_id: uuid.UUID) -> dto.CreativeOutput:
|
||||
target_channel = await self.database.get_target_channel(user_id, channel_id=input.target_channel_id)
|
||||
async def create_creative(
|
||||
self: 'Usecase', input: dto.CreateCreativeInput, user_id: uuid.UUID, workspace_id: uuid.UUID
|
||||
) -> dto.CreativeOutput:
|
||||
await self.ensure_workspace_access(workspace_id, user_id)
|
||||
|
||||
target_channel = await self.database.get_target_channel(workspace_id, channel_id=input.target_channel_id)
|
||||
if not target_channel:
|
||||
log.warning('User %s attempted to create creative for unavailable target %s', user_id, input.target_channel_id)
|
||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||
@@ -21,9 +25,8 @@ async def create_creative(self: 'Usecase', input: dto.CreateCreativeInput, user_
|
||||
text=input.text,
|
||||
status=domain.CreativeStatus.ACTIVE,
|
||||
target_channel_id=target_channel.id,
|
||||
user_id=user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
await self.database.create_creative(creative)
|
||||
|
||||
return dto.CreativeOutput(
|
||||
|
||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> None:
|
||||
creative = await self.database.get_creative(input.user_id, input.creative_id)
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
creative = await self.database.get_creative(input.workspace_id, input.creative_id)
|
||||
if not creative:
|
||||
log.warning('User %s attempted to delete unavailable creative %s', input.user_id, input.creative_id)
|
||||
raise domain.CreativeNotFound(input.creative_id)
|
||||
|
||||
@@ -10,9 +10,10 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.CreativeOutput:
|
||||
creative = await self.database.get_creative(input.user_id, input.creative_id)
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
creative = await self.database.get_creative(input.workspace_id, input.creative_id)
|
||||
if not creative:
|
||||
log.warning('User %s attempted to access unavailable creative %s', input.user_id, input.creative_id)
|
||||
raise domain.CreativeNotFound(input.creative_id)
|
||||
|
||||
return dto.CreativeOutput(
|
||||
|
||||
@@ -7,7 +7,11 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.GetCreativesOutput:
|
||||
creatives = await self.database.get_user_creatives(input.user_id, input.target_channel_id, input.include_archived)
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
creatives = await self.database.get_workspace_creatives(
|
||||
input.workspace_id, input.target_channel_id, input.include_archived
|
||||
)
|
||||
|
||||
return dto.GetCreativesOutput(
|
||||
creatives=[
|
||||
|
||||
@@ -11,9 +11,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def update_creative(
|
||||
self: 'Usecase', creative_id: uuid.UUID, input: dto.UpdateCreativeInput, user_id: uuid.UUID
|
||||
self: 'Usecase',
|
||||
creative_id: uuid.UUID,
|
||||
input: dto.UpdateCreativeInput,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
) -> dto.CreativeOutput:
|
||||
creative = await self.database.get_creative(user_id, creative_id)
|
||||
await self.ensure_workspace_access(workspace_id, user_id)
|
||||
|
||||
creative = await self.database.get_creative(workspace_id, creative_id)
|
||||
if not creative:
|
||||
log.warning('User %s attempted to update unavailable creative %s', user_id, creative_id)
|
||||
raise domain.CreativeNotFound(creative_id)
|
||||
|
||||
@@ -11,17 +11,18 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_external_channel(
|
||||
self: 'Usecase', input: dto.CreateExternalChannelInput, user_id: uuid.UUID
|
||||
self: 'Usecase', input: dto.CreateExternalChannelInput, user_id: uuid.UUID, workspace_id: uuid.UUID
|
||||
) -> dto.ExternalChannelOutput:
|
||||
existing_channel = await self.database.get_external_channel(user_id, telegram_id=input.telegram_id)
|
||||
await self.ensure_workspace_access(workspace_id, user_id)
|
||||
|
||||
existing_channel = await self.database.get_external_channel(workspace_id, telegram_id=input.telegram_id)
|
||||
if existing_channel:
|
||||
raise domain.ExternalChannelAlreadyExists(input.telegram_id)
|
||||
|
||||
for target_id in input.target_channel_ids:
|
||||
target_channel = await self.database.get_target_channel(user_id, channel_id=target_id)
|
||||
target_channel = await self.database.get_target_channel(workspace_id, channel_id=target_id)
|
||||
|
||||
if not target_channel:
|
||||
log.warning('User %s attempted to add external channel to unavailable target %s', user_id, target_id)
|
||||
raise domain.TargetChannelNotFound(target_id)
|
||||
|
||||
async with self.database.transaction():
|
||||
@@ -31,7 +32,7 @@ async def create_external_channel(
|
||||
username=input.username,
|
||||
description=input.description,
|
||||
subscribers_count=input.subscribers_count,
|
||||
user_id=user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
await self.database.create_external_channel(channel)
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def delete_external_channel(self: 'Usecase', input: dto.DeleteExternalChannelInput) -> None:
|
||||
channel = await self.database.get_external_channel(input.user_id, channel_id=input.channel_id)
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
channel = await self.database.get_external_channel(input.workspace_id, channel_id=input.channel_id)
|
||||
if not channel:
|
||||
log.warning('User %s attempted to delete unavailable external channel %s', input.user_id, input.channel_id)
|
||||
raise domain.ExternalChannelNotFound(input.channel_id)
|
||||
|
||||
await self.database.delete_external_channel(input.channel_id)
|
||||
|
||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_external_channels(self: 'Usecase', input: dto.GetExternalChannelsInput) -> dto.GetExternalChannelsOutput:
|
||||
target_channel = await self.database.get_target_channel(input.user_id, channel_id=input.target_channel_id)
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
target_channel = await self.database.get_target_channel(input.workspace_id, channel_id=input.target_channel_id)
|
||||
if not target_channel:
|
||||
log.warning('Target channel %s not found or not accessible for user %s', input.target_channel_id, input.user_id)
|
||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||
|
||||
@@ -11,9 +11,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def update_external_channel(
|
||||
self: 'Usecase', channel_id: uuid.UUID, input: dto.UpdateExternalChannelInput, user_id: uuid.UUID
|
||||
self: 'Usecase',
|
||||
channel_id: uuid.UUID,
|
||||
input: dto.UpdateExternalChannelInput,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
) -> dto.ExternalChannelOutput:
|
||||
external_channel = await self.database.get_external_channel(user_id, channel_id=channel_id)
|
||||
await self.ensure_workspace_access(workspace_id, user_id)
|
||||
|
||||
external_channel = await self.database.get_external_channel(workspace_id, channel_id=channel_id)
|
||||
if not external_channel:
|
||||
log.warning('External channel %s not found', channel_id)
|
||||
raise domain.ExternalChannelNotFound(channel_id)
|
||||
|
||||
@@ -11,22 +11,28 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def update_external_channel_links(
|
||||
self: 'Usecase', channel_id: uuid.UUID, input: dto.UpdateExternalChannelLinksInput, user_id: uuid.UUID
|
||||
self: 'Usecase',
|
||||
channel_id: uuid.UUID,
|
||||
input: dto.UpdateExternalChannelLinksInput,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
) -> dto.ExternalChannelOutput:
|
||||
external_channel = await self.database.get_external_channel(user_id, channel_id=channel_id)
|
||||
await self.ensure_workspace_access(workspace_id, user_id)
|
||||
|
||||
external_channel = await self.database.get_external_channel(workspace_id, channel_id=channel_id)
|
||||
if not external_channel:
|
||||
log.warning('External channel %s not found', channel_id)
|
||||
raise domain.ExternalChannelNotFound(channel_id)
|
||||
|
||||
for target_id in input.add_target_channel_ids:
|
||||
target_channel = await self.database.get_target_channel(user_id, channel_id=target_id)
|
||||
target_channel = await self.database.get_target_channel(workspace_id, channel_id=target_id)
|
||||
if not target_channel:
|
||||
log.warning('User %s attempted to link external channel to target %s they do not own', user_id, target_id)
|
||||
raise domain.TargetChannelNotFound(target_id)
|
||||
|
||||
async with self.database.transaction():
|
||||
if input.add_target_channel_ids:
|
||||
await self.database.add_external_channel_to_targets(channel_id, input.add_target_channel_ids)
|
||||
await self.database.add_external_channel_to_targets(external_channel, input.add_target_channel_ids)
|
||||
|
||||
for target_id in input.remove_target_channel_ids:
|
||||
await self.database.remove_external_channel_from_target(channel_id, target_id)
|
||||
|
||||
@@ -10,16 +10,20 @@ if TYPE_CHECKING:
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_placement(self: 'Usecase', input: dto.CreatePlacementInput, user_id: uuid.UUID) -> dto.PlacementOutput:
|
||||
target_channel = await self.database.get_target_channel(user_id, channel_id=input.target_channel_id)
|
||||
async def create_placement(
|
||||
self: 'Usecase', input: dto.CreatePlacementInput, user_id: uuid.UUID, workspace_id: uuid.UUID
|
||||
) -> dto.PlacementOutput:
|
||||
await self.ensure_workspace_access(workspace_id, user_id)
|
||||
|
||||
target_channel = await self.database.get_target_channel(workspace_id, channel_id=input.target_channel_id)
|
||||
if not target_channel:
|
||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||
|
||||
external_channel = await self.database.get_external_channel(user_id, channel_id=input.external_channel_id)
|
||||
external_channel = await self.database.get_external_channel(workspace_id, channel_id=input.external_channel_id)
|
||||
if not external_channel:
|
||||
raise domain.ExternalChannelNotFound(input.external_channel_id)
|
||||
|
||||
creative = await self.database.get_creative(user_id, input.creative_id)
|
||||
creative = await self.database.get_creative(workspace_id, input.creative_id)
|
||||
if not creative:
|
||||
raise domain.CreativeNotFound(input.creative_id)
|
||||
|
||||
@@ -30,7 +34,7 @@ async def create_placement(self: 'Usecase', input: dto.CreatePlacementInput, use
|
||||
target_channel_id=input.target_channel_id,
|
||||
external_channel_id=input.external_channel_id,
|
||||
creative_id=input.creative_id,
|
||||
user_id=user_id,
|
||||
workspace_id=workspace_id,
|
||||
placement_date=input.placement_date,
|
||||
cost=input.cost,
|
||||
comment=input.comment,
|
||||
|
||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def delete_placement(self: 'Usecase', input: dto.DeletePlacementInput) -> None:
|
||||
placement = await self.database.get_placement(input.user_id, input.placement_id)
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
|
||||
if not placement:
|
||||
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
||||
raise domain.PlacementNotFound(input.placement_id)
|
||||
@@ -20,7 +22,7 @@ async def delete_placement(self: 'Usecase', input: dto.DeletePlacementInput) ->
|
||||
return
|
||||
|
||||
async with self.database.transaction():
|
||||
creative = await self.database.get_creative(input.user_id, placement.creative_id)
|
||||
creative = await self.database.get_creative(input.workspace_id, placement.creative_id)
|
||||
if creative and creative.placements_count > 0:
|
||||
creative.placements_count -= 1
|
||||
await self.database.update_creative(creative)
|
||||
|
||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.PlacementOutput:
|
||||
placement = await self.database.get_placement(input.user_id, input.placement_id)
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
|
||||
if not placement:
|
||||
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
||||
raise domain.PlacementNotFound(input.placement_id)
|
||||
|
||||
@@ -7,8 +7,10 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.GetPlacementsOutput:
|
||||
placements = await self.database.get_user_placements(
|
||||
input.user_id,
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
placements = await self.database.get_workspace_placements(
|
||||
input.workspace_id,
|
||||
target_channel_id=input.target_channel_id,
|
||||
external_channel_id=input.external_channel_id,
|
||||
creative_id=input.creative_id,
|
||||
|
||||
@@ -11,9 +11,15 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def update_placement(
|
||||
self: 'Usecase', placement_id: uuid.UUID, input: dto.UpdatePlacementInput, user_id: uuid.UUID
|
||||
self: 'Usecase',
|
||||
placement_id: uuid.UUID,
|
||||
input: dto.UpdatePlacementInput,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
) -> dto.PlacementOutput:
|
||||
placement = await self.database.get_placement(user_id, placement_id)
|
||||
await self.ensure_workspace_access(workspace_id, user_id)
|
||||
|
||||
placement = await self.database.get_placement(workspace_id, placement_id)
|
||||
if not placement:
|
||||
log.warning('Placement %s not found for user %s', placement_id, user_id)
|
||||
raise domain.PlacementNotFound(placement_id)
|
||||
|
||||
@@ -15,7 +15,7 @@ async def disconnect_target_chan_by_tg_id(self: 'Usecase', input: dto.Disconnect
|
||||
log.warning(f'User with telegram_id {input.user_telegram_id} not found when disconnecting channel')
|
||||
return
|
||||
|
||||
channel = await self.database.get_target_channel(user.id, telegram_id=input.telegram_id)
|
||||
channel = await self.database.get_target_channel_for_user_by_telegram(user.id, input.telegram_id)
|
||||
if not channel:
|
||||
log.warning(f'Target channel {input.telegram_id} not found')
|
||||
raise domain.TargetChannelNotFound()
|
||||
|
||||
@@ -7,7 +7,9 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
async def get_user_target_chans(self: 'Usecase', input: dto.GetUserTargetChansInput) -> dto.GetUserTargetChansOutput:
|
||||
channels = await self.database.get_user_target_channels(input.user_id)
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
channels = await self.database.get_workspace_target_channels(input.workspace_id)
|
||||
|
||||
return dto.GetUserTargetChansOutput(
|
||||
target_channels=[
|
||||
|
||||
@@ -22,7 +22,7 @@ async def update_target_chan_permissions(self: 'Usecase', input: dto.UpdateTarge
|
||||
log.warning(f'User with telegram_id {input.user_telegram_id} not found when updating channel permissions')
|
||||
return
|
||||
|
||||
channel = await self.database.get_target_channel(user.id, telegram_id=input.telegram_id)
|
||||
channel = await self.database.get_target_channel_for_user_by_telegram(user.id, input.telegram_id)
|
||||
if not channel:
|
||||
log.warning(f'Target channel {input.telegram_id} not found when permissions changed')
|
||||
raise domain.TargetChannelNotFound()
|
||||
|
||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> dto.GetViewsHistoryOutput:
|
||||
placement = await self.database.get_placement(input.user_id, input.placement_id)
|
||||
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||
|
||||
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
|
||||
if not placement:
|
||||
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
||||
raise domain.PlacementNotFound(input.placement_id)
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from tortoise import timezone
|
||||
|
||||
from src import domain, dto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def update_views_manually(self: 'Usecase', input: dto.UpdateViewsManuallyInput) -> dto.PlacementOutput:
|
||||
placement = await self.database.get_placement(input.user_id, input.placement_id)
|
||||
if not placement:
|
||||
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
||||
raise domain.PlacementNotFound(input.placement_id)
|
||||
|
||||
fetched_at = timezone.now()
|
||||
|
||||
async with self.database.transaction():
|
||||
placement.views_count = input.views_count
|
||||
placement.views_availability = domain.PostViewsAvailability.MANUAL
|
||||
placement.last_views_fetch_at = fetched_at
|
||||
await self.database.update_placement(placement)
|
||||
|
||||
history = domain.PlacementViewsHistory(
|
||||
placement_id=placement.id,
|
||||
views_count=input.views_count,
|
||||
fetched_at=fetched_at,
|
||||
)
|
||||
await self.database.create_placement_views_history(history)
|
||||
|
||||
log.info('Views manually updated for placement %s: %d', placement.id, input.views_count)
|
||||
|
||||
return dto.PlacementOutput(
|
||||
id=placement.id,
|
||||
target_channel_id=placement.target_channel_id,
|
||||
target_channel_title=placement.target_channel.title,
|
||||
external_channel_id=placement.external_channel_id,
|
||||
external_channel_title=placement.external_channel.title,
|
||||
creative_id=placement.creative_id,
|
||||
creative_name=placement.creative.name,
|
||||
placement_date=placement.placement_date,
|
||||
cost=placement.cost,
|
||||
comment=placement.comment,
|
||||
ad_post_url=placement.ad_post_url,
|
||||
invite_link_type=placement.invite_link_type,
|
||||
invite_link=placement.invite_link,
|
||||
status=placement.status,
|
||||
subscriptions_count=placement.subscriptions_count,
|
||||
views_count=placement.views_count,
|
||||
views_availability=placement.views_availability,
|
||||
last_views_fetch_at=placement.last_views_fetch_at,
|
||||
created_at=placement.created_at,
|
||||
)
|
||||
28
src/usecase/workspace/create_workspace.py
Normal file
28
src/usecase/workspace/create_workspace.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src import domain, dto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
|
||||
|
||||
async def create_workspace(
|
||||
self: 'Usecase', user_id: uuid.UUID, input: dto.CreateWorkspaceInput
|
||||
) -> dto.CreateWorkspaceOutput:
|
||||
user = await self.database.get_user(user_id=user_id)
|
||||
if not user:
|
||||
raise domain.UserNotFound(user_id)
|
||||
|
||||
workspace = domain.Workspace(
|
||||
name=input.name,
|
||||
)
|
||||
|
||||
async with self.database.transaction():
|
||||
await self.database.create_workspace(workspace)
|
||||
await self.database.add_user_to_workspace(workspace.id, user_id)
|
||||
|
||||
return dto.CreateWorkspaceOutput(
|
||||
id=workspace.id,
|
||||
name=workspace.name,
|
||||
)
|
||||
11
src/usecase/workspace/delete_workspace.py
Normal file
11
src/usecase/workspace/delete_workspace.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
|
||||
|
||||
async def delete_workspace(self: 'Usecase', workspace_id: uuid.UUID, user_id: uuid.UUID) -> None:
|
||||
self.ensure_workspace_access(workspace_id, user_id)
|
||||
|
||||
await self.database.delete_workspace(workspace_id)
|
||||
21
src/usecase/workspace/get_workspaces.py
Normal file
21
src/usecase/workspace/get_workspaces.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src import dto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
|
||||
|
||||
async def get_workspaces(self: 'Usecase', user_id: uuid.UUID) -> dto.GetWorkspacesOutput:
|
||||
memberships = await self.database.get_user_workspaces(user_id)
|
||||
|
||||
return dto.GetWorkspacesOutput(
|
||||
workspaces=[
|
||||
dto.WorkspaceMembershipOutput(
|
||||
id=membership.workspace_id,
|
||||
name=membership.workspace.name,
|
||||
)
|
||||
for membership in memberships
|
||||
]
|
||||
)
|
||||
26
src/usecase/workspace/update_workspace.py
Normal file
26
src/usecase/workspace/update_workspace.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src import domain, dto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .. import Usecase
|
||||
|
||||
|
||||
async def update_workspace(
|
||||
self: 'Usecase', workspace_id: uuid.UUID, user_id: uuid.UUID, input: dto.UpdateWorkspaceInput
|
||||
) -> dto.WorkspaceMembershipOutput:
|
||||
membership = await self.database.get_workspace_membership(workspace_id, user_id)
|
||||
if not membership:
|
||||
raise domain.WorkspaceNotFound(workspace_id)
|
||||
|
||||
workspace = membership.workspace
|
||||
|
||||
if input.name is not None:
|
||||
workspace.name = input.name
|
||||
await self.database.update_workspace(workspace)
|
||||
|
||||
return dto.WorkspaceMembershipOutput(
|
||||
id=workspace.id,
|
||||
name=workspace.name,
|
||||
)
|
||||
Reference in New Issue
Block a user