правки

This commit is contained in:
Artem Tsyrulnikov
2026-02-01 18:13:46 +03:00
parent 4e35c73a66
commit 52cd2ec1a0
8 changed files with 503 additions and 115 deletions

View File

@@ -397,6 +397,17 @@ class Postgres(DatabaseBase):
'project', 'project__channel', 'channel', 'creative'
)
@staticmethod
async def count_placements_by_project_and_channel(
project_id: uuid.UUID,
channel_id: uuid.UUID,
) -> int:
"""Count placements for a project in a specific channel."""
return await domain.Placement.filter(
project_id=project_id,
channel_id=channel_id,
).count()
@staticmethod
async def update_placement(placement: domain.Placement) -> None:
await placement.save()

View File

@@ -9,6 +9,12 @@ from src.domain.placement_post import PlacementPostStatus
from .channel import ChannelOutput
class ProjectOutput(pydantic.BaseModel):
id: uuid.UUID
status: str
channel: ChannelOutput
class CostInfo(pydantic.BaseModel):
type: CostType
value: float
@@ -30,11 +36,14 @@ class PlacementOutput(pydantic.BaseModel):
id: uuid.UUID
status: PlacementStatus
creative_id: uuid.UUID | None = None
creative_name: str | None = None
comment: str | None = None
invite_link: str | None
invite_link_created_at: datetime.datetime | None = None
invite_link_type: InviteLinkType
channel: ChannelOutput
project: ProjectOutput | None = None
placement_number: int
details: PlacementDetails | None = None

View File

@@ -31,16 +31,41 @@ def _build_placement_details(placement: domain.Placement) -> dto.PlacementDetail
return None
def _build_placement_output(placement: domain.Placement) -> dto.PlacementOutput:
def _build_placement_output(
placement: domain.Placement,
project: domain.Project | None = None,
placement_number: int = 0,
creative_name: str | None = None,
) -> dto.PlacementOutput:
channel = placement.channel
if channel is None:
log.error('Placement %s has no channel prefetched', placement.id)
raise ValueError(f'Placement {placement.id} has no channel')
# Build project output if project is provided
project_output: dto.ProjectOutput | None = None
if project is not None:
project_channel = project.channel
if project_channel is None:
log.error('Project %s has no channel', project.id)
raise ValueError(f'Project {project.id} has no channel')
project_output = dto.ProjectOutput(
id=project.id,
status=project.status.value,
channel=dto.ChannelOutput(
id=project_channel.id,
telegram_id=project_channel.telegram_id,
title=project_channel.title,
username=project_channel.username,
),
)
return dto.PlacementOutput(
id=placement.id,
status=placement.status,
creative_id=placement.creative_id,
creative_name=creative_name,
comment=placement.comment,
invite_link=placement.invite_link,
invite_link_type=placement.invite_link_type,
@@ -50,6 +75,8 @@ def _build_placement_output(placement: domain.Placement) -> dto.PlacementOutput:
title=channel.title,
username=channel.username,
),
project=project_output,
placement_number=placement_number,
details=_build_placement_details(placement),
)
@@ -73,7 +100,6 @@ async def create_placements(
if project.channel.telegram_id is None:
raise domain.ChannelNotFound(project.channel.id)
invite_link_type = project.purchase_invite_type_default
details = input.details
placements: list[domain.Placement] = []
creatives_by_id: dict[uuid.UUID, domain.Creative] = {}
@@ -106,7 +132,9 @@ async def create_placements(
creative_id=creative.id if creative else None,
channel_id=channel.id,
invite_link=None,
invite_link_type=invite_link_type,
invite_link_type=(channel_details.invite_link_type if channel_details and channel_details.invite_link_type else None)
or (details.invite_link_type if details and details.invite_link_type else None)
or project.purchase_invite_type_default,
status=channel_input.status or domain.PlacementStatus.NO_STATUS,
comment=channel_input.comment,
# Приоритет: channel details > общие details
@@ -138,7 +166,7 @@ async def create_placements(
placement_outputs = []
for placement in placements:
placement_output = _build_placement_output(placement)
placement_output = _build_placement_output(placement, project=project)
placement_outputs.append(
dto.PlacementWithPostsOutput(
**placement_output.model_dump(),

View File

@@ -77,6 +77,30 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d
channel = await self.database.get_channel(channel_id=placement.channel_id)
placement.channel = channel
# Prefetch project channel for output building
if project.channel is None:
project_channel = await self.database.get_channel(channel_id=project.channel_id)
project.channel = project_channel
# Get creative name if exists
creative_name = None
if placement.creative_id:
creative = await self.database.get_creative(input.workspace_id, placement.creative_id)
if creative:
creative_name = creative.name
# Calculate placement number (ordinal number of this placement)
placement_number = await self.database.count_placements_by_project_and_channel(
project.id, placement.channel_id
)
# Count placements created before this one
placements_before = await domain.Placement.filter(
project_id=project.id,
channel_id=placement.channel_id,
created_at__lt=placement.created_at,
).count()
placement_number = placements_before + 1
placement_posts = await self.database.get_workspace_placement_posts(
input.workspace_id,
placement_id=placement.id,
@@ -112,7 +136,12 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d
if placement_post_output is not None:
break
placement_output = _build_placement_output(placement)
placement_output = _build_placement_output(
placement,
project=project,
placement_number=placement_number,
creative_name=creative_name,
)
return dto.PlacementWithPostsOutput(
**placement_output.model_dump(),
placement_post=placement_post_output,

View File

@@ -82,7 +82,7 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
placement_outputs = []
for placement in placements:
placement_output = _build_placement_output(placement)
placement_output = _build_placement_output(placement, project=project)
placement_post_output = None
placement_posts_for_placement = placement_posts_by_placement_id.get(placement.id, [])
if placement_posts_for_placement: