права фикс
This commit is contained in:
149
migrations/models/10_20260127194137_update.py
Normal file
149
migrations/models/10_20260127194137_update.py
Normal file
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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=['*'],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,6 +130,13 @@ 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)
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,6 +133,9 @@ def _calculate_metrics(
|
||||
|
||||
# Средние значения
|
||||
if should_include(dto.ProjectMetrics.AVG_CPF):
|
||||
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
|
||||
@@ -160,6 +164,9 @@ def _calculate_metrics(
|
||||
|
||||
if should_include(dto.ProjectMetrics.AVG_CONVERSION):
|
||||
# Конверсия = подписки / просмотры * 100
|
||||
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
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user