611 lines
23 KiB
Python
611 lines
23 KiB
Python
import datetime
|
|
import logging
|
|
import typing
|
|
import uuid
|
|
|
|
from tortoise import timezone
|
|
from tortoise.transactions import in_transaction
|
|
|
|
from shared.datebase_base import DatabaseBase
|
|
from src import domain
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class Postgres(DatabaseBase):
|
|
@staticmethod
|
|
def transaction() -> typing.AsyncContextManager[None]:
|
|
return in_transaction()
|
|
|
|
@staticmethod
|
|
async def create_user(user: domain.User) -> None:
|
|
await user.save()
|
|
|
|
@staticmethod
|
|
async def get_telegram_user(
|
|
telegram_user_id: uuid.UUID | None = None, telegram_id: int | None = None
|
|
) -> domain.TelegramUser | None:
|
|
if telegram_user_id:
|
|
return await domain.TelegramUser.get_or_none(id=telegram_user_id)
|
|
if telegram_id:
|
|
return await domain.TelegramUser.get_or_none(telegram_id=telegram_id)
|
|
|
|
raise ValueError('Either telegram_user_id or telegram_id must be provided')
|
|
|
|
@staticmethod
|
|
async def create_telegram_user(telegram_user: domain.TelegramUser) -> None:
|
|
await telegram_user.save()
|
|
|
|
@staticmethod
|
|
async def update_telegram_user(telegram_user: domain.TelegramUser) -> None:
|
|
await telegram_user.save()
|
|
|
|
@staticmethod
|
|
async def create_workspace(workspace: domain.Workspace) -> None:
|
|
await workspace.save()
|
|
|
|
@staticmethod
|
|
async def update_workspace(workspace: domain.Workspace) -> None:
|
|
await workspace.save()
|
|
|
|
@staticmethod
|
|
async def delete_workspace(workspace_id: uuid.UUID) -> None:
|
|
await domain.Workspace.filter(id=workspace_id).delete()
|
|
|
|
@staticmethod
|
|
async def add_user_to_workspace(workspace_id: uuid.UUID, user_id: uuid.UUID) -> domain.WorkspaceUser:
|
|
return await domain.WorkspaceUser.create(
|
|
workspace_id=workspace_id,
|
|
user_id=user_id,
|
|
status=domain.WorkspaceUserStatus.ACTIVE,
|
|
)
|
|
|
|
@staticmethod
|
|
async def update_workspace_user(workspace_user: domain.WorkspaceUser) -> None:
|
|
await workspace_user.save()
|
|
|
|
@staticmethod
|
|
async def get_user_workspaces(user_id: uuid.UUID) -> list[domain.WorkspaceUser]:
|
|
return (
|
|
await domain.WorkspaceUser.filter(user_id=user_id)
|
|
.prefetch_related('workspace')
|
|
.order_by('created_at')
|
|
.all()
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_workspace(workspace_id: uuid.UUID) -> domain.Workspace | None:
|
|
return await domain.Workspace.get_or_none(id=workspace_id)
|
|
|
|
@staticmethod
|
|
async def get_workspace_for_user(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
|
|
|
|
@staticmethod
|
|
async def get_default_workspace_for_user(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
|
|
|
|
@staticmethod
|
|
async def get_workspace_membership(workspace_id: uuid.UUID, user_id: uuid.UUID) -> domain.WorkspaceUser | None:
|
|
return (
|
|
await domain.WorkspaceUser.filter(workspace_id=workspace_id, user_id=user_id)
|
|
.prefetch_related('workspace', 'user', 'user__telegram_user', 'permissions', 'permission_scopes')
|
|
.first()
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_workspace_members(workspace_id: uuid.UUID) -> list[domain.WorkspaceUser]:
|
|
return (
|
|
await domain.WorkspaceUser.filter(workspace_id=workspace_id)
|
|
.prefetch_related('workspace', 'user__telegram_user', 'permissions', 'permission_scopes')
|
|
.order_by('created_at')
|
|
.all()
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_workspace_member(workspace_user_id: uuid.UUID) -> domain.WorkspaceUser | None:
|
|
return (
|
|
await domain.WorkspaceUser.filter(id=workspace_user_id)
|
|
.prefetch_related('workspace', 'user__telegram_user', 'permissions', 'permission_scopes')
|
|
.first()
|
|
)
|
|
|
|
@staticmethod
|
|
async def create_workspace_invite(invite: domain.WorkspaceInvite) -> None:
|
|
await invite.save()
|
|
|
|
@staticmethod
|
|
async def update_workspace_invite(invite: domain.WorkspaceInvite) -> None:
|
|
await invite.save()
|
|
|
|
@staticmethod
|
|
async def get_workspace_invite(invite_id: uuid.UUID) -> domain.WorkspaceInvite | None:
|
|
return (
|
|
await domain.WorkspaceInvite.filter(id=invite_id)
|
|
.prefetch_related('workspace', 'user__telegram_user', 'invited_by__telegram_user')
|
|
.first()
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_workspace_invite_by_user(
|
|
workspace_id: uuid.UUID, user_id: uuid.UUID
|
|
) -> domain.WorkspaceInvite | None:
|
|
return await domain.WorkspaceInvite.get_or_none(workspace_id=workspace_id, user_id=user_id)
|
|
|
|
@staticmethod
|
|
async def get_workspace_invites(workspace_id: uuid.UUID) -> list[domain.WorkspaceInvite]:
|
|
return (
|
|
await domain.WorkspaceInvite.filter(workspace_id=workspace_id)
|
|
.prefetch_related('user__telegram_user', 'invited_by__telegram_user')
|
|
.order_by('created_at')
|
|
.all()
|
|
)
|
|
|
|
@staticmethod
|
|
async def set_workspace_user_permissions(
|
|
workspace_user_id: uuid.UUID,
|
|
global_permissions: set[domain.PermissionKey],
|
|
scoped_permissions: list[tuple[domain.PermissionKey, domain.PermissionScopeType, uuid.UUID]],
|
|
) -> None:
|
|
await domain.WorkspaceUserPermission.filter(workspace_user_id=workspace_user_id).delete()
|
|
await domain.WorkspaceUserPermissionScope.filter(workspace_user_id=workspace_user_id).delete()
|
|
|
|
if global_permissions:
|
|
await domain.WorkspaceUserPermission.bulk_create(
|
|
[
|
|
domain.WorkspaceUserPermission(workspace_user_id=workspace_user_id, permission=permission)
|
|
for permission in global_permissions
|
|
]
|
|
)
|
|
|
|
if scoped_permissions:
|
|
scope_objects = []
|
|
for permission, scope_type, scope_id in scoped_permissions:
|
|
kwargs = {
|
|
'workspace_user_id': workspace_user_id,
|
|
'permission': permission,
|
|
}
|
|
if scope_type == domain.PermissionScopeType.PROJECT:
|
|
kwargs['project_id'] = scope_id
|
|
elif scope_type == domain.PermissionScopeType.CREATIVE:
|
|
kwargs['creative_id'] = scope_id
|
|
elif scope_type == domain.PermissionScopeType.PLACEMENT:
|
|
kwargs['placement_id'] = scope_id
|
|
elif scope_type == domain.PermissionScopeType.CHANNEL:
|
|
kwargs['channel_id'] = scope_id
|
|
|
|
scope_objects.append(domain.WorkspaceUserPermissionScope(**kwargs))
|
|
|
|
await domain.WorkspaceUserPermissionScope.bulk_create(scope_objects)
|
|
|
|
@staticmethod
|
|
async def create_login_token(login_token: domain.LoginToken) -> None:
|
|
await login_token.save()
|
|
|
|
@staticmethod
|
|
async def create_creative(creative: domain.Creative) -> None:
|
|
await creative.save()
|
|
|
|
@staticmethod
|
|
async def get_user(user_id: uuid.UUID | None = None, telegram_id: int | None = None) -> domain.User | None:
|
|
if user_id:
|
|
return await domain.User.filter(id=user_id).prefetch_related('telegram_user').first()
|
|
elif telegram_id:
|
|
return (
|
|
await domain.User.filter(telegram_user__telegram_id=telegram_id)
|
|
.prefetch_related('telegram_user')
|
|
.first()
|
|
)
|
|
|
|
raise ValueError('Either user_id or telegram_id must be provided')
|
|
|
|
@staticmethod
|
|
async def get_user_by_username(username: str) -> domain.User | None:
|
|
return (
|
|
await domain.User.filter(telegram_user__username__iexact=username).prefetch_related('telegram_user').first()
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_login_token(token: str) -> domain.LoginToken | None:
|
|
return await domain.LoginToken.get_or_none(token=token)
|
|
|
|
@staticmethod
|
|
async def mark_token_as_used(token: str) -> None:
|
|
updated = await domain.LoginToken.filter(token=token, used_at__isnull=True).update(used_at=timezone.now())
|
|
|
|
if updated == 0:
|
|
raise domain.LoginTokenAlreadyUsed() # либо нет токена, либо он уже использован
|
|
|
|
@staticmethod
|
|
async def get_channel(
|
|
channel_id: uuid.UUID | None = None, telegram_id: int | None = None, username: str | None = None
|
|
) -> domain.Channel | None:
|
|
if channel_id:
|
|
return await domain.Channel.get_or_none(id=channel_id)
|
|
if telegram_id:
|
|
return await domain.Channel.get_or_none(telegram_id=telegram_id)
|
|
if username:
|
|
return await domain.Channel.filter(username__iexact=username).first()
|
|
return None
|
|
|
|
@staticmethod
|
|
async def create_channel(channel: domain.Channel) -> None:
|
|
await channel.save()
|
|
|
|
@staticmethod
|
|
async def update_channel(channel: domain.Channel) -> None:
|
|
await channel.save()
|
|
|
|
@staticmethod
|
|
async def search_channels(username_query: str | None = None) -> list[domain.Channel]:
|
|
query = domain.Channel.all()
|
|
|
|
if username_query:
|
|
# Частичный поиск (case-insensitive)
|
|
query = query.filter(username__icontains=username_query)
|
|
|
|
return await query.all()
|
|
|
|
@staticmethod
|
|
async def get_project(
|
|
workspace_id: uuid.UUID, project_id: uuid.UUID | None = None, channel_id: uuid.UUID | None = None
|
|
) -> domain.Project | None:
|
|
query = domain.Project.filter(workspace_id=workspace_id, deleted_at__isnull=True).prefetch_related('channel')
|
|
if project_id:
|
|
query = query.filter(id=project_id)
|
|
if channel_id:
|
|
query = query.filter(channel_id=channel_id)
|
|
return await query.first()
|
|
|
|
@staticmethod
|
|
async def get_project_for_user_by_telegram(user_id: uuid.UUID, channel_telegram_id: int) -> domain.Project | None:
|
|
return (
|
|
await domain.Project.filter(
|
|
channel__telegram_id=channel_telegram_id, workspace__workspace_users__user_id=user_id
|
|
)
|
|
.prefetch_related('channel')
|
|
.first()
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_project_by_channel_telegram(channel_telegram_id: int) -> domain.Project | None:
|
|
return await domain.Project.filter(channel__telegram_id=channel_telegram_id).prefetch_related('channel').first()
|
|
|
|
@staticmethod
|
|
async def create_project(project: domain.Project) -> None:
|
|
await project.save()
|
|
|
|
@staticmethod
|
|
async def update_project(project: domain.Project) -> None:
|
|
await project.save()
|
|
|
|
@staticmethod
|
|
async def get_workspace_projects(
|
|
workspace_id: uuid.UUID,
|
|
allowed_project_ids: set[uuid.UUID] | None = None,
|
|
include_archived: bool = False,
|
|
) -> list[domain.Project]:
|
|
query = domain.Project.filter(workspace_id=workspace_id, deleted_at__isnull=True).prefetch_related('channel')
|
|
|
|
if allowed_project_ids is not None:
|
|
if not allowed_project_ids:
|
|
return []
|
|
query = query.filter(id__in=list(allowed_project_ids))
|
|
|
|
if not include_archived:
|
|
query = query.filter(status=domain.ProjectStatus.ACTIVE)
|
|
|
|
return await query.order_by('created_at').all()
|
|
|
|
@staticmethod
|
|
async def archive_project(workspace_id: uuid.UUID, project_id: uuid.UUID) -> None:
|
|
project = await domain.Project.get_or_none(id=project_id, workspace_id=workspace_id)
|
|
if not project:
|
|
raise domain.ProjectNotFound()
|
|
project.status = domain.ProjectStatus.ARCHIVED
|
|
await project.save()
|
|
|
|
@staticmethod
|
|
async def delete_project(workspace_id: uuid.UUID, project_id: uuid.UUID) -> None:
|
|
project = await domain.Project.get_or_none(id=project_id, workspace_id=workspace_id)
|
|
if not project:
|
|
raise domain.ProjectNotFound()
|
|
import datetime
|
|
|
|
project.deleted_at = datetime.datetime.now()
|
|
await project.save()
|
|
|
|
@staticmethod
|
|
async def check_channel_exists_in_workspace(channel_id: uuid.UUID, workspace_id: uuid.UUID) -> bool:
|
|
project = await domain.Project.get_or_none(channel_id=channel_id, workspace_id=workspace_id)
|
|
return project is not None
|
|
|
|
@staticmethod
|
|
async def get_creative(workspace_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
|
|
return await domain.Creative.get_or_none(id=creative_id, project__workspace_id=workspace_id).prefetch_related(
|
|
'project', 'project__channel'
|
|
)
|
|
|
|
@staticmethod
|
|
async def update_creative(creative: domain.Creative) -> None:
|
|
await creative.save()
|
|
|
|
@staticmethod
|
|
async def get_workspace_creatives(
|
|
workspace_id: uuid.UUID,
|
|
project_id: uuid.UUID | None = None,
|
|
include_archived: bool = False,
|
|
allowed_project_ids: set[uuid.UUID] | None = None,
|
|
) -> list[domain.Creative]:
|
|
query = domain.Creative.filter(project__workspace_id=workspace_id)
|
|
|
|
if project_id:
|
|
query = query.filter(project_id=project_id)
|
|
elif allowed_project_ids is not None:
|
|
if not allowed_project_ids:
|
|
return []
|
|
query = query.filter(project_id__in=list(allowed_project_ids))
|
|
|
|
if not include_archived:
|
|
query = query.filter(status=domain.CreativeStatus.ACTIVE)
|
|
|
|
return await query.prefetch_related('project', 'project__channel').order_by('-created_at').all()
|
|
|
|
@staticmethod
|
|
async def delete_creative(creative_id: uuid.UUID) -> None:
|
|
await domain.Creative.filter(id=creative_id).delete()
|
|
|
|
@staticmethod
|
|
async def create_placement(placement: domain.Placement) -> None:
|
|
await placement.save()
|
|
|
|
@staticmethod
|
|
async def get_placement(workspace_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None:
|
|
return await domain.Placement.get_or_none(id=placement_id, project__workspace_id=workspace_id).prefetch_related(
|
|
'project', 'project__channel', 'channel', 'creative'
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_project_placements(
|
|
workspace_id: uuid.UUID, project_id: uuid.UUID, include_archived: bool = False
|
|
) -> list[domain.Placement]:
|
|
query = domain.Placement.filter(project__workspace_id=workspace_id, project_id=project_id)
|
|
|
|
if not include_archived:
|
|
query = query.filter(
|
|
status__in=[
|
|
domain.PlacementStatus.PLANNED,
|
|
domain.PlacementStatus.APPROVED,
|
|
domain.PlacementStatus.IN_PROGRESS,
|
|
domain.PlacementStatus.COMPLETED,
|
|
]
|
|
)
|
|
|
|
return (
|
|
await query.prefetch_related('project', 'project__channel', 'channel', 'creative')
|
|
.order_by('-created_at')
|
|
.all()
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_placement_post(workspace_id: uuid.UUID, placement_post_id: uuid.UUID) -> domain.PlacementPost | None:
|
|
return await domain.PlacementPost.get_or_none(
|
|
id=placement_post_id, placement__project__workspace_id=workspace_id
|
|
).prefetch_related(
|
|
'placement',
|
|
'placement__project',
|
|
'placement__project__channel',
|
|
'placement__channel',
|
|
'placement__creative',
|
|
'post',
|
|
'post__channel',
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_workspace_placement_posts(
|
|
workspace_id: uuid.UUID,
|
|
project_id: uuid.UUID | None = None,
|
|
placement_channel_id: uuid.UUID | None = None,
|
|
creative_id: uuid.UUID | None = None,
|
|
include_archived: bool = False,
|
|
allowed_project_ids: set[uuid.UUID] | None = None,
|
|
date_from: datetime.datetime | None = None,
|
|
date_to: datetime.datetime | None = None,
|
|
) -> list[domain.PlacementPost]:
|
|
query = domain.PlacementPost.filter(placement__project__workspace_id=workspace_id)
|
|
|
|
if project_id:
|
|
query = query.filter(placement__project_id=project_id)
|
|
elif allowed_project_ids is not None:
|
|
if not allowed_project_ids:
|
|
return []
|
|
query = query.filter(placement__project_id__in=list(allowed_project_ids))
|
|
if placement_channel_id:
|
|
query = query.filter(placement__channel_id=placement_channel_id)
|
|
if creative_id:
|
|
query = query.filter(placement__creative_id=creative_id)
|
|
|
|
if date_from:
|
|
query = query.filter(created_at__gte=date_from)
|
|
if date_to:
|
|
query = query.filter(created_at__lte=date_to)
|
|
|
|
if not include_archived:
|
|
query = query.filter(status=domain.PlacementPostStatus.ACTIVE)
|
|
|
|
return (
|
|
await query.prefetch_related(
|
|
'placement',
|
|
'placement__project',
|
|
'placement__project__channel',
|
|
'placement__channel',
|
|
'placement__creative',
|
|
'post',
|
|
'post__channel',
|
|
)
|
|
.order_by('-created_at')
|
|
.all()
|
|
)
|
|
|
|
@staticmethod
|
|
async def create_placement_post(placement_post: domain.PlacementPost) -> None:
|
|
await placement_post.save()
|
|
|
|
@staticmethod
|
|
async def get_placement_post_by_invite_link(invite_link: str) -> domain.PlacementPost | None:
|
|
return await domain.PlacementPost.get_or_none(placement__invite_link=invite_link).prefetch_related(
|
|
'placement',
|
|
'placement__project',
|
|
'placement__project__channel',
|
|
'placement__channel',
|
|
'placement__creative',
|
|
)
|
|
|
|
# Subscription methods
|
|
@staticmethod
|
|
async def create_subscription(subscription: domain.Subscription) -> None:
|
|
await subscription.save()
|
|
|
|
@staticmethod
|
|
async def get_subscription_by_subscriber_and_placement_post(
|
|
telegram_user_id: uuid.UUID, placement_post_id: uuid.UUID
|
|
) -> domain.Subscription | None:
|
|
return await domain.Subscription.get_or_none(
|
|
telegram_user_id=telegram_user_id, placement_post_id=placement_post_id
|
|
)
|
|
|
|
@staticmethod
|
|
async def update_subscription(subscription: domain.Subscription) -> None:
|
|
await subscription.save()
|
|
|
|
@staticmethod
|
|
async def get_subscriptions_for_placement_posts(
|
|
placement_post_ids: list[uuid.UUID],
|
|
*,
|
|
date_from: datetime.datetime | None = None,
|
|
date_to: datetime.datetime | None = None,
|
|
) -> list[domain.Subscription]:
|
|
if not placement_post_ids:
|
|
return []
|
|
|
|
query = domain.Subscription.filter(placement_post_id__in=placement_post_ids)
|
|
|
|
if date_from:
|
|
query = query.filter(created_at__gte=date_from)
|
|
if date_to:
|
|
query = query.filter(created_at__lte=date_to)
|
|
|
|
return await query.all()
|
|
|
|
@staticmethod
|
|
async def get_active_subscriptions_by_subscriber_and_project(
|
|
telegram_user_id: uuid.UUID, project_id: uuid.UUID
|
|
) -> list[domain.Subscription]:
|
|
return (
|
|
await domain.Subscription.filter(
|
|
telegram_user_id=telegram_user_id,
|
|
placement_post__placement__project_id=project_id,
|
|
status=domain.SubscriptionStatus.ACTIVE,
|
|
)
|
|
.prefetch_related('placement_post', 'telegram_user')
|
|
.all()
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_active_subscription_by_subscriber_and_project(
|
|
telegram_user_id: uuid.UUID, project_id: uuid.UUID
|
|
) -> domain.Subscription | None:
|
|
return (
|
|
await domain.Subscription.filter(
|
|
telegram_user_id=telegram_user_id,
|
|
placement_post__placement__project_id=project_id,
|
|
status=domain.SubscriptionStatus.ACTIVE,
|
|
)
|
|
.prefetch_related('placement_post', 'telegram_user')
|
|
.first()
|
|
)
|
|
|
|
@staticmethod
|
|
async def get_views_history(
|
|
post_id: uuid.UUID, *, from_date: datetime.datetime | None = None, to_date: datetime.datetime | None = None
|
|
) -> list[domain.PostViewsHistory]:
|
|
query = domain.PostViewsHistory.filter(post_id=post_id)
|
|
|
|
if from_date:
|
|
query = query.filter(fetched_at__gte=from_date)
|
|
if to_date:
|
|
query = query.filter(fetched_at__lte=to_date)
|
|
|
|
return await query.order_by('fetched_at').all()
|
|
|
|
@staticmethod
|
|
async def get_latest_views_data_batch(post_ids: list[uuid.UUID]) -> dict[uuid.UUID, tuple[int, datetime.datetime]]:
|
|
if not post_ids:
|
|
return {}
|
|
|
|
results: dict[uuid.UUID, tuple[int, datetime.datetime]] = {}
|
|
for post_id in post_ids:
|
|
latest = await domain.PostViewsHistory.filter(post_id=post_id).order_by('-fetched_at').first()
|
|
if latest:
|
|
results[post_id] = (latest.views_count, latest.fetched_at)
|
|
|
|
return results
|
|
|
|
# Count methods
|
|
@staticmethod
|
|
async def count_placement_posts_by_creative(creative_id: uuid.UUID) -> int:
|
|
return await domain.PlacementPost.filter(placement__creative_id=creative_id).count()
|
|
|
|
@staticmethod
|
|
async def count_subscriptions_by_placement_post(placement_post_id: uuid.UUID) -> int:
|
|
return await domain.Subscription.filter(placement_post_id=placement_post_id).count()
|
|
|
|
@staticmethod
|
|
async def count_placement_posts_by_creative_batch(creative_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
|
|
if not creative_ids:
|
|
return {}
|
|
|
|
from tortoise.functions import Count
|
|
|
|
results = (
|
|
await domain.PlacementPost.filter(placement__creative_id__in=creative_ids)
|
|
.group_by('placement__creative_id')
|
|
.annotate(count=Count('id'))
|
|
.values('placement__creative_id', 'count')
|
|
)
|
|
|
|
counts = {row['placement__creative_id']: row['count'] for row in results}
|
|
return {cid: counts.get(cid, 0) for cid in creative_ids}
|
|
|
|
@staticmethod
|
|
async def count_subscriptions_by_placement_post_batch(placement_post_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
|
|
if not placement_post_ids:
|
|
return {}
|
|
|
|
from tortoise.functions import Count
|
|
|
|
results = (
|
|
await domain.Subscription.filter(placement_post_id__in=placement_post_ids)
|
|
.group_by('placement_post_id')
|
|
.annotate(count=Count('id'))
|
|
.values('placement_post_id', 'count')
|
|
)
|
|
|
|
counts = {row['placement_post_id']: row['count'] for row in results}
|
|
return {pid: counts.get(pid, 0) for pid in placement_post_ids}
|
|
|
|
@staticmethod
|
|
async def has_placement_posts_for_creative(creative_id: uuid.UUID) -> bool:
|
|
return await domain.PlacementPost.filter(placement__creative_id=creative_id).exists()
|