исправил линтеры
This commit is contained in:
@@ -10,12 +10,11 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class FetchChannelResponse(BaseModel):
|
class FetchChannelResponse(BaseModel):
|
||||||
id: str
|
|
||||||
telegram_id: int
|
telegram_id: int
|
||||||
username: str | None
|
username: str | None
|
||||||
title: str | None
|
title: str | None
|
||||||
access_hash: int
|
access_hash: int | None
|
||||||
pts: int
|
pts: int | None
|
||||||
|
|
||||||
|
|
||||||
class ParserClient(Parser):
|
class ParserClient(Parser):
|
||||||
|
|||||||
@@ -549,7 +549,15 @@ class Postgres(DatabaseBase):
|
|||||||
telegram_user_id: uuid.UUID, placement_post_id: uuid.UUID
|
telegram_user_id: uuid.UUID, placement_post_id: uuid.UUID
|
||||||
) -> domain.Subscription | None:
|
) -> domain.Subscription | None:
|
||||||
return await domain.Subscription.get_or_none(
|
return await domain.Subscription.get_or_none(
|
||||||
telegram_user_id=telegram_user_id, placement_post_id=placement_post_id
|
telegram_user_id=telegram_user_id, placement_id=placement_post_id
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_subscription_by_subscriber_and_placement(
|
||||||
|
telegram_user_id: uuid.UUID, placement_id: uuid.UUID
|
||||||
|
) -> domain.Subscription | None:
|
||||||
|
return await domain.Subscription.get_or_none(
|
||||||
|
telegram_user_id=telegram_user_id, placement_id=placement_id
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -563,10 +571,18 @@ class Postgres(DatabaseBase):
|
|||||||
date_from: datetime.datetime | None = None,
|
date_from: datetime.datetime | None = None,
|
||||||
date_to: datetime.datetime | None = None,
|
date_to: datetime.datetime | None = None,
|
||||||
) -> list[domain.Subscription]:
|
) -> list[domain.Subscription]:
|
||||||
|
"""Get subscriptions for placement_posts by finding their parent placements."""
|
||||||
if not placement_post_ids:
|
if not placement_post_ids:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
query = domain.Subscription.filter(placement_post_id__in=placement_post_ids)
|
# Get placement_ids from placement_posts
|
||||||
|
placement_posts = await domain.PlacementPost.filter(id__in=placement_post_ids).all()
|
||||||
|
placement_ids = [pp.placement_id for pp in placement_posts]
|
||||||
|
|
||||||
|
if not placement_ids:
|
||||||
|
return []
|
||||||
|
|
||||||
|
query = domain.Subscription.filter(placement_id__in=placement_ids)
|
||||||
|
|
||||||
if date_from:
|
if date_from:
|
||||||
query = query.filter(created_at__gte=date_from)
|
query = query.filter(created_at__gte=date_from)
|
||||||
@@ -636,7 +652,15 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def count_subscriptions_by_placement_post(placement_post_id: uuid.UUID) -> int:
|
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()
|
"""Count subscriptions for a placement_post by finding its parent placement."""
|
||||||
|
placement_post = await domain.PlacementPost.get_or_none(id=placement_post_id)
|
||||||
|
if not placement_post:
|
||||||
|
return 0
|
||||||
|
return await domain.Subscription.filter(placement_id=placement_post.placement_id).count()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def count_subscriptions_by_placement(placement_id: uuid.UUID) -> int:
|
||||||
|
return await domain.Subscription.filter(placement_id=placement_id).count()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def count_placement_posts_by_creative_batch(creative_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
|
async def count_placement_posts_by_creative_batch(creative_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
|
||||||
@@ -657,20 +681,57 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def count_subscriptions_by_placement_post_batch(placement_post_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
|
async def count_subscriptions_by_placement_post_batch(placement_post_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
|
||||||
|
"""Count subscriptions for placement_posts by finding their parent placements."""
|
||||||
if not placement_post_ids:
|
if not placement_post_ids:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
from tortoise.functions import Count
|
from tortoise.functions import Count
|
||||||
|
|
||||||
|
# Get placement_ids from placement_posts
|
||||||
|
placement_posts = await domain.PlacementPost.filter(id__in=placement_post_ids).all()
|
||||||
|
placement_id_to_post_ids: dict[uuid.UUID, list[uuid.UUID]] = {}
|
||||||
|
for pp in placement_posts:
|
||||||
|
placement_id_to_post_ids.setdefault(pp.placement_id, []).append(pp.id)
|
||||||
|
|
||||||
|
placement_ids = list(placement_id_to_post_ids.keys())
|
||||||
|
if not placement_ids:
|
||||||
|
return dict.fromkeys(placement_post_ids, 0)
|
||||||
|
|
||||||
|
# Count subscriptions by placement
|
||||||
results = (
|
results = (
|
||||||
await domain.Subscription.filter(placement_post_id__in=placement_post_ids)
|
await domain.Subscription.filter(placement_id__in=placement_ids)
|
||||||
.group_by('placement_post_id')
|
.group_by('placement_id')
|
||||||
.annotate(count=Count('id'))
|
.annotate(count=Count('id'))
|
||||||
.values('placement_post_id', 'count')
|
.values('placement_id', 'count')
|
||||||
)
|
)
|
||||||
|
|
||||||
counts = {row['placement_post_id']: row['count'] for row in results}
|
placement_counts = {row['placement_id']: row['count'] for row in results}
|
||||||
return {pid: counts.get(pid, 0) for pid in placement_post_ids}
|
|
||||||
|
# Map back to placement_post_ids
|
||||||
|
post_counts: dict[uuid.UUID, int] = {}
|
||||||
|
for placement_id, post_ids in placement_id_to_post_ids.items():
|
||||||
|
count = placement_counts.get(placement_id, 0)
|
||||||
|
for post_id in post_ids:
|
||||||
|
post_counts[post_id] = count
|
||||||
|
|
||||||
|
return {pid: post_counts.get(pid, 0) for pid in placement_post_ids}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def count_subscriptions_by_placement_batch(placement_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
|
||||||
|
if not placement_ids:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
from tortoise.functions import Count
|
||||||
|
|
||||||
|
results = (
|
||||||
|
await domain.Subscription.filter(placement_id__in=placement_ids)
|
||||||
|
.group_by('placement_id')
|
||||||
|
.annotate(count=Count('id'))
|
||||||
|
.values('placement_id', 'count')
|
||||||
|
)
|
||||||
|
|
||||||
|
counts = {row['placement_id']: row['count'] for row in results}
|
||||||
|
return {pid: counts.get(pid, 0) for pid in placement_ids}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def has_placement_posts_for_creative(creative_id: uuid.UUID) -> bool:
|
async def has_placement_posts_for_creative(creative_id: uuid.UUID) -> bool:
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Sequence
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, InputMediaPhoto, InputMediaVideo
|
from aiogram.types import (
|
||||||
|
InlineKeyboardButton,
|
||||||
|
InlineKeyboardMarkup,
|
||||||
|
InputMediaPhoto,
|
||||||
|
InputMediaVideo,
|
||||||
|
)
|
||||||
|
|
||||||
from shared.telegram_base import TelegramBase
|
from shared.telegram_base import TelegramBase
|
||||||
from src.usecase import TelegramBotWriter
|
from src.usecase import TelegramBotWriter
|
||||||
@@ -85,14 +91,14 @@ class TelegramBot(TelegramBase, TelegramBotWriter):
|
|||||||
async def send_media_group(
|
async def send_media_group(
|
||||||
self,
|
self,
|
||||||
chat_id: int,
|
chat_id: int,
|
||||||
media_items: list[Any],
|
media_items: Sequence[Any],
|
||||||
caption: str | None = None,
|
caption: str | None = None,
|
||||||
parse_mode: str | None = None,
|
parse_mode: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if not media_items:
|
if not media_items:
|
||||||
return
|
return
|
||||||
|
|
||||||
group = []
|
group: list[InputMediaPhoto | InputMediaVideo] = []
|
||||||
for index, item in enumerate(media_items):
|
for index, item in enumerate(media_items):
|
||||||
item_caption = caption if index == 0 else None
|
item_caption = caption if index == 0 else None
|
||||||
if item.media_type == 'photo':
|
if item.media_type == 'photo':
|
||||||
@@ -106,7 +112,8 @@ class TelegramBot(TelegramBase, TelegramBotWriter):
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f'Unsupported media type for group: {item.media_type}')
|
raise ValueError(f'Unsupported media type for group: {item.media_type}')
|
||||||
|
|
||||||
await self.bot.send_media_group(chat_id=chat_id, media=group)
|
# Type ignore: aiogram expects Union type but we only use Photo/Video
|
||||||
|
await self.bot.send_media_group(chat_id=chat_id, media=group) # type: ignore[arg-type]
|
||||||
|
|
||||||
async def edit_message_text(self, text: str, chat_id: int, message_id: int) -> None:
|
async def edit_message_text(self, text: str, chat_id: int, message_id: int) -> None:
|
||||||
await self.bot.edit_message_text(chat_id=chat_id, message_id=message_id, text=text, reply_markup=None)
|
await self.bot.edit_message_text(chat_id=chat_id, message_id=message_id, text=text, reply_markup=None)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from .error import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
from .project import Project
|
from .project import Project
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ class Creative(TimestampedModel):
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
project_id: UUID
|
project_id: UUID
|
||||||
created_by_user_id: UUID | None
|
created_by_user_id: UUID | None
|
||||||
|
media_items: 'fields.ReverseRelation[CreativeMedia]'
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'creative'
|
table = 'creative'
|
||||||
|
|||||||
@@ -125,12 +125,12 @@ from .analytics import (
|
|||||||
from .channel import (
|
from .channel import (
|
||||||
AttachChannelToWorkspaceInput,
|
AttachChannelToWorkspaceInput,
|
||||||
ChannelOutput,
|
ChannelOutput,
|
||||||
|
CreateChannelInput,
|
||||||
|
CreateChannelResult,
|
||||||
|
CreateChannelsInput,
|
||||||
|
CreateChannelsOutput,
|
||||||
GetChannelInput,
|
GetChannelInput,
|
||||||
GetChannelsInput,
|
GetChannelsInput,
|
||||||
CreateChannelInput,
|
|
||||||
CreateChannelsInput,
|
|
||||||
CreateChannelResult,
|
|
||||||
CreateChannelsOutput,
|
|
||||||
GetChannelsOutput,
|
GetChannelsOutput,
|
||||||
)
|
)
|
||||||
from .creative import (
|
from .creative import (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import typing
|
import typing
|
||||||
|
from collections.abc import Sequence
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@@ -94,7 +95,7 @@ class TelegramBotWriter(typing.Protocol):
|
|||||||
async def send_media_group(
|
async def send_media_group(
|
||||||
self,
|
self,
|
||||||
chat_id: int,
|
chat_id: int,
|
||||||
media_items: list[MediaItem],
|
media_items: Sequence[MediaItem],
|
||||||
caption: str | None = None,
|
caption: str | None = None,
|
||||||
parse_mode: str | None = None,
|
parse_mode: str | None = None,
|
||||||
) -> None: ...
|
) -> None: ...
|
||||||
|
|||||||
@@ -86,13 +86,17 @@ async def get_overview_analytics(
|
|||||||
elif placement_date >= previous_period_start and placement_date < input.date_from:
|
elif placement_date >= previous_period_start and placement_date < input.date_from:
|
||||||
previous_placements.append(placement_post)
|
previous_placements.append(placement_post)
|
||||||
|
|
||||||
placement_ids = [p.id for p in placements]
|
placement_post_ids = [p.id for p in placements]
|
||||||
subscriptions = await self.database.get_subscriptions_for_placement_posts(
|
subscriptions = await self.database.get_subscriptions_for_placement_posts(
|
||||||
placement_ids, date_from=previous_period_start, date_to=input.date_to
|
placement_post_ids, date_from=previous_period_start, date_to=input.date_to
|
||||||
)
|
)
|
||||||
|
|
||||||
subs_per_placement_current: dict[UUID, int] = defaultdict(int)
|
# Map placement_post_id to placement_id for subscription aggregation
|
||||||
subs_per_placement_previous: dict[UUID, int] = defaultdict(int)
|
# Since subscriptions link to placement, we need to map back to placement_post
|
||||||
|
placement_post_to_placement: dict[UUID, UUID] = {p.id: p.placement_id for p in placements}
|
||||||
|
|
||||||
|
subs_per_placement_post_current: dict[UUID, int] = defaultdict(int)
|
||||||
|
subs_per_placement_post_previous: dict[UUID, int] = defaultdict(int)
|
||||||
subs_per_day_current: dict[datetime.date, int] = defaultdict(int)
|
subs_per_day_current: dict[datetime.date, int] = defaultdict(int)
|
||||||
|
|
||||||
for sub in subscriptions:
|
for sub in subscriptions:
|
||||||
@@ -100,17 +104,24 @@ async def get_overview_analytics(
|
|||||||
if created_at is None:
|
if created_at is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Find which placement_post(s) this subscription belongs to via placement_id
|
||||||
|
# A placement can have multiple placement_posts, so we count it for each
|
||||||
|
for pp_id, p_id in placement_post_to_placement.items():
|
||||||
|
if p_id == sub.placement_id:
|
||||||
|
if created_at >= input.date_from and created_at <= input.date_to:
|
||||||
|
subs_per_placement_post_current[pp_id] += 1
|
||||||
|
elif created_at >= previous_period_start and created_at < input.date_from:
|
||||||
|
subs_per_placement_post_previous[pp_id] += 1
|
||||||
|
|
||||||
|
# Count each subscription only once for daily stats
|
||||||
if created_at >= input.date_from and created_at <= input.date_to:
|
if created_at >= input.date_from and created_at <= input.date_to:
|
||||||
subs_per_placement_current[sub.placement_post_id] += 1
|
|
||||||
subs_per_day_current[created_at.date()] += 1
|
subs_per_day_current[created_at.date()] += 1
|
||||||
elif created_at >= previous_period_start and created_at < input.date_from:
|
|
||||||
subs_per_placement_previous[sub.placement_post_id] += 1
|
|
||||||
|
|
||||||
post_ids = [p.post.id for p in placements if p.post]
|
post_ids = [p.post.id for p in placements if p.post]
|
||||||
views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
|
views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
|
||||||
|
|
||||||
def _aggregate_totals(
|
def _aggregate_totals(
|
||||||
placement_list: list[domain.PlacementPost], subs_per_placement: dict[UUID, int]
|
placement_list: list[domain.PlacementPost], subs_per_placement_post: dict[UUID, int]
|
||||||
) -> tuple[float, int, int]:
|
) -> tuple[float, int, int]:
|
||||||
total_cost = 0.0
|
total_cost = 0.0
|
||||||
total_subscriptions = 0
|
total_subscriptions = 0
|
||||||
@@ -119,7 +130,7 @@ async def get_overview_analytics(
|
|||||||
for placement_post in placement_list:
|
for placement_post in placement_list:
|
||||||
total_cost += _get_cost(placement_post)
|
total_cost += _get_cost(placement_post)
|
||||||
|
|
||||||
subs = subs_per_placement.get(placement_post.id, 0)
|
subs = subs_per_placement_post.get(placement_post.id, 0)
|
||||||
total_subscriptions += subs
|
total_subscriptions += subs
|
||||||
|
|
||||||
if placement_post.post and placement_post.post.id in views_map:
|
if placement_post.post and placement_post.post.id in views_map:
|
||||||
@@ -127,8 +138,12 @@ async def get_overview_analytics(
|
|||||||
|
|
||||||
return total_cost, total_subscriptions, total_views
|
return total_cost, total_subscriptions, total_views
|
||||||
|
|
||||||
current_cost, current_subs, current_views = _aggregate_totals(current_placements, subs_per_placement_current)
|
current_cost, current_subs, current_views = _aggregate_totals(
|
||||||
previous_cost, previous_subs, previous_views = _aggregate_totals(previous_placements, subs_per_placement_previous)
|
current_placements, subs_per_placement_post_current
|
||||||
|
)
|
||||||
|
previous_cost, previous_subs, previous_views = _aggregate_totals(
|
||||||
|
previous_placements, subs_per_placement_post_previous
|
||||||
|
)
|
||||||
|
|
||||||
# Apply hide_subscriptions filter
|
# Apply hide_subscriptions filter
|
||||||
if hide_subscriptions:
|
if hide_subscriptions:
|
||||||
@@ -159,7 +174,7 @@ async def get_overview_analytics(
|
|||||||
cost_value = _get_cost(placement_post)
|
cost_value = _get_cost(placement_post)
|
||||||
cost_per_day[placement_day] += cost_value
|
cost_per_day[placement_day] += cost_value
|
||||||
project_spending_map[placement.project_id] += cost_value
|
project_spending_map[placement.project_id] += cost_value
|
||||||
subs = subs_per_placement_current.get(placement_post.id, 0)
|
subs = subs_per_placement_post_current.get(placement_post.id, 0)
|
||||||
|
|
||||||
channel_id = placement.channel_id
|
channel_id = placement.channel_id
|
||||||
if channel_id not in channel_stats:
|
if channel_id not in channel_stats:
|
||||||
|
|||||||
@@ -124,7 +124,11 @@ async def tg_add_project(self: 'Usecase', input: dto.ConnectProjectInput) -> dto
|
|||||||
if channel.telegram_id != telegram_id:
|
if channel.telegram_id != telegram_id:
|
||||||
channel.telegram_id = telegram_id
|
channel.telegram_id = telegram_id
|
||||||
updated = True
|
updated = True
|
||||||
if parser_response and parser_response.access_hash is not None and channel.access_hash != parser_response.access_hash:
|
if (
|
||||||
|
parser_response
|
||||||
|
and parser_response.access_hash is not None
|
||||||
|
and channel.access_hash != parser_response.access_hash
|
||||||
|
):
|
||||||
channel.access_hash = parser_response.access_hash
|
channel.access_hash = parser_response.access_hash
|
||||||
updated = True
|
updated = True
|
||||||
if parser_response and parser_response.pts is not None and channel.pts != parser_response.pts:
|
if parser_response and parser_response.pts is not None and channel.pts != parser_response.pts:
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ async def handle_subscription(
|
|||||||
first_name: str | None = None,
|
first_name: str | None = None,
|
||||||
last_name: str | None = None,
|
last_name: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
placement = await self.database.get_placement_by_invite_link(invite_link)
|
placement_post = await self.database.get_placement_post_by_invite_link(invite_link)
|
||||||
if not placement:
|
if not placement_post or not placement_post.placement:
|
||||||
log.warning('Placement not found for invite_link: %s', invite_link)
|
log.warning('PlacementPost not found for invite_link: %s', invite_link)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
placement = placement_post.placement
|
||||||
|
|
||||||
subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id)
|
subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id)
|
||||||
if not subscriber:
|
if not subscriber:
|
||||||
subscriber = domain.TelegramUser(
|
subscriber = domain.TelegramUser(
|
||||||
@@ -57,6 +59,7 @@ async def handle_subscription(
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Проверяем, была ли раньше подписка через ЭТОТ placement (для реактивации)
|
# Проверяем, была ли раньше подписка через ЭТОТ placement (для реактивации)
|
||||||
|
# Note: Subscription now links to placement directly, not placement_post
|
||||||
existing_sub = await self.database.get_subscription_by_subscriber_and_placement(
|
existing_sub = await self.database.get_subscription_by_subscriber_and_placement(
|
||||||
subscriber.id, placement.id
|
subscriber.id, placement.id
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user