118 lines
4.3 KiB
Python
118 lines
4.3 KiB
Python
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from tortoise import timezone
|
|
|
|
from src import domain, dto
|
|
from src.usecase.purchase.create_placements import _build_placement_output
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def _build_post_output(post: domain.Post) -> dto.PostOutput:
|
|
channel = post.channel
|
|
if not channel:
|
|
raise ValueError(f'Post {post.id} has no channel')
|
|
|
|
return dto.PostOutput(
|
|
id=post.id,
|
|
message_id=post.message_id,
|
|
text=post.text,
|
|
url=post.url,
|
|
deleted_from_channel_at=post.deleted_from_channel_at,
|
|
created_at=post.created_at,
|
|
updated_at=post.updated_at,
|
|
)
|
|
|
|
|
|
def _build_placement_post_output(
|
|
placement_post: domain.PlacementPost,
|
|
subscriptions_count: int,
|
|
views_count: int | None,
|
|
time_on_top: int | None = None,
|
|
) -> dto.PlacementPostOutput | None:
|
|
if not placement_post.post:
|
|
log.warning('PlacementPost %s missing post', placement_post.id)
|
|
return None
|
|
|
|
try:
|
|
post_output = _build_post_output(placement_post.post)
|
|
except ValueError:
|
|
log.warning('PlacementPost %s post missing channel data', placement_post.id)
|
|
return None
|
|
|
|
return dto.PlacementPostOutput(
|
|
subscriptions_count=subscriptions_count,
|
|
views_count=views_count,
|
|
created_at=placement_post.created_at,
|
|
time_on_top=time_on_top,
|
|
post=post_output,
|
|
)
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from .. import Usecase
|
|
|
|
|
|
async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> dto.PlacementWithPostsOutput:
|
|
"""Get single placement by ID (user-managed)"""
|
|
context = await self.ensure_workspace_permission(
|
|
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
|
)
|
|
|
|
project = await self.database.get_project(input.workspace_id, project_id=input.project_id)
|
|
if not project:
|
|
raise domain.ProjectNotFound(input.project_id)
|
|
|
|
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, project.id)
|
|
|
|
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
|
|
if not placement or placement.project_id != project.id:
|
|
raise domain.PlacementNotFound(input.placement_id)
|
|
|
|
# Prefetch channel for output building
|
|
if placement.channel is None:
|
|
channel = await self.database.get_channel(channel_id=placement.channel_id)
|
|
placement.channel = channel
|
|
|
|
placement_posts = await self.database.get_workspace_placement_posts(
|
|
input.workspace_id,
|
|
placement_id=placement.id,
|
|
include_archived=True,
|
|
)
|
|
# Подсчёт подписок по placement_id (один Placement = одна ссылка = один счётчик подписок)
|
|
subscriptions_count = await self.database.count_subscriptions_by_placement(placement.id)
|
|
post_ids = [placement_post.post.id for placement_post in placement_posts if placement_post.post]
|
|
views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
|
|
|
|
placement_post_output = None
|
|
if placement_posts:
|
|
if len(placement_posts) > 1:
|
|
log.warning('Placement %s has %s placement_posts, returning latest', placement.id, len(placement_posts))
|
|
for placement_post in placement_posts:
|
|
views_count = None
|
|
time_on_top = None
|
|
if placement_post.post:
|
|
views_count = views_map.get(placement_post.post.id, (None,))[0]
|
|
# Calculate time_on_top
|
|
post = placement_post.post
|
|
next_post = await self.database.get_next_post_after(post.channel_id, post.message_id)
|
|
if next_post and next_post.published_at and post.published_at:
|
|
time_on_top = int((next_post.published_at - post.published_at).total_seconds())
|
|
elif post.published_at:
|
|
time_on_top = int((timezone.now() - post.published_at).total_seconds())
|
|
placement_post_output = _build_placement_post_output(
|
|
placement_post,
|
|
subscriptions_count,
|
|
views_count,
|
|
time_on_top,
|
|
)
|
|
if placement_post_output is not None:
|
|
break
|
|
|
|
placement_output = _build_placement_output(placement)
|
|
return dto.PlacementWithPostsOutput(
|
|
**placement_output.model_dump(),
|
|
placement_post=placement_post_output,
|
|
)
|