feat: add workspace
This commit is contained in:
@@ -19,6 +19,55 @@ class Postgres(DatabaseBase):
|
||||
async def create_user(self, user: domain.User) -> None:
|
||||
await user.save()
|
||||
|
||||
async def create_workspace(self, workspace: domain.Workspace) -> None:
|
||||
await workspace.save()
|
||||
|
||||
async def update_workspace(self, workspace: domain.Workspace) -> None:
|
||||
await workspace.save()
|
||||
|
||||
async def delete_workspace(self, workspace_id: uuid.UUID) -> None:
|
||||
await domain.Workspace.filter(id=workspace_id).delete()
|
||||
|
||||
async def add_user_to_workspace(
|
||||
self, workspace_id: uuid.UUID, user_id: uuid.UUID, *, is_owner: bool = False
|
||||
) -> None:
|
||||
await domain.WorkspaceUser.create(workspace_id=workspace_id, user_id=user_id, is_owner=is_owner)
|
||||
|
||||
async def get_user_workspaces(self, user_id: uuid.UUID) -> list[domain.WorkspaceUser]:
|
||||
return (
|
||||
await domain.WorkspaceUser.filter(user_id=user_id)
|
||||
.prefetch_related('workspace')
|
||||
.order_by('created_at')
|
||||
.all()
|
||||
)
|
||||
|
||||
async def get_workspace(self, workspace_id: uuid.UUID) -> domain.Workspace | None:
|
||||
return await domain.Workspace.get_or_none(id=workspace_id)
|
||||
|
||||
async def get_workspace_for_user(self, workspace_id: uuid.UUID, user_id: uuid.UUID) -> domain.Workspace | None:
|
||||
membership = (
|
||||
await domain.WorkspaceUser.filter(workspace_id=workspace_id, user_id=user_id)
|
||||
.prefetch_related('workspace')
|
||||
.first()
|
||||
)
|
||||
return membership.workspace if membership else None
|
||||
|
||||
async def get_default_workspace_for_user(self, user_id: uuid.UUID) -> domain.Workspace | None:
|
||||
membership = (
|
||||
await domain.WorkspaceUser.filter(user_id=user_id)
|
||||
.prefetch_related('workspace')
|
||||
.order_by('created_at')
|
||||
.first()
|
||||
)
|
||||
return membership.workspace if membership else None
|
||||
|
||||
async def get_workspace_membership(
|
||||
self, workspace_id: uuid.UUID, user_id: uuid.UUID
|
||||
) -> domain.WorkspaceUser | None:
|
||||
return await domain.WorkspaceUser.get_or_none(workspace_id=workspace_id, user_id=user_id).prefetch_related(
|
||||
'workspace'
|
||||
)
|
||||
|
||||
async def create_login_token(self, login_token: domain.LoginToken) -> None:
|
||||
await login_token.save()
|
||||
|
||||
@@ -46,12 +95,12 @@ class Postgres(DatabaseBase):
|
||||
raise domain.LoginTokenAlreadyUsed() # либо нет токена, либо он уже использован
|
||||
|
||||
async def get_target_channel(
|
||||
self, user_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
||||
self, workspace_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
||||
) -> domain.TargetChannel | None:
|
||||
if not channel_id and not telegram_id:
|
||||
raise ValueError('Either channel_id or telegram_id must be provided')
|
||||
|
||||
q = domain.TargetChannel.filter(user_id=user_id)
|
||||
q = domain.TargetChannel.filter(workspace_id=workspace_id)
|
||||
if channel_id:
|
||||
q = q.filter(id=channel_id)
|
||||
if telegram_id:
|
||||
@@ -59,8 +108,17 @@ class Postgres(DatabaseBase):
|
||||
|
||||
return await q.first()
|
||||
|
||||
async def get_target_channel_for_user_by_telegram(
|
||||
self, user_id: uuid.UUID, telegram_id: int
|
||||
) -> domain.TargetChannel | None:
|
||||
return (
|
||||
await domain.TargetChannel.filter(telegram_id=telegram_id, workspace__workspace_users__user_id=user_id)
|
||||
.prefetch_related('workspace')
|
||||
.first()
|
||||
)
|
||||
|
||||
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None:
|
||||
existing = await self.get_target_channel(user_id=channel.user_id, telegram_id=channel.telegram_id)
|
||||
existing = await self.get_target_channel(workspace_id=channel.workspace_id, telegram_id=channel.telegram_id)
|
||||
|
||||
if not existing:
|
||||
await channel.save()
|
||||
@@ -68,22 +126,24 @@ class Postgres(DatabaseBase):
|
||||
|
||||
existing.title = channel.title
|
||||
existing.username = channel.username
|
||||
existing.user_id = channel.user_id
|
||||
existing.workspace_id = channel.workspace_id
|
||||
existing.status = channel.status
|
||||
existing.deleted_at = None
|
||||
existing.updated_at = timezone.now()
|
||||
await existing.save()
|
||||
|
||||
async def get_user_target_channels(self, user_id: uuid.UUID) -> list[domain.TargetChannel]:
|
||||
return await domain.TargetChannel.filter(user_id=user_id, status=domain.TargetChannelStatus.ACTIVE).all()
|
||||
async def get_workspace_target_channels(self, workspace_id: uuid.UUID) -> list[domain.TargetChannel]:
|
||||
return await domain.TargetChannel.filter(
|
||||
workspace_id=workspace_id, status=domain.TargetChannelStatus.ACTIVE
|
||||
).all()
|
||||
|
||||
async def update_target_channel(self, channel: domain.TargetChannel) -> None:
|
||||
await channel.save()
|
||||
|
||||
async def get_external_channel(
|
||||
self, user_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
||||
self, workspace_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
||||
) -> domain.ExternalChannel | None:
|
||||
query = domain.ExternalChannel.filter(user_id=user_id)
|
||||
query = domain.ExternalChannel.filter(workspace_id=workspace_id)
|
||||
if channel_id:
|
||||
query = query.filter(id=channel_id)
|
||||
if telegram_id:
|
||||
@@ -94,8 +154,8 @@ class Postgres(DatabaseBase):
|
||||
async def get_external_channels_for_target(self, target_channel_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
||||
return await domain.ExternalChannel.filter(target_channels__id=target_channel_id).all()
|
||||
|
||||
async def get_user_external_channels(self, user_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
||||
return await domain.ExternalChannel.filter(user_id=user_id).all()
|
||||
async def get_workspace_external_channels(self, workspace_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
||||
return await domain.ExternalChannel.filter(workspace_id=workspace_id).all()
|
||||
|
||||
async def add_external_channel_to_targets(
|
||||
self, external_channel: domain.ExternalChannel, target_channel_ids: list[uuid.UUID]
|
||||
@@ -121,16 +181,18 @@ class Postgres(DatabaseBase):
|
||||
async def update_external_channel(self, channel: domain.ExternalChannel) -> None:
|
||||
await channel.save()
|
||||
|
||||
async def get_creative(self, user_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
|
||||
return await domain.Creative.get_or_none(id=creative_id, user_id=user_id).prefetch_related('target_channel')
|
||||
async def get_creative(self, workspace_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
|
||||
return await domain.Creative.get_or_none(id=creative_id, workspace_id=workspace_id).prefetch_related(
|
||||
'target_channel'
|
||||
)
|
||||
|
||||
async def update_creative(self, creative: domain.Creative) -> None:
|
||||
await creative.save()
|
||||
|
||||
async def get_user_creatives(
|
||||
self, user_id: uuid.UUID, target_channel_id: uuid.UUID | None = None, include_archived: bool = False
|
||||
async def get_workspace_creatives(
|
||||
self, workspace_id: uuid.UUID, target_channel_id: uuid.UUID | None = None, include_archived: bool = False
|
||||
) -> list[domain.Creative]:
|
||||
query = domain.Creative.filter(user_id=user_id)
|
||||
query = domain.Creative.filter(workspace_id=workspace_id)
|
||||
|
||||
if target_channel_id:
|
||||
query = query.filter(target_channel_id=target_channel_id)
|
||||
@@ -143,8 +205,8 @@ class Postgres(DatabaseBase):
|
||||
async def delete_creative(self, creative_id: uuid.UUID) -> None:
|
||||
await domain.Creative.filter(id=creative_id).delete()
|
||||
|
||||
async def get_placement(self, user_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None:
|
||||
return await domain.Placement.get_or_none(id=placement_id, user_id=user_id).prefetch_related(
|
||||
async def get_placement(self, workspace_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None:
|
||||
return await domain.Placement.get_or_none(id=placement_id, workspace_id=workspace_id).prefetch_related(
|
||||
'target_channel', 'external_channel', 'creative'
|
||||
)
|
||||
|
||||
@@ -154,15 +216,15 @@ class Postgres(DatabaseBase):
|
||||
async def update_placement(self, placement: domain.Placement) -> None:
|
||||
await placement.save()
|
||||
|
||||
async def get_user_placements(
|
||||
async def get_workspace_placements(
|
||||
self,
|
||||
user_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
external_channel_id: uuid.UUID | None = None,
|
||||
creative_id: uuid.UUID | None = None,
|
||||
include_archived: bool = False,
|
||||
) -> list[domain.Placement]:
|
||||
query = domain.Placement.filter(user_id=user_id)
|
||||
query = domain.Placement.filter(workspace_id=workspace_id)
|
||||
|
||||
if target_channel_id:
|
||||
query = query.filter(target_channel_id=target_channel_id)
|
||||
|
||||
@@ -7,6 +7,7 @@ from src.controller.http.external_channels import external_channels_router
|
||||
from src.controller.http.placements import placements_router
|
||||
from src.controller.http.target_channels import target_channels_router
|
||||
from src.controller.http.views import views_router
|
||||
from src.controller.http.workspaces import workspaces_router
|
||||
|
||||
api_router = APIRouter()
|
||||
|
||||
@@ -19,5 +20,6 @@ api_v1_router.include_router(creatives_router)
|
||||
api_v1_router.include_router(placements_router)
|
||||
api_v1_router.include_router(views_router)
|
||||
api_v1_router.include_router(analytics_router)
|
||||
api_v1_router.include_router(workspaces_router)
|
||||
|
||||
api_router.include_router(api_v1_router)
|
||||
|
||||
@@ -8,16 +8,18 @@ from fastapi.routing import APIRouter
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
analytics_router = APIRouter(prefix='/analytics', tags=['analytics'])
|
||||
analytics_router = APIRouter(prefix='/workspaces/{workspace_id}/analytics', tags=['analytics'])
|
||||
|
||||
|
||||
@analytics_router.get('/placements')
|
||||
async def get_placements_analytics(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
) -> dto.GetPlacementsAnalyticsOutput:
|
||||
input = dto.GetPlacementsAnalyticsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
)
|
||||
return await deps.get_usecase().get_placements_analytics(input)
|
||||
@@ -25,11 +27,13 @@ async def get_placements_analytics(
|
||||
|
||||
@analytics_router.get('/creatives')
|
||||
async def get_creatives_analytics(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
) -> dto.GetCreativesAnalyticsOutput:
|
||||
input = dto.GetCreativesAnalyticsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
)
|
||||
return await deps.get_usecase().get_creatives_analytics(input)
|
||||
@@ -37,11 +41,13 @@ async def get_creatives_analytics(
|
||||
|
||||
@analytics_router.get('/external-channels')
|
||||
async def get_external_channels_analytics(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
) -> dto.GetExternalChannelsAnalyticsOutput:
|
||||
input = dto.GetExternalChannelsAnalyticsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
)
|
||||
return await deps.get_usecase().get_external_channels_analytics(input)
|
||||
@@ -49,6 +55,7 @@ async def get_external_channels_analytics(
|
||||
|
||||
@analytics_router.get('/spending')
|
||||
async def get_spending_analytics(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
date_from: datetime.datetime | None = None,
|
||||
@@ -57,6 +64,7 @@ async def get_spending_analytics(
|
||||
) -> dto.GetSpendingAnalyticsOutput:
|
||||
input = dto.GetSpendingAnalyticsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
|
||||
@@ -7,17 +7,19 @@ from fastapi.routing import APIRouter
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
creatives_router = APIRouter(prefix='/creatives', tags=['creatives'])
|
||||
creatives_router = APIRouter(prefix='/workspaces/{workspace_id}/creatives', tags=['creatives'])
|
||||
|
||||
|
||||
@creatives_router.get('')
|
||||
async def list_creatives(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
include_archived: bool = False,
|
||||
) -> dto.GetCreativesOutput:
|
||||
input = dto.GetCreativesInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
include_archived=include_archived,
|
||||
)
|
||||
@@ -28,11 +30,13 @@ async def list_creatives(
|
||||
@creatives_router.get('/{creative_id}')
|
||||
async def get_creative(
|
||||
creative_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.CreativeOutput:
|
||||
input = dto.GetCreativeInput(
|
||||
creative_id=creative_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
return await deps.get_usecase().get_creative(input=input)
|
||||
@@ -41,32 +45,37 @@ async def get_creative(
|
||||
@creatives_router.post('')
|
||||
async def create_creative(
|
||||
request: dto.CreateCreativeInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.CreativeOutput:
|
||||
return await deps.get_usecase().create_creative(request, current_user.user_id)
|
||||
return await deps.get_usecase().create_creative(request, current_user.user_id, workspace_id)
|
||||
|
||||
|
||||
@creatives_router.patch('/{creative_id}')
|
||||
async def update_creative(
|
||||
creative_id: uuid.UUID,
|
||||
request: dto.UpdateCreativeInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.CreativeOutput:
|
||||
return await deps.get_usecase().update_creative(
|
||||
creative_id=creative_id,
|
||||
input=request,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
@creatives_router.delete('/{creative_id}')
|
||||
async def delete_creative(
|
||||
creative_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> None:
|
||||
input = dto.DeleteCreativeInput(
|
||||
creative_id=creative_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
await deps.get_usecase().delete_creative(input)
|
||||
|
||||
@@ -7,18 +7,20 @@ from fastapi.routing import APIRouter
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
external_channels_router = APIRouter(prefix='/external_channels', tags=['external_channels'])
|
||||
external_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/external_channels', tags=['external_channels'])
|
||||
|
||||
|
||||
@external_channels_router.get('/target/{target_channel_id}')
|
||||
async def get_external_channels(
|
||||
target_channel_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.GetExternalChannelsOutput:
|
||||
"""Get all external channels for a specific target channel."""
|
||||
input = dto.GetExternalChannelsInput(
|
||||
target_channel_id=target_channel_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
return await deps.get_usecase().get_external_channels(input=input)
|
||||
@@ -27,12 +29,14 @@ async def get_external_channels(
|
||||
@external_channels_router.post('')
|
||||
async def create_external_channel(
|
||||
request: dto.CreateExternalChannelInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.ExternalChannelOutput:
|
||||
"""Create a new external channel and link it to target channels."""
|
||||
return await deps.get_usecase().create_external_channel(
|
||||
input=request,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -40,6 +44,7 @@ async def create_external_channel(
|
||||
async def update_external_channel(
|
||||
channel_id: uuid.UUID,
|
||||
request: dto.UpdateExternalChannelInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.ExternalChannelOutput:
|
||||
"""Update external channel fields (title, username, description, subscribers_count)."""
|
||||
@@ -47,6 +52,7 @@ async def update_external_channel(
|
||||
channel_id=channel_id,
|
||||
input=request,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -54,6 +60,7 @@ async def update_external_channel(
|
||||
async def update_external_channel_links(
|
||||
channel_id: uuid.UUID,
|
||||
request: dto.UpdateExternalChannelLinksInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.ExternalChannelOutput:
|
||||
"""
|
||||
@@ -65,18 +72,21 @@ async def update_external_channel_links(
|
||||
channel_id=channel_id,
|
||||
input=request,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
@external_channels_router.delete('/{channel_id}')
|
||||
async def delete_external_channel(
|
||||
channel_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> None:
|
||||
"""Delete an external channel."""
|
||||
input = dto.DeleteExternalChannelInput(
|
||||
channel_id=channel_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
await deps.get_usecase().delete_external_channel(input=input)
|
||||
|
||||
@@ -7,11 +7,12 @@ from fastapi.routing import APIRouter
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
placements_router = APIRouter(prefix='/placements', tags=['placements'])
|
||||
placements_router = APIRouter(prefix='/workspaces/{workspace_id}/placements', tags=['placements'])
|
||||
|
||||
|
||||
@placements_router.get('')
|
||||
async def list_placements(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
target_channel_id: uuid.UUID | None = None,
|
||||
external_channel_id: uuid.UUID | None = None,
|
||||
@@ -20,6 +21,7 @@ async def list_placements(
|
||||
) -> dto.GetPlacementsOutput:
|
||||
input = dto.GetPlacementsInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
target_channel_id=target_channel_id,
|
||||
external_channel_id=external_channel_id,
|
||||
creative_id=creative_id,
|
||||
@@ -32,11 +34,13 @@ async def list_placements(
|
||||
@placements_router.get('/{placement_id}')
|
||||
async def get_placement(
|
||||
placement_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PlacementOutput:
|
||||
input = dto.GetPlacementInput(
|
||||
placement_id=placement_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
return await deps.get_usecase().get_placement(input=input)
|
||||
@@ -45,32 +49,37 @@ async def get_placement(
|
||||
@placements_router.post('')
|
||||
async def create_placement(
|
||||
request: dto.CreatePlacementInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PlacementOutput:
|
||||
return await deps.get_usecase().create_placement(request, current_user.user_id)
|
||||
return await deps.get_usecase().create_placement(request, current_user.user_id, workspace_id)
|
||||
|
||||
|
||||
@placements_router.patch('/{placement_id}')
|
||||
async def update_placement(
|
||||
placement_id: uuid.UUID,
|
||||
request: dto.UpdatePlacementInput,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PlacementOutput:
|
||||
return await deps.get_usecase().update_placement(
|
||||
placement_id=placement_id,
|
||||
input=request,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
|
||||
@placements_router.delete('/{placement_id}')
|
||||
async def delete_placement(
|
||||
placement_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> None:
|
||||
input = dto.DeletePlacementInput(
|
||||
placement_id=placement_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
await deps.get_usecase().delete_placement(input)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
@@ -6,15 +7,17 @@ from fastapi.routing import APIRouter
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
target_channels_router = APIRouter(prefix='/target_channels', tags=['target_channels'])
|
||||
target_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/target_channels', tags=['target_channels'])
|
||||
|
||||
|
||||
@target_channels_router.get('')
|
||||
async def get_user_target_chans(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.GetUserTargetChansOutput:
|
||||
input = dto.GetUserTargetChansInput(
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
)
|
||||
|
||||
return await deps.get_usecase().get_user_target_chans(input=input)
|
||||
|
||||
@@ -8,12 +8,13 @@ from fastapi.routing import APIRouter
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
views_router = APIRouter(prefix='/placements/{placement_id}/views', tags=['views'])
|
||||
views_router = APIRouter(prefix='/workspaces/{workspace_id}/placements/{placement_id}/views', tags=['views'])
|
||||
|
||||
|
||||
@views_router.get('/history')
|
||||
async def get_views_history(
|
||||
placement_id: uuid.UUID,
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
from_date: datetime.datetime | None = None,
|
||||
to_date: datetime.datetime | None = None,
|
||||
@@ -21,23 +22,9 @@ async def get_views_history(
|
||||
input_data = dto.GetViewsHistoryInput(
|
||||
placement_id=placement_id,
|
||||
user_id=current_user.user_id,
|
||||
workspace_id=workspace_id,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
)
|
||||
|
||||
return await deps.get_usecase().get_views_history(input=input_data)
|
||||
|
||||
|
||||
@views_router.post('/manual')
|
||||
async def update_views_manually(
|
||||
placement_id: uuid.UUID,
|
||||
views_count: int,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.PlacementOutput:
|
||||
input_data = dto.UpdateViewsManuallyInput(
|
||||
placement_id=placement_id,
|
||||
user_id=current_user.user_id,
|
||||
views_count=views_count,
|
||||
)
|
||||
|
||||
return await deps.get_usecase().update_views_manually(input=input_data)
|
||||
|
||||
42
src/controller/http/workspaces.py
Normal file
42
src/controller/http/workspaces.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import uuid
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from src import deps, dto
|
||||
from src.adapter.jwt import JWTPayload
|
||||
|
||||
workspaces_router = APIRouter(prefix='/workspaces', tags=['workspaces'])
|
||||
|
||||
|
||||
@workspaces_router.get('')
|
||||
async def list_workspaces(
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.GetWorkspacesOutput:
|
||||
return await deps.get_usecase().get_workspaces(current_user.user_id)
|
||||
|
||||
|
||||
@workspaces_router.post('')
|
||||
async def create_workspace(
|
||||
request: dto.CreateWorkspaceInput,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.CreateWorkspaceOutput:
|
||||
return await deps.get_usecase().create_workspace(current_user.user_id, request)
|
||||
|
||||
|
||||
@workspaces_router.patch('/{workspace_id}')
|
||||
async def update_workspace(
|
||||
workspace_id: uuid.UUID,
|
||||
request: dto.UpdateWorkspaceInput,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> dto.WorkspaceMembershipOutput:
|
||||
return await deps.get_usecase().update_workspace(workspace_id, current_user.user_id, request)
|
||||
|
||||
|
||||
@workspaces_router.delete('/{workspace_id}')
|
||||
async def delete_workspace(
|
||||
workspace_id: uuid.UUID,
|
||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||
) -> None:
|
||||
await deps.get_usecase().delete_workspace(workspace_id, current_user.user_id)
|
||||
@@ -1,5 +1,7 @@
|
||||
__all__ = (
|
||||
'User',
|
||||
'Workspace',
|
||||
'WorkspaceUser',
|
||||
'TargetChannel',
|
||||
'ExternalChannel',
|
||||
'Creative',
|
||||
@@ -17,6 +19,8 @@ __all__ = (
|
||||
'PostViewsAvailability',
|
||||
'LoginToken',
|
||||
'UserNotFound',
|
||||
'WorkspaceNotFound',
|
||||
'WorkspaceAccessDenied',
|
||||
'LoginTokenNotFound',
|
||||
'LoginTokenExpired',
|
||||
'LoginTokenAlreadyUsed',
|
||||
@@ -44,6 +48,8 @@ from .error import (
|
||||
PlacementNotFound,
|
||||
TargetChannelNotFound,
|
||||
UserNotFound,
|
||||
WorkspaceAccessDenied,
|
||||
WorkspaceNotFound,
|
||||
)
|
||||
from .external_channel import ExternalChannel
|
||||
from .login_token import LoginToken
|
||||
@@ -54,3 +60,4 @@ 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
|
||||
|
||||
@@ -8,7 +8,7 @@ from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .target_channel import TargetChannel
|
||||
from .user import User
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class CreativeStatus(str, enum.Enum):
|
||||
@@ -23,15 +23,15 @@ class Creative(TimestampedModel):
|
||||
status = fields.CharEnumField(CreativeStatus, default=CreativeStatus.ACTIVE)
|
||||
placements_count = fields.IntField(default=0)
|
||||
|
||||
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
||||
'models.User', related_name='creatives', on_delete=fields.CASCADE, index=True
|
||||
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
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
user_id: UUID
|
||||
workspace_id: UUID
|
||||
target_channel_id: UUID
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -64,3 +64,15 @@ def PlacementNotFound(placement_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if placement_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Placement not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Placement {placement_id} not found')
|
||||
|
||||
|
||||
def WorkspaceNotFound(workspace_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if workspace_id is None:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Workspace not found')
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Workspace {workspace_id} not found')
|
||||
|
||||
|
||||
def WorkspaceAccessDenied(workspace_id: uuid.UUID | None = None) -> HTTPException:
|
||||
if workspace_id is None:
|
||||
return HTTPException(status.HTTP_403_FORBIDDEN, 'Workspace access denied')
|
||||
return HTTPException(status.HTTP_403_FORBIDDEN, f'Workspace {workspace_id} access denied')
|
||||
|
||||
@@ -7,7 +7,7 @@ from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .target_channel import TargetChannel
|
||||
from .user import User
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class ExternalChannel(TimestampedModel):
|
||||
@@ -18,14 +18,13 @@ class ExternalChannel(TimestampedModel):
|
||||
description = fields.TextField(null=True)
|
||||
subscribers_count = fields.IntField(null=True)
|
||||
|
||||
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
||||
'models.User', related_name='external_channels', on_delete=fields.CASCADE, index=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']
|
||||
user_id: UUID
|
||||
workspace_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'external_channel'
|
||||
unique_together = (('user_id', 'telegram_id'),)
|
||||
|
||||
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
||||
from .creative import Creative
|
||||
from .external_channel import ExternalChannel
|
||||
from .target_channel import TargetChannel
|
||||
from .user import User
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class PlacementStatus(str, enum.Enum):
|
||||
@@ -59,15 +59,15 @@ class Placement(TimestampedModel):
|
||||
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
||||
'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
||||
'models.User', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
target_channel_id: UUID
|
||||
external_channel_id: UUID
|
||||
creative_id: UUID
|
||||
user_id: UUID
|
||||
workspace_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'placement'
|
||||
|
||||
@@ -8,7 +8,7 @@ from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .external_channel import ExternalChannel
|
||||
from .user import User
|
||||
from .workspace import Workspace
|
||||
|
||||
|
||||
class TargetChannelStatus(str, enum.Enum):
|
||||
@@ -24,8 +24,8 @@ class TargetChannel(TimestampedModel):
|
||||
username = fields.CharField(max_length=255, null=True, index=True)
|
||||
status = fields.CharEnumField(TargetChannelStatus, default=TargetChannelStatus.ACTIVE)
|
||||
|
||||
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
||||
'models.User', related_name='target_channels', on_delete=fields.CASCADE, index=True
|
||||
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(
|
||||
@@ -37,7 +37,7 @@ class TargetChannel(TimestampedModel):
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
user_id: UUID
|
||||
workspace_id: UUID
|
||||
|
||||
class Meta:
|
||||
table = 'target_channel'
|
||||
|
||||
38
src/domain/workspace.py
Normal file
38
src/domain/workspace.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from tortoise import fields
|
||||
|
||||
from .base import TimestampedModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .user import User
|
||||
|
||||
|
||||
class Workspace(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
name = fields.CharField(max_length=255)
|
||||
|
||||
class Meta:
|
||||
table = 'workspace'
|
||||
|
||||
|
||||
class WorkspaceUser(TimestampedModel):
|
||||
id = fields.UUIDField(pk=True)
|
||||
|
||||
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
||||
'models.Workspace', related_name='workspace_users', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
|
||||
'models.User', related_name='workspace_users', on_delete=fields.CASCADE, index=True
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
workspace_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
|
||||
class Meta:
|
||||
table = 'workspace_user'
|
||||
unique_together = (('workspace_id', 'user_id'),)
|
||||
@@ -53,6 +53,11 @@ __all__ = (
|
||||
'ParserPostFoundEvent',
|
||||
'ParserPostDeletedEvent',
|
||||
'ParserViewsUpdatedEvent',
|
||||
'WorkspaceMembershipOutput',
|
||||
'GetWorkspacesOutput',
|
||||
'CreateWorkspaceInput',
|
||||
'CreateWorkspaceOutput',
|
||||
'UpdateWorkspaceInput',
|
||||
)
|
||||
|
||||
from .analytics import (
|
||||
@@ -122,3 +127,10 @@ from .views import (
|
||||
PlacementViewsHistoryOutput,
|
||||
UpdateViewsManuallyInput,
|
||||
)
|
||||
from .workspace import (
|
||||
CreateWorkspaceInput,
|
||||
CreateWorkspaceOutput,
|
||||
GetWorkspacesOutput,
|
||||
UpdateWorkspaceInput,
|
||||
WorkspaceMembershipOutput,
|
||||
)
|
||||
|
||||
@@ -54,6 +54,7 @@ class ExternalChannelAnalyticsOutput(pydantic.BaseModel):
|
||||
|
||||
class GetPlacementsAnalyticsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
@@ -63,6 +64,7 @@ class GetPlacementsAnalyticsOutput(pydantic.BaseModel):
|
||||
|
||||
class GetCreativesAnalyticsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
@@ -72,6 +74,7 @@ class GetCreativesAnalyticsOutput(pydantic.BaseModel):
|
||||
|
||||
class GetExternalChannelsAnalyticsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
|
||||
|
||||
@@ -90,6 +93,7 @@ class SpendingDataPoint(pydantic.BaseModel):
|
||||
|
||||
class GetSpendingAnalyticsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
date_from: datetime.datetime | None = None
|
||||
date_to: datetime.datetime | None = None
|
||||
|
||||
@@ -19,6 +19,7 @@ class CreativeOutput(pydantic.BaseModel):
|
||||
|
||||
class GetCreativesInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
include_archived: bool = False
|
||||
|
||||
@@ -30,6 +31,7 @@ class GetCreativesOutput(pydantic.BaseModel):
|
||||
class GetCreativeInput(pydantic.BaseModel):
|
||||
creative_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
|
||||
class CreateCreativeInput(pydantic.BaseModel):
|
||||
@@ -47,3 +49,4 @@ class UpdateCreativeInput(pydantic.BaseModel):
|
||||
class DeleteCreativeInput(pydantic.BaseModel):
|
||||
creative_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
@@ -15,6 +15,7 @@ class ExternalChannelOutput(pydantic.BaseModel):
|
||||
class GetExternalChannelsInput(pydantic.BaseModel):
|
||||
target_channel_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
|
||||
class GetExternalChannelsOutput(pydantic.BaseModel):
|
||||
@@ -33,6 +34,7 @@ class CreateExternalChannelInput(pydantic.BaseModel):
|
||||
class DeleteExternalChannelInput(pydantic.BaseModel):
|
||||
channel_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
|
||||
class UpdateExternalChannelInput(pydantic.BaseModel):
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import pydantic
|
||||
|
||||
|
||||
# Исходящие сообщения (к tgex-parser)
|
||||
class ParserTrackTaskRequest(pydantic.BaseModel):
|
||||
"""Сообщение для parser.track - добавить задачу на отслеживание канала"""
|
||||
|
||||
task_id: str # UUID placement
|
||||
channel: str # username внешнего канала
|
||||
target_text: str # invite_link для поиска в постах
|
||||
|
||||
|
||||
class ParserGetViewsRequest(pydantic.BaseModel):
|
||||
"""Сообщение для parser.get_views - синхронно получить просмотры поста"""
|
||||
|
||||
channel: str
|
||||
message_id: int
|
||||
|
||||
|
||||
# Входящие события (от tgex-parser)
|
||||
class ParserPostFoundEvent(pydantic.BaseModel):
|
||||
"""Событие parser.event.post_found - найден новый пост с target_text"""
|
||||
|
||||
task_id: str # UUID placement
|
||||
channel: str
|
||||
message_id: int
|
||||
link: str # полная ссылка https://t.me/channel/message_id
|
||||
text: str # текст поста
|
||||
views: int
|
||||
|
||||
|
||||
class ParserPostDeletedEvent(pydantic.BaseModel):
|
||||
"""Событие parser.event.post_deleted - отслеживаемый пост удален"""
|
||||
|
||||
task_id: str
|
||||
channel: str
|
||||
message_id: int
|
||||
link: str
|
||||
views: int
|
||||
|
||||
|
||||
class ParserViewsUpdatedEvent(pydantic.BaseModel):
|
||||
"""Событие parser.event.views_updated - обновилось количество просмотров"""
|
||||
|
||||
task_id: str
|
||||
channel: str
|
||||
message_id: int
|
||||
views: int
|
||||
@@ -30,6 +30,7 @@ class PlacementOutput(pydantic.BaseModel):
|
||||
|
||||
class GetPlacementsInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
target_channel_id: uuid.UUID | None = None
|
||||
external_channel_id: uuid.UUID | None = None
|
||||
creative_id: uuid.UUID | None = None
|
||||
@@ -43,6 +44,7 @@ class GetPlacementsOutput(pydantic.BaseModel):
|
||||
class GetPlacementInput(pydantic.BaseModel):
|
||||
placement_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
|
||||
class CreatePlacementInput(pydantic.BaseModel):
|
||||
@@ -65,3 +67,4 @@ class UpdatePlacementInput(pydantic.BaseModel):
|
||||
class DeletePlacementInput(pydantic.BaseModel):
|
||||
placement_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
@@ -37,6 +37,7 @@ class ConnectTargetChanOutput(pydantic.BaseModel):
|
||||
|
||||
class GetUserTargetChansInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
|
||||
|
||||
class GetUserTargetChansOutput(pydantic.BaseModel):
|
||||
|
||||
@@ -19,6 +19,7 @@ class GetViewsHistoryInput(pydantic.BaseModel):
|
||||
|
||||
placement_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
from_date: datetime.datetime | None = None # Фильтр: с какой даты
|
||||
to_date: datetime.datetime | None = None # Фильтр: по какую дату
|
||||
|
||||
@@ -34,4 +35,5 @@ class UpdateViewsManuallyInput(pydantic.BaseModel):
|
||||
|
||||
placement_id: uuid.UUID
|
||||
user_id: uuid.UUID
|
||||
workspace_id: uuid.UUID
|
||||
views_count: int
|
||||
|
||||
23
src/dto/workspace.py
Normal file
23
src/dto/workspace.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import uuid
|
||||
|
||||
import pydantic
|
||||
|
||||
|
||||
class WorkspaceMembershipOutput(pydantic.BaseModel):
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
|
||||
|
||||
class CreateWorkspaceOutput(WorkspaceMembershipOutput): ...
|
||||
|
||||
|
||||
class GetWorkspacesOutput(pydantic.BaseModel):
|
||||
workspaces: list[WorkspaceMembershipOutput]
|
||||
|
||||
|
||||
class CreateWorkspaceInput(pydantic.BaseModel):
|
||||
name: str
|
||||
|
||||
|
||||
class UpdateWorkspaceInput(pydantic.BaseModel):
|
||||
name: str | None = None
|
||||
@@ -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