From 2d35588a8538cfba3a198dae391886131af957f9 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Tue, 27 Jan 2026 19:29:10 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=B0=20=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migrations/models/10_20260127194137_update.py | 149 ++++++++++++++++++ src/adapter/postgres.py | 4 + src/domain/creative.py | 5 + src/domain/workspace.py | 23 ++- src/domain/workspace_permissions.py | 56 +++---- src/dto/workspace.py | 7 +- src/main.py | 1 + src/usecase/__init__.py | 15 ++ .../analytics/get_channel_analytics.py | 17 +- .../analytics/get_creatives_analytics.py | 24 +-- .../analytics/get_overview_analytics.py | 32 ++-- .../analytics/get_placements_analytics.py | 11 +- .../analytics/get_projects_analytics.py | 40 +++-- .../analytics/get_spending_analytics.py | 15 +- src/usecase/creative/create_creative.py | 3 +- src/usecase/creative/delete_creative.py | 4 +- src/usecase/creative/get_creative.py | 4 +- src/usecase/creative/get_creatives.py | 6 +- src/usecase/creative/update_creative.py | 4 +- .../controller/worker/channel_worker.go | 2 +- .../controller/worker/views_worker.go | 2 +- 21 files changed, 309 insertions(+), 115 deletions(-) create mode 100644 migrations/models/10_20260127194137_update.py diff --git a/migrations/models/10_20260127194137_update.py b/migrations/models/10_20260127194137_update.py new file mode 100644 index 0000000..375c6a5 --- /dev/null +++ b/migrations/models/10_20260127194137_update.py @@ -0,0 +1,149 @@ +from tortoise import BaseDBAsyncClient + +RUN_IN_TRANSACTION = True + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + DROP INDEX IF EXISTS "idx_workspace_u_channel_d94b48"; + ALTER TABLE "workspace_user_permission_scope" DROP CONSTRAINT IF EXISTS "fk_workspac_channel_e039fdfd"; + ALTER TABLE "creative" ADD "created_by_user_id" UUID; + COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full +PROJECTS_READ: projects_read +PROJECTS_WRITE: projects_write +CREATIVES_READ: creatives_read +CREATIVES_WRITE: creatives_write +PLACEMENTS_READ: placements_read +PLACEMENTS_WRITE: placements_write +ANALYTICS_READ: analytics_read +ANALYTICS_WITHOUT_CLICKS: analytics_without_clicks +ANALYTICS_OWN_CREATIVES: analytics_own_creatives'; + ALTER TABLE "workspace_user_permission_scope" DROP COLUMN "channel_id"; + COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full +PROJECTS_READ: projects_read +PROJECTS_WRITE: projects_write +CREATIVES_READ: creatives_read +CREATIVES_WRITE: creatives_write +PLACEMENTS_READ: placements_read +PLACEMENTS_WRITE: placements_write +ANALYTICS_READ: analytics_read +ANALYTICS_WITHOUT_CLICKS: analytics_without_clicks +ANALYTICS_OWN_CREATIVES: analytics_own_creatives'; + ALTER TABLE "creative" ADD CONSTRAINT "fk_creative_user_2ed7d825" FOREIGN KEY ("created_by_user_id") REFERENCES "user" ("id") ON DELETE SET NULL; + CREATE INDEX IF NOT EXISTS "idx_creative_created_2d9564" ON "creative" ("created_by_user_id"); + CREATE UNIQUE INDEX IF NOT EXISTS "uid_creative_me_creativ_b07496" ON "creative_media" ("creative_id", "position");""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + DROP INDEX IF EXISTS "uid_creative_me_creativ_b07496"; + DROP INDEX IF EXISTS "idx_creative_created_2d9564"; + ALTER TABLE "creative" DROP CONSTRAINT IF EXISTS "fk_creative_user_2ed7d825"; + ALTER TABLE "creative" DROP COLUMN "created_by_user_id"; + COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full +PROJECTS_READ: projects_read +PROJECTS_WRITE: projects_write +CREATIVES_READ: creatives_read +CREATIVES_WRITE: creatives_write +PLACEMENTS_READ: placements_read +PLACEMENTS_WRITE: placements_write +ANALYTICS_READ: analytics_read +ANALYTICS_WITHOUT_CLICKS: analytics_without_clicks +CHANNELS_READ: channels_read +CHANNELS_WRITE: channels_write'; + ALTER TABLE "workspace_user_permission_scope" ADD "channel_id" UUID; + COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full +PROJECTS_READ: projects_read +PROJECTS_WRITE: projects_write +CREATIVES_READ: creatives_read +CREATIVES_WRITE: creatives_write +PLACEMENTS_READ: placements_read +PLACEMENTS_WRITE: placements_write +ANALYTICS_READ: analytics_read +ANALYTICS_WITHOUT_CLICKS: analytics_without_clicks +CHANNELS_READ: channels_read +CHANNELS_WRITE: channels_write'; + ALTER TABLE "workspace_user_permission_scope" ADD CONSTRAINT "fk_workspac_channel_e039fdfd" FOREIGN KEY ("channel_id") REFERENCES "channel" ("id") ON DELETE CASCADE; + CREATE INDEX IF NOT EXISTS "idx_workspace_u_channel_d94b48" ON "workspace_user_permission_scope" ("channel_id");""" + + +MODELS_STATE = ( + "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" + "6H29Y3Htd2t3/Nr7ZJBtfJboLLf3p5/xfKHDK4" +) diff --git a/src/adapter/postgres.py b/src/adapter/postgres.py index 9b14dc7..9e35202 100644 --- a/src/adapter/postgres.py +++ b/src/adapter/postgres.py @@ -361,6 +361,7 @@ class Postgres(DatabaseBase): project_id: uuid.UUID | None = None, include_archived: bool = False, allowed_project_ids: set[uuid.UUID] | None = None, + created_by_user_id: uuid.UUID | None = None, ) -> list[domain.Creative]: query = domain.Creative.filter(project__workspace_id=workspace_id) @@ -371,6 +372,9 @@ class Postgres(DatabaseBase): return [] query = query.filter(project_id__in=list(allowed_project_ids)) + if created_by_user_id is not None: + query = query.filter(created_by_user_id=created_by_user_id) + if not include_archived: query = query.filter(status=domain.CreativeStatus.ACTIVE) diff --git a/src/domain/creative.py b/src/domain/creative.py index 88d736f..911ac83 100644 --- a/src/domain/creative.py +++ b/src/domain/creative.py @@ -16,6 +16,7 @@ from .error import ( if TYPE_CHECKING: from .project import Project + from .user import User class CreativeStatus(str, enum.Enum): @@ -32,9 +33,13 @@ class Creative(TimestampedModel): project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField( 'models.Project', related_name='creatives', on_delete=fields.CASCADE, index=True ) + created_by_user: fields.ForeignKeyRelation['User'] | None = fields.ForeignKeyField( + 'models.User', related_name='created_creatives', on_delete=fields.SET_NULL, null=True, index=True + ) if TYPE_CHECKING: project_id: UUID + created_by_user_id: UUID | None class Meta: table = 'creative' diff --git a/src/domain/workspace.py b/src/domain/workspace.py index 0792255..b5511f8 100644 --- a/src/domain/workspace.py +++ b/src/domain/workspace.py @@ -10,7 +10,6 @@ from .base import TimestampedModel from .error import WorkspaceAvatarTooLarge if TYPE_CHECKING: - from .channel import Channel from .creative import Creative from .placement import Placement from .project import Project @@ -94,16 +93,28 @@ class PermissionKey(enum.StrEnum): ANALYTICS_READ = 'analytics_read' ANALYTICS_WITHOUT_CLICKS = 'analytics_without_clicks' + ANALYTICS_OWN_CREATIVES = 'analytics_own_creatives' - CHANNELS_READ = 'channels_read' - CHANNELS_WRITE = 'channels_write' + @property + def description(self) -> str: + return { + PermissionKey.ADMIN_FULL: 'полный доступ', + PermissionKey.PROJECTS_READ: 'просмотр каталога каналов (проектов)', + PermissionKey.PROJECTS_WRITE: 'редактирование каталога каналов', + PermissionKey.CREATIVES_READ: 'просмотр креативов', + PermissionKey.CREATIVES_WRITE: 'создание/редактирование креативов', + PermissionKey.PLACEMENTS_READ: 'просмотр планов закупок', + PermissionKey.PLACEMENTS_WRITE: 'создание/редактирование закупок', + PermissionKey.ANALYTICS_READ: 'полный доступ к статистике', + PermissionKey.ANALYTICS_WITHOUT_CLICKS: 'статистика кроме переходов', + PermissionKey.ANALYTICS_OWN_CREATIVES: 'статистика только по своим креативам', + }.get(self, self.value) class PermissionScopeType(enum.StrEnum): PROJECT = 'project' CREATIVE = 'creative' PLACEMENT = 'placement' - CHANNEL = 'channel' class WorkspaceUserPermission(TimestampedModel): @@ -138,16 +149,12 @@ class WorkspaceUserPermissionScope(TimestampedModel): placement: fields.ForeignKeyRelation[Placement] | None = fields.ForeignKeyField( 'models.Placement', related_name='permission_scopes', on_delete=fields.CASCADE, null=True, index=True ) - channel: fields.ForeignKeyRelation[Channel] | None = fields.ForeignKeyField( - 'models.Channel', related_name='permission_scopes', on_delete=fields.CASCADE, null=True, index=True - ) if TYPE_CHECKING: workspace_user_id: uuid.UUID project_id: uuid.UUID | None creative_id: uuid.UUID | None placement_id: uuid.UUID | None - channel_id: uuid.UUID | None class Meta: table = 'workspace_user_permission_scope' diff --git a/src/domain/workspace_permissions.py b/src/domain/workspace_permissions.py index 3dda902..903e7aa 100644 --- a/src/domain/workspace_permissions.py +++ b/src/domain/workspace_permissions.py @@ -32,9 +32,6 @@ class WorkspacePermissions: elif scope.placement_id: key = (scope.permission, PermissionScopeType.PLACEMENT) scoped_permissions.setdefault(key, set()).add(scope.placement_id) - elif scope.channel_id: - key = (scope.permission, PermissionScopeType.CHANNEL) - scoped_permissions.setdefault(key, set()).add(scope.channel_id) return cls(global_permissions=global_permissions, scoped_permissions=scoped_permissions) @@ -77,18 +74,6 @@ class WorkspacePermissions: return allowed - def allowed_channel_ids(self, permission: PermissionKey) -> set[UUID] | None: - if self.has_global(permission): - return None - - allowed: set[UUID] = set() - for key in (permission, PermissionKey.ADMIN_FULL): - channel_scope = self.scoped_permissions.get((key, PermissionScopeType.CHANNEL)) - if channel_scope: - allowed.update(channel_scope) - - return allowed - def has_permission( self, permission: PermissionKey, @@ -106,8 +91,6 @@ class WorkspacePermissions: allowed = self.allowed_creative_ids(permission) elif scope_type == PermissionScopeType.PLACEMENT: allowed = self.allowed_placement_ids(permission) - elif scope_type == PermissionScopeType.CHANNEL: - allowed = self.allowed_channel_ids(permission) else: return False @@ -123,6 +106,28 @@ class WorkspacePermissions: return True return bool(allowed) + def has_any_analytics_permission(self) -> bool: + """Check if user has any analytics permission.""" + return ( + self.has_global(PermissionKey.ANALYTICS_READ) + or self.has_global(PermissionKey.ANALYTICS_WITHOUT_CLICKS) + or self.has_global(PermissionKey.ANALYTICS_OWN_CREATIVES) + ) + + def should_hide_subscriptions(self) -> bool: + """Check if subscription data should be hidden (user has analytics_without_clicks but not analytics_read).""" + if self.has_global(PermissionKey.ANALYTICS_READ): + return False + return self.has_global(PermissionKey.ANALYTICS_WITHOUT_CLICKS) + + def should_filter_own_creatives(self) -> bool: + """Check if analytics should be filtered to user's own creatives only.""" + if self.has_global(PermissionKey.ANALYTICS_READ): + return False + if self.has_global(PermissionKey.ANALYTICS_WITHOUT_CLICKS): + return False + return self.has_global(PermissionKey.ANALYTICS_OWN_CREATIVES) + @dataclass class WorkspacePermissionContext: @@ -139,9 +144,6 @@ class WorkspacePermissionContext: def allowed_placement_ids(self, permission: PermissionKey) -> set[UUID] | None: return self.permissions.allowed_placement_ids(permission) - def allowed_channel_ids(self, permission: PermissionKey) -> set[UUID] | None: - return self.permissions.allowed_channel_ids(permission) - def ensure_project_permission(self, permission: PermissionKey, project_id: UUID) -> None: if not self.permissions.has_permission( permission, @@ -166,13 +168,13 @@ class WorkspacePermissionContext: ): raise WorkspaceAccessDenied(self.workspace.id) - def ensure_channel_permission(self, permission: PermissionKey, channel_id: UUID) -> None: - if not self.permissions.has_permission( - permission, - scope_type=PermissionScopeType.CHANNEL, - scope_id=channel_id, - ): - raise WorkspaceAccessDenied(self.workspace.id) + def should_hide_subscriptions(self) -> bool: + """Check if subscription data should be hidden.""" + return self.permissions.should_hide_subscriptions() + + def should_filter_own_creatives(self) -> bool: + """Check if analytics should be filtered to user's own creatives only.""" + return self.permissions.should_filter_own_creatives() def build_workspace_permission_context(membership: WorkspaceUser) -> WorkspacePermissionContext: diff --git a/src/dto/workspace.py b/src/dto/workspace.py index 96fb671..e235ce1 100644 --- a/src/dto/workspace.py +++ b/src/dto/workspace.py @@ -74,9 +74,6 @@ class WorkspaceMemberOutput(pydantic.BaseModel): elif scope.placement_id: scope_type = domain.PermissionScopeType.PLACEMENT scope_id = scope.placement_id - elif scope.channel_id: - scope_type = domain.PermissionScopeType.CHANNEL - scope_id = scope.channel_id if scope_type and scope_id: permissions_map.setdefault(scope.permission, []).append( @@ -112,8 +109,8 @@ class WorkspacePermissionScopeInput(pydantic.BaseModel): class WorkspacePermissionInput(pydantic.BaseModel): key: domain.PermissionKey = pydantic.Field( description=( - 'Ключ права. Доступные значения: "projects_read", "projects_write", ' - '"placements_read", "placements_write", "analytics_read", "admin_full".' + 'Ключ права. Доступные значения: ' + + ', '.join(f'"{k.value}" - {k.description}' for k in domain.PermissionKey) ) ) scopes: list[WorkspacePermissionScopeInput] = pydantic.Field( diff --git a/src/main.py b/src/main.py index 554c8bb..1a002c0 100644 --- a/src/main.py +++ b/src/main.py @@ -66,6 +66,7 @@ app.add_middleware( allow_origins=[ 'https://app.tgexchange.ru', 'http://app.tgexchange.ru', + 'http://localhost:3000', ], allow_credentials=True, allow_methods=['*'], diff --git a/src/usecase/__init__.py b/src/usecase/__init__.py index 083ae55..9c3a54d 100644 --- a/src/usecase/__init__.py +++ b/src/usecase/__init__.py @@ -164,6 +164,21 @@ class Usecase: return domain.build_workspace_permission_context(membership) + async def ensure_analytics_permission( + self, workspace_id: UUID, user_id: UUID, *, for_project_id: UUID | None = None + ) -> domain.WorkspacePermissionContext: + """Ensure user has any analytics permission.""" + membership = await self.database.get_workspace_membership(workspace_id, user_id) + if not membership or not membership.workspace: + raise domain.WorkspaceNotFound(workspace_id) + + permissions = domain.WorkspacePermissions.from_membership(membership) + + if not permissions.has_any_analytics_permission(): + raise domain.WorkspaceAccessDenied(workspace_id) + + return domain.build_workspace_permission_context(membership) + async def get_or_create_personal_workspace(self, user: domain.User) -> domain.Workspace: workspace = await self.database.get_default_workspace_for_user(user.id) if workspace: diff --git a/src/usecase/analytics/get_channel_analytics.py b/src/usecase/analytics/get_channel_analytics.py index e5598d3..467211d 100644 --- a/src/usecase/analytics/get_channel_analytics.py +++ b/src/usecase/analytics/get_channel_analytics.py @@ -19,17 +19,15 @@ def _get_cost(placement_post: domain.PlacementPost) -> float: async def get_channel_analytics( self: 'Usecase', input: dto.GetChannelAnalyticsInput ) -> list[dto.ChannelAnalyticsOutput]: - context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ - ) + context = await self.ensure_analytics_permission(input.workspace_id, input.user_id) allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.ANALYTICS_READ) + hide_subscriptions = context.should_hide_subscriptions() if input.project_id: project = await self.database.get_project(input.workspace_id, input.project_id) if not project: raise domain.ProjectNotFound(input.project_id) - context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id) allowed_project_filter = None else: allowed_project_filter = allowed_project_ids @@ -88,11 +86,10 @@ async def get_channel_analytics( for ch in channels: stats = channel_stats[ch.id] - avg_cpf = ( - (stats.total_cost / stats.total_subscriptions) - if stats.total_subscriptions > 0 and stats.total_cost > 0 - else None - ) + total_subscriptions = 0 if hide_subscriptions else stats.total_subscriptions + avg_cpf = None + if not hide_subscriptions and stats.total_subscriptions > 0 and stats.total_cost > 0: + avg_cpf = stats.total_cost / stats.total_subscriptions avg_cpm = ( (stats.total_cost / stats.total_views * 1000) if stats.total_views > 0 and stats.total_cost > 0 else None ) @@ -104,7 +101,7 @@ async def get_channel_analytics( username=ch.username, placements_count=stats.placements_count, total_cost=stats.total_cost, - total_subscriptions=stats.total_subscriptions, + total_subscriptions=total_subscriptions, total_views=stats.total_views, avg_cpf=avg_cpf, avg_cpm=avg_cpm, diff --git a/src/usecase/analytics/get_creatives_analytics.py b/src/usecase/analytics/get_creatives_analytics.py index a295371..b57ae44 100644 --- a/src/usecase/analytics/get_creatives_analytics.py +++ b/src/usecase/analytics/get_creatives_analytics.py @@ -19,17 +19,21 @@ def _get_cost(placement_post: domain.PlacementPost) -> float: async def get_creatives_analytics( self: 'Usecase', input: dto.GetCreativesAnalyticsInput ) -> list[dto.CreativeAnalyticsOutput]: - context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ - ) + context = await self.ensure_analytics_permission(input.workspace_id, input.user_id) allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.ANALYTICS_READ) + hide_subscriptions = context.should_hide_subscriptions() + filter_own_creatives = context.should_filter_own_creatives() + + # Get user_id for own creatives filter + created_by_filter: UUID | None = None + if filter_own_creatives: + created_by_filter = context.membership.user_id if input.project_id: project = await self.database.get_project(input.workspace_id, input.project_id) if not project: raise domain.ProjectNotFound(input.project_id) - context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id) allowed_project_ids = None creatives = await self.database.get_workspace_creatives( @@ -37,6 +41,7 @@ async def get_creatives_analytics( input.project_id, include_archived=False, allowed_project_ids=allowed_project_ids, + created_by_user_id=created_by_filter, ) placements = await self.database.get_workspace_placement_posts( input.workspace_id, @@ -84,11 +89,10 @@ async def get_creatives_analytics( for cr in creatives: stats = creative_stats[cr.id] - avg_cpf = ( - (stats.total_cost / stats.total_subscriptions) - if stats.total_subscriptions > 0 and stats.total_cost > 0 - else None - ) + total_subscriptions = 0 if hide_subscriptions else stats.total_subscriptions + avg_cpf = None + if not hide_subscriptions and stats.total_subscriptions > 0 and stats.total_cost > 0: + avg_cpf = stats.total_cost / stats.total_subscriptions avg_cpm = ( (stats.total_cost / stats.total_views * 1000) if stats.total_views > 0 and stats.total_cost > 0 else None ) @@ -99,7 +103,7 @@ async def get_creatives_analytics( name=cr.name, placements_count=stats.placements_count, total_cost=stats.total_cost, - total_subscriptions=stats.total_subscriptions, + total_subscriptions=total_subscriptions, total_views=stats.total_views, avg_cpf=avg_cpf, avg_cpm=avg_cpm, diff --git a/src/usecase/analytics/get_overview_analytics.py b/src/usecase/analytics/get_overview_analytics.py index 93e36af..b4310ba 100644 --- a/src/usecase/analytics/get_overview_analytics.py +++ b/src/usecase/analytics/get_overview_analytics.py @@ -49,17 +49,15 @@ async def get_overview_analytics( if input.date_from > input.date_to: raise HTTPException(status.HTTP_400_BAD_REQUEST, 'date_from must be before date_to') - context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ - ) + context = await self.ensure_analytics_permission(input.workspace_id, input.user_id) allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.ANALYTICS_READ) + hide_subscriptions = context.should_hide_subscriptions() if input.project_id: project = await self.database.get_project(input.workspace_id, input.project_id) if not project: raise domain.ProjectNotFound(input.project_id) - context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id) allowed_project_ids = None raw_duration = input.date_to - input.date_from @@ -132,8 +130,15 @@ async def get_overview_analytics( current_cost, current_subs, current_views = _aggregate_totals(current_placements, subs_per_placement_current) previous_cost, previous_subs, previous_views = _aggregate_totals(previous_placements, subs_per_placement_previous) - current_avg_cpf = current_cost / current_subs if current_cost > 0 and current_subs > 0 else None - previous_avg_cpf = previous_cost / previous_subs if previous_cost > 0 and previous_subs > 0 else None + # Apply hide_subscriptions filter + if hide_subscriptions: + current_subs = 0 + previous_subs = 0 + current_avg_cpf = None + previous_avg_cpf = None + else: + current_avg_cpf = current_cost / current_subs if current_cost > 0 and current_subs > 0 else None + previous_avg_cpf = previous_cost / previous_subs if previous_cost > 0 and previous_subs > 0 else None current_avg_cpm = (current_cost / current_views * 1000) if current_cost > 0 and current_views > 0 else None previous_avg_cpm = (previous_cost / previous_views * 1000) if previous_cost > 0 and previous_views > 0 else None @@ -171,9 +176,9 @@ async def get_overview_analytics( for day_offset in range(days_span + 1): day = start_date + datetime.timedelta(days=day_offset) cost = cost_per_day.get(day, 0.0) - subs = subs_per_day_current.get(day, 0) - delta = (subs - previous_day_subs) if previous_day_subs is not None else subs - cpf = cost / subs if subs > 0 and cost > 0 else None + subs = 0 if hide_subscriptions else subs_per_day_current.get(day, 0) + delta = None if hide_subscriptions else ((subs - previous_day_subs) if previous_day_subs is not None else subs) + cpf = None if hide_subscriptions else (cost / subs if subs > 0 and cost > 0 else None) daily_stats.append( dto.OverviewDailyPoint(date=day, cost=cost, subscriptions=subs, subscriptions_delta=delta, cpf=cpf) ) @@ -181,12 +186,15 @@ async def get_overview_analytics( channel_performance = [] for stats in channel_stats.values(): - subs = stats.total_subs + subs = 0 if hide_subscriptions else stats.total_subs cost_value = stats.total_cost - cpf = cost_value / subs if subs > 0 else None + cpf = None if hide_subscriptions else (cost_value / stats.total_subs if stats.total_subs > 0 else None) channel = stats.channel - if not channel or cpf is None: + if not channel: + continue + # Skip channels without CPF only if we're not hiding subscriptions + if not hide_subscriptions and cpf is None: continue channel_performance.append( diff --git a/src/usecase/analytics/get_placements_analytics.py b/src/usecase/analytics/get_placements_analytics.py index fa6a096..28cb174 100644 --- a/src/usecase/analytics/get_placements_analytics.py +++ b/src/usecase/analytics/get_placements_analytics.py @@ -39,17 +39,15 @@ def _calculate_cpm(cost: float | None, views: int | None) -> float | None: async def get_placements_analytics( self: 'Usecase', input: dto.GetPlacementsAnalyticsInput ) -> list[dto.PlacementAnalyticsOutput]: - context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ - ) + context = await self.ensure_analytics_permission(input.workspace_id, input.user_id) allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.ANALYTICS_READ) + hide_subscriptions = context.should_hide_subscriptions() if input.project_id: project = await self.database.get_project(input.workspace_id, input.project_id) if not project: raise domain.ProjectNotFound(input.project_id) - context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id) allowed_project_ids = None placements = await self.database.get_workspace_placement_posts( @@ -93,11 +91,12 @@ async def get_placements_analytics( post_url = f'https://t.me/{post.channel.username}/{post.message_id}' cost = _get_cost(placement_post) - subs_count = subscriptions_counts.get(placement_post.id, 0) + subs_count = 0 if hide_subscriptions else subscriptions_counts.get(placement_post.id, 0) if placement_post.post and placement_post.post.id in views_map: views_count = views_map[placement_post.post.id][0] else: views_count = None + cpf = None if hide_subscriptions else _calculate_cpf(cost, subs_count) results.append( dto.PlacementAnalyticsOutput( id=placement_post.id, @@ -111,7 +110,7 @@ async def get_placements_analytics( cost=cost, subscriptions_count=subs_count, views_count=views_count, - cpf=_calculate_cpf(cost, subs_count), + cpf=cpf, cpm=_calculate_cpm(cost, views_count), post_url=post_url, ) diff --git a/src/usecase/analytics/get_projects_analytics.py b/src/usecase/analytics/get_projects_analytics.py index 690583f..a8748d5 100644 --- a/src/usecase/analytics/get_projects_analytics.py +++ b/src/usecase/analytics/get_projects_analytics.py @@ -101,6 +101,7 @@ class PeriodMetrics: def _calculate_metrics( period_metrics: PeriodMetrics, requested_metrics: list[dto.ProjectMetrics] | None, + hide_subscriptions: bool = False, ) -> dto.ProjectMetricsData: all_metrics = requested_metrics is None or len(requested_metrics) == 0 @@ -116,13 +117,13 @@ def _calculate_metrics( metrics.purchases_count = period_metrics.purchases_count if should_include(dto.ProjectMetrics.TOTAL_SUBSCRIPTIONS): - metrics.total_subscriptions = period_metrics.total_subscriptions + metrics.total_subscriptions = 0 if hide_subscriptions else period_metrics.total_subscriptions if should_include(dto.ProjectMetrics.TOTAL_VIEWS): metrics.total_views = period_metrics.total_views if should_include(dto.ProjectMetrics.CLICKS_COUNT): - metrics.clicks_count = period_metrics.clicks_count + metrics.clicks_count = 0 if hide_subscriptions else period_metrics.clicks_count if should_include(dto.ProjectMetrics.REACH_VOLUME): metrics.reach_volume = period_metrics.reach_volume @@ -132,11 +133,14 @@ def _calculate_metrics( # Средние значения if should_include(dto.ProjectMetrics.AVG_CPF): - metrics.avg_cpf = ( - period_metrics.total_cost / period_metrics.total_subscriptions - if period_metrics.total_subscriptions > 0 and period_metrics.total_cost > 0 - else None - ) + if hide_subscriptions: + metrics.avg_cpf = None + else: + metrics.avg_cpf = ( + period_metrics.total_cost / period_metrics.total_subscriptions + if period_metrics.total_subscriptions > 0 and period_metrics.total_cost > 0 + else None + ) if should_include(dto.ProjectMetrics.AVG_CPM): metrics.avg_cpm = ( @@ -160,11 +164,14 @@ def _calculate_metrics( if should_include(dto.ProjectMetrics.AVG_CONVERSION): # Конверсия = подписки / просмотры * 100 - metrics.avg_conversion = ( - (period_metrics.total_subscriptions / period_metrics.total_views) * 100 - if period_metrics.total_views > 0 and period_metrics.total_subscriptions > 0 - else 0.0 - ) + if hide_subscriptions: + metrics.avg_conversion = None + else: + metrics.avg_conversion = ( + (period_metrics.total_subscriptions / period_metrics.total_views) * 100 + if period_metrics.total_views > 0 and period_metrics.total_subscriptions > 0 + else 0.0 + ) return metrics @@ -175,11 +182,10 @@ async def get_projects_analytics( if input.date_from and input.date_to and input.date_from > input.date_to: raise HTTPException(status.HTTP_400_BAD_REQUEST, 'date_from must be before date_to') - context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ - ) + context = await self.ensure_analytics_permission(input.workspace_id, input.user_id) allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.ANALYTICS_READ) + hide_subscriptions = context.should_hide_subscriptions() # Фильтрация по project_ids если указаны if input.project_ids: @@ -293,7 +299,7 @@ async def get_projects_analytics( period_dt = datetime.datetime.now() period_label = _format_period_label(period_dt, input.grouping) - metrics = _calculate_metrics(pd, input.metrics) + metrics = _calculate_metrics(pd, input.metrics, hide_subscriptions) periods.append( dto.ProjectAnalyticsPeriod( @@ -303,6 +309,6 @@ async def get_projects_analytics( ) ) - totals = _calculate_metrics(total_metrics, input.metrics) + totals = _calculate_metrics(total_metrics, input.metrics, hide_subscriptions) return dto.GetProjectsAnalyticsOutput(periods=periods, totals=totals) diff --git a/src/usecase/analytics/get_spending_analytics.py b/src/usecase/analytics/get_spending_analytics.py index 48b9aa8..ff80d38 100644 --- a/src/usecase/analytics/get_spending_analytics.py +++ b/src/usecase/analytics/get_spending_analytics.py @@ -47,17 +47,15 @@ def _get_cost(placement_post: domain.PlacementPost) -> float | None: async def get_spending_analytics( self: 'Usecase', input: dto.GetSpendingAnalyticsInput ) -> dto.GetSpendingAnalyticsOutput: - context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.ANALYTICS_READ - ) + context = await self.ensure_analytics_permission(input.workspace_id, input.user_id) allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.ANALYTICS_READ) + hide_subscriptions = context.should_hide_subscriptions() if input.project_id: project = await self.database.get_project(input.workspace_id, input.project_id) if not project: raise domain.ProjectNotFound(input.project_id) - context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id) allowed_project_ids = None placements = await self.database.get_workspace_placement_posts( @@ -116,7 +114,8 @@ async def get_spending_analytics( total_views += views_count pd.views += views_count - avg_cpf = total_cost / total_subs if total_subs > 0 and total_cost > 0 else None + output_total_subs = 0 if hide_subscriptions else total_subs + avg_cpf = None if hide_subscriptions else (total_cost / total_subs if total_subs > 0 and total_cost > 0 else None) avg_cpm = (total_cost / total_views * 1000) if total_views > 0 and total_cost > 0 else None # Count unique placements @@ -132,10 +131,10 @@ async def get_spending_analytics( d = period_data[period] cost = d.cost - subs = d.subscriptions + subs = 0 if hide_subscriptions else d.subscriptions views = d.views - cpf = cost / subs if subs > 0 and cost > 0 else None + cpf = None if hide_subscriptions else (cost / d.subscriptions if d.subscriptions > 0 and cost > 0 else None) cpm = (cost / views * 1000) if views > 0 and cost > 0 else None chart_data.append( @@ -151,7 +150,7 @@ async def get_spending_analytics( return dto.GetSpendingAnalyticsOutput( total_cost=total_cost, - total_subscriptions=total_subs, + total_subscriptions=output_total_subs, total_views=total_views, avg_cpf=avg_cpf, avg_cpm=avg_cpm, diff --git a/src/usecase/creative/create_creative.py b/src/usecase/creative/create_creative.py index 5d02c44..83f541d 100644 --- a/src/usecase/creative/create_creative.py +++ b/src/usecase/creative/create_creative.py @@ -14,7 +14,7 @@ async def create_creative( self: 'Usecase', input: dto.CreateCreativeInput, project_id: uuid.UUID, user_id: uuid.UUID, workspace_id: uuid.UUID ) -> dto.CreativeOutput: await self.ensure_workspace_permission( - workspace_id, user_id, domain.PermissionKey.PROJECTS_WRITE, for_project_id=project_id + workspace_id, user_id, domain.PermissionKey.CREATIVES_WRITE, for_project_id=project_id ) project = await self.database.get_project(workspace_id, project_id=project_id) @@ -34,6 +34,7 @@ async def create_creative( buttons=[button.model_dump() for button in input.buttons], status=domain.CreativeStatus.ACTIVE, project_id=project.id, + created_by_user_id=user_id, ) await self.database.create_creative(creative) diff --git a/src/usecase/creative/delete_creative.py b/src/usecase/creative/delete_creative.py index 10e78f9..c8a75d5 100644 --- a/src/usecase/creative/delete_creative.py +++ b/src/usecase/creative/delete_creative.py @@ -12,7 +12,7 @@ log = logging.getLogger(__name__) async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> None: context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.PROJECTS_WRITE + input.workspace_id, input.user_id, domain.PermissionKey.CREATIVES_WRITE ) creative = await self.database.get_creative(input.workspace_id, input.creative_id) @@ -20,7 +20,7 @@ async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> No log.warning('User %s attempted to delete unavailable creative %s', input.user_id, input.creative_id) raise domain.CreativeNotFound(input.creative_id) - context.ensure_project_permission(domain.PermissionKey.PROJECTS_WRITE, creative.project_id) + context.ensure_project_permission(domain.PermissionKey.CREATIVES_WRITE, creative.project_id) has_placement_posts = await self.database.has_placement_posts_for_creative(creative.id) if has_placement_posts: diff --git a/src/usecase/creative/get_creative.py b/src/usecase/creative/get_creative.py index b4339c9..13adbf6 100644 --- a/src/usecase/creative/get_creative.py +++ b/src/usecase/creative/get_creative.py @@ -11,14 +11,14 @@ log = logging.getLogger(__name__) async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.CreativeOutput: context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.PROJECTS_READ + input.workspace_id, input.user_id, domain.PermissionKey.CREATIVES_READ ) creative = await self.database.get_creative(input.workspace_id, input.creative_id) if not creative: raise domain.CreativeNotFound(input.creative_id) - context.ensure_project_permission(domain.PermissionKey.PROJECTS_READ, creative.project_id) + context.ensure_project_permission(domain.PermissionKey.CREATIVES_READ, creative.project_id) placements_count = await self.database.count_placement_posts_by_creative(creative.id) media_rel = creative.media_items diff --git a/src/usecase/creative/get_creatives.py b/src/usecase/creative/get_creatives.py index 03f2894..f0e7b9d 100644 --- a/src/usecase/creative/get_creatives.py +++ b/src/usecase/creative/get_creatives.py @@ -8,13 +8,13 @@ if TYPE_CHECKING: async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> list[dto.CreativeOutput]: context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.PROJECTS_READ + input.workspace_id, input.user_id, domain.PermissionKey.CREATIVES_READ ) - allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.PROJECTS_READ) + allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.CREATIVES_READ) if input.project_id is not None: - context.ensure_project_permission(domain.PermissionKey.PROJECTS_READ, input.project_id) + context.ensure_project_permission(domain.PermissionKey.CREATIVES_READ, input.project_id) allowed_project_ids = None creatives = await self.database.get_workspace_creatives( diff --git a/src/usecase/creative/update_creative.py b/src/usecase/creative/update_creative.py index 2906dae..472cb88 100644 --- a/src/usecase/creative/update_creative.py +++ b/src/usecase/creative/update_creative.py @@ -18,14 +18,14 @@ async def update_creative( user_id: uuid.UUID, workspace_id: uuid.UUID, ) -> dto.CreativeOutput: - context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PROJECTS_WRITE) + context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.CREATIVES_WRITE) 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) - context.ensure_project_permission(domain.PermissionKey.PROJECTS_WRITE, creative.project_id) + context.ensure_project_permission(domain.PermissionKey.CREATIVES_WRITE, creative.project_id) if input.name: creative.name = input.name diff --git a/tg_parser/internal/controller/worker/channel_worker.go b/tg_parser/internal/controller/worker/channel_worker.go index e7ad34e..663ca5f 100644 --- a/tg_parser/internal/controller/worker/channel_worker.go +++ b/tg_parser/internal/controller/worker/channel_worker.go @@ -15,7 +15,7 @@ type ChannelConfig struct { ChannelDelay time.Duration `envconfig:"WORKER__CHANNEL_DELAY" default:"500ms"` RequestTimeout time.Duration `envconfig:"WORKER__REQUEST_TIMEOUT" default:"10s"` MessagesLimit int `envconfig:"WORKER__MESSAGES_LIMIT" default:"20"` - PollInterval time.Duration `envconfig:"WORKER__POLL_INTERVAL" default:"20s"` + PollInterval time.Duration `envconfig:"WORKER__POLL_INTERVAL" default:"120s"` } type ChannelWorker struct { diff --git a/tg_parser/internal/controller/worker/views_worker.go b/tg_parser/internal/controller/worker/views_worker.go index 2ae9bce..a8eca60 100644 --- a/tg_parser/internal/controller/worker/views_worker.go +++ b/tg_parser/internal/controller/worker/views_worker.go @@ -10,7 +10,7 @@ import ( ) type ViewsConfig struct { - Interval time.Duration `envconfig:"WORKER__VIEWS_INTERVAL" default:"30s"` + Interval time.Duration `envconfig:"WORKER__VIEWS_INTERVAL" default:"1800s"` RequestTimeout time.Duration `envconfig:"WORKER__REQUEST_TIMEOUT" default:"10s"` }