refactor
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
# ruff: noqa
|
||||||
from tortoise import BaseDBAsyncClient
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
RUN_IN_TRANSACTION = True
|
RUN_IN_TRANSACTION = True
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# ruff: noqa
|
||||||
from tortoise import BaseDBAsyncClient
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
RUN_IN_TRANSACTION = True
|
RUN_IN_TRANSACTION = True
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# ruff: noqa
|
||||||
from tortoise import BaseDBAsyncClient
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
RUN_IN_TRANSACTION = True
|
RUN_IN_TRANSACTION = True
|
||||||
@@ -55,8 +56,8 @@ async def upgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
FROM "purchase_channel" pc
|
FROM "purchase_channel" pc
|
||||||
JOIN "purchase" p ON pc.purchase_id = p.id;
|
JOIN "purchase" p ON pc.purchase_id = p.id;
|
||||||
|
|
||||||
-- Шаг 4: Создать таблицу publication (бывший placement)
|
-- Шаг 4: Создать таблицу placement_post (бывший placement)
|
||||||
CREATE TABLE "publication" (
|
CREATE TABLE "placement_post" (
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -71,8 +72,8 @@ async def upgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Шаг 5: Мигрировать данные из old_placement в publication
|
-- Шаг 5: Мигрировать данные из old_placement в placement_post
|
||||||
INSERT INTO "publication" (
|
INSERT INTO "placement_post" (
|
||||||
"id", "created_at", "updated_at", "deleted_at",
|
"id", "created_at", "updated_at", "deleted_at",
|
||||||
"wanted_placement_date", "cost", "comment", "status",
|
"wanted_placement_date", "cost", "comment", "status",
|
||||||
"creative_id", "placement_id", "post_id", "project_id"
|
"creative_id", "placement_id", "post_id", "project_id"
|
||||||
@@ -85,13 +86,13 @@ async def upgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
|
|
||||||
-- Шаг 6: Обновить foreign keys для таблиц, ссылающихся на old_placement
|
-- Шаг 6: Обновить foreign keys для таблиц, ссылающихся на old_placement
|
||||||
|
|
||||||
-- 6.1: Обновить subscription (placement_id -> publication_id)
|
-- 6.1: Обновить subscription (placement_id -> placement_post_id)
|
||||||
-- Удалить constraint ДО переименования колонки
|
-- Удалить constraint ДО переименования колонки
|
||||||
ALTER TABLE "subscription"
|
ALTER TABLE "subscription"
|
||||||
DROP CONSTRAINT IF EXISTS "subscription_placement_id_fkey";
|
DROP CONSTRAINT IF EXISTS "subscription_placement_id_fkey";
|
||||||
|
|
||||||
-- Переименовать колонку
|
-- Переименовать колонку
|
||||||
ALTER TABLE "subscription" RENAME COLUMN "placement_id" TO "publication_id";
|
ALTER TABLE "subscription" RENAME COLUMN "placement_id" TO "placement_post_id";
|
||||||
|
|
||||||
-- 6.2: Обновить workspace_user_permission_scope.placement_id
|
-- 6.2: Обновить workspace_user_permission_scope.placement_id
|
||||||
-- Удалить старый foreign key constraint ДО обновления данных
|
-- Удалить старый foreign key constraint ДО обновления данных
|
||||||
@@ -109,22 +110,22 @@ async def upgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
ON "placement" ("project_id", "status");
|
ON "placement" ("project_id", "status");
|
||||||
CREATE INDEX "idx_placement_channel_fb3968"
|
CREATE INDEX "idx_placement_channel_fb3968"
|
||||||
ON "placement" ("channel_id");
|
ON "placement" ("channel_id");
|
||||||
CREATE INDEX "idx_publication_creativ_1c6807"
|
CREATE INDEX "idx_placement_post_creativ_1c6807"
|
||||||
ON "publication" ("creative_id");
|
ON "placement_post" ("creative_id");
|
||||||
CREATE INDEX "idx_publication_placeme_bd5bc9"
|
CREATE INDEX "idx_placement_post_placeme_bd5bc9"
|
||||||
ON "publication" ("placement_id");
|
ON "placement_post" ("placement_id");
|
||||||
CREATE INDEX "idx_publication_post_id_998755"
|
CREATE INDEX "idx_placement_post_post_id_998755"
|
||||||
ON "publication" ("post_id");
|
ON "placement_post" ("post_id");
|
||||||
CREATE INDEX "idx_publication_project_94129d"
|
CREATE INDEX "idx_placement_post_project_94129d"
|
||||||
ON "publication" ("project_id");
|
ON "placement_post" ("project_id");
|
||||||
|
|
||||||
ALTER TABLE "subscription"
|
ALTER TABLE "subscription"
|
||||||
ADD CONSTRAINT "fk_subscrip_publicat_1959be9e"
|
ADD CONSTRAINT "fk_subscrip_placem_1959be9e"
|
||||||
FOREIGN KEY ("publication_id")
|
FOREIGN KEY ("placement_post_id")
|
||||||
REFERENCES "publication" ("id") ON DELETE CASCADE;
|
REFERENCES "placement_post" ("id") ON DELETE CASCADE;
|
||||||
|
|
||||||
CREATE UNIQUE INDEX "uid_subscriptio_publica_578bfb"
|
CREATE UNIQUE INDEX "uid_subscriptio_placem_578bfb"
|
||||||
ON "subscription" ("publication_id", "telegram_user_id");
|
ON "subscription" ("placement_post_id", "telegram_user_id");
|
||||||
|
|
||||||
ALTER TABLE "workspace_user_permission_scope"
|
ALTER TABLE "workspace_user_permission_scope"
|
||||||
ADD CONSTRAINT "fk_workspac_placemen_8c5d3f2a"
|
ADD CONSTRAINT "fk_workspac_placemen_8c5d3f2a"
|
||||||
@@ -150,9 +151,9 @@ CPM: cpm';
|
|||||||
IS 'SELF_PROMO: self_promo
|
IS 'SELF_PROMO: self_promo
|
||||||
STANDARD: standard';
|
STANDARD: standard';
|
||||||
|
|
||||||
COMMENT ON TABLE "publication"
|
COMMENT ON TABLE "placement_post"
|
||||||
IS 'Публикация, мониторимая системой (бывший Placement)';
|
IS 'Публикация, мониторимая системой (бывший Placement)';
|
||||||
COMMENT ON COLUMN "publication"."status"
|
COMMENT ON COLUMN "placement_post"."status"
|
||||||
IS 'ACTIVE: active
|
IS 'ACTIVE: active
|
||||||
ARCHIVED: archived';
|
ARCHIVED: archived';
|
||||||
|
|
||||||
@@ -273,7 +274,7 @@ async def downgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Шаг 7: Мигрировать данные из publication в placement
|
-- Шаг 7: Мигрировать данные из placement_post в placement
|
||||||
INSERT INTO "placement" (
|
INSERT INTO "placement" (
|
||||||
"id", "created_at", "updated_at", "deleted_at",
|
"id", "created_at", "updated_at", "deleted_at",
|
||||||
"wanted_placement_date", "cost", "comment", "status",
|
"wanted_placement_date", "cost", "comment", "status",
|
||||||
@@ -283,17 +284,17 @@ async def downgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
pub.id, pub.created_at, pub.updated_at, pub.deleted_at,
|
pub.id, pub.created_at, pub.updated_at, pub.deleted_at,
|
||||||
pub.wanted_placement_date, pub.cost, pub.comment, pub.status,
|
pub.wanted_placement_date, pub.cost, pub.comment, pub.status,
|
||||||
pub.creative_id, pub.placement_id, pub.post_id, pub.project_id
|
pub.creative_id, pub.placement_id, pub.post_id, pub.project_id
|
||||||
FROM "publication" pub;
|
FROM "placement_post" pub;
|
||||||
|
|
||||||
-- Шаг 8: Обновить foreign keys обратно
|
-- Шаг 8: Обновить foreign keys обратно
|
||||||
|
|
||||||
-- 8.1: Обновить subscription (publication_id -> placement_id)
|
-- 8.1: Обновить subscription (placement_post_id -> placement_id)
|
||||||
-- Удалить новый foreign key constraint
|
-- Удалить новый foreign key constraint
|
||||||
ALTER TABLE "subscription"
|
ALTER TABLE "subscription"
|
||||||
DROP CONSTRAINT IF EXISTS "fk_subscrip_publicat_1959be9e";
|
DROP CONSTRAINT IF EXISTS "fk_subscrip_placem_1959be9e";
|
||||||
|
|
||||||
-- Переименовать колонку обратно
|
-- Переименовать колонку обратно
|
||||||
ALTER TABLE "subscription" RENAME COLUMN "publication_id" TO "placement_id";
|
ALTER TABLE "subscription" RENAME COLUMN "placement_post_id" TO "placement_id";
|
||||||
|
|
||||||
-- 8.2: Обновить workspace_user_permission_scope.placement_id обратно
|
-- 8.2: Обновить workspace_user_permission_scope.placement_id обратно
|
||||||
-- Удалить constraint на новый placement
|
-- Удалить constraint на новый placement
|
||||||
@@ -303,7 +304,7 @@ async def downgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
-- Обновить данные обратно: новые placement_id -> старые placement_id
|
-- Обновить данные обратно: новые placement_id -> старые placement_id
|
||||||
UPDATE "workspace_user_permission_scope" wups
|
UPDATE "workspace_user_permission_scope" wups
|
||||||
SET placement_id = pub.id
|
SET placement_id = pub.id
|
||||||
FROM "publication" pub
|
FROM "placement_post" pub
|
||||||
WHERE wups.placement_id = pub.placement_id;
|
WHERE wups.placement_id = pub.placement_id;
|
||||||
|
|
||||||
-- Шаг 9: Создать индексы и constraints
|
-- Шаг 9: Создать индексы и constraints
|
||||||
@@ -326,86 +327,86 @@ async def downgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
REFERENCES "placement" ("id") ON DELETE CASCADE;
|
REFERENCES "placement" ("id") ON DELETE CASCADE;
|
||||||
|
|
||||||
-- Шаг 10: Удалить новые таблицы
|
-- Шаг 10: Удалить новые таблицы
|
||||||
DROP TABLE IF EXISTS "publication";
|
DROP TABLE IF EXISTS "placement_post";
|
||||||
DROP TABLE IF EXISTS "new_placement";
|
DROP TABLE IF EXISTS "new_placement";
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
MODELS_STATE = (
|
MODELS_STATE = (
|
||||||
"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'
|
||||||
"OfXn7/A5u7TFM="
|
'OfXn7/A5u7TFM='
|
||||||
)
|
)
|
||||||
@@ -9,4 +9,4 @@ class JWTConfig(BaseModel):
|
|||||||
|
|
||||||
class JWTBase:
|
class JWTBase:
|
||||||
def __init__(self, config: JWTConfig) -> None:
|
def __init__(self, config: JWTConfig) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|||||||
@@ -12,4 +12,3 @@ class TelegramBase:
|
|||||||
def __init__(self, config: TelegramConfig) -> None:
|
def __init__(self, config: TelegramConfig) -> None:
|
||||||
self.bot: Bot = Bot(token=config.TOKEN)
|
self.bot: Bot = Bot(token=config.TOKEN)
|
||||||
logging.info('Telegram bot initialized')
|
logging.info('Telegram bot initialized')
|
||||||
|
|
||||||
|
|||||||
@@ -392,9 +392,9 @@ class Postgres(DatabaseBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_publication(workspace_id: uuid.UUID, publication_id: uuid.UUID) -> domain.Publication | None:
|
async def get_placement_post(workspace_id: uuid.UUID, placement_post_id: uuid.UUID) -> domain.PlacementPost | None:
|
||||||
return await domain.Publication.get_or_none(
|
return await domain.PlacementPost.get_or_none(
|
||||||
id=publication_id, placement__project__workspace_id=workspace_id
|
id=placement_post_id, placement__project__workspace_id=workspace_id
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'placement',
|
'placement',
|
||||||
'placement__project',
|
'placement__project',
|
||||||
@@ -406,7 +406,7 @@ class Postgres(DatabaseBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_workspace_publications(
|
async def get_workspace_placement_posts(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
project_id: uuid.UUID | None = None,
|
project_id: uuid.UUID | None = None,
|
||||||
placement_channel_id: uuid.UUID | None = None,
|
placement_channel_id: uuid.UUID | None = None,
|
||||||
@@ -415,8 +415,8 @@ class Postgres(DatabaseBase):
|
|||||||
allowed_project_ids: set[uuid.UUID] | None = None,
|
allowed_project_ids: set[uuid.UUID] | None = None,
|
||||||
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.Publication]:
|
) -> list[domain.PlacementPost]:
|
||||||
query = domain.Publication.filter(placement__project__workspace_id=workspace_id)
|
query = domain.PlacementPost.filter(placement__project__workspace_id=workspace_id)
|
||||||
|
|
||||||
if project_id:
|
if project_id:
|
||||||
query = query.filter(placement__project_id=project_id)
|
query = query.filter(placement__project_id=project_id)
|
||||||
@@ -435,7 +435,7 @@ class Postgres(DatabaseBase):
|
|||||||
query = query.filter(created_at__lte=date_to)
|
query = query.filter(created_at__lte=date_to)
|
||||||
|
|
||||||
if not include_archived:
|
if not include_archived:
|
||||||
query = query.filter(status=domain.PublicationStatus.ACTIVE)
|
query = query.filter(status=domain.PlacementPostStatus.ACTIVE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await query.prefetch_related(
|
await query.prefetch_related(
|
||||||
@@ -452,12 +452,12 @@ class Postgres(DatabaseBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create_publication(publication: domain.Publication) -> None:
|
async def create_placement_post(placement_post: domain.PlacementPost) -> None:
|
||||||
await publication.save()
|
await placement_post.save()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_publication_by_invite_link(invite_link: str) -> domain.Publication | None:
|
async def get_placement_post_by_invite_link(invite_link: str) -> domain.PlacementPost | None:
|
||||||
return await domain.Publication.get_or_none(placement__invite_link=invite_link).prefetch_related(
|
return await domain.PlacementPost.get_or_none(placement__invite_link=invite_link).prefetch_related(
|
||||||
'placement',
|
'placement',
|
||||||
'placement__project',
|
'placement__project',
|
||||||
'placement__project__channel',
|
'placement__project__channel',
|
||||||
@@ -471,26 +471,28 @@ class Postgres(DatabaseBase):
|
|||||||
await subscription.save()
|
await subscription.save()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_subscription_by_subscriber_and_publication(
|
async def get_subscription_by_subscriber_and_placement_post(
|
||||||
telegram_user_id: uuid.UUID, publication_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(telegram_user_id=telegram_user_id, publication_id=publication_id)
|
return await domain.Subscription.get_or_none(
|
||||||
|
telegram_user_id=telegram_user_id, placement_post_id=placement_post_id
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_subscription(subscription: domain.Subscription) -> None:
|
async def update_subscription(subscription: domain.Subscription) -> None:
|
||||||
await subscription.save()
|
await subscription.save()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_subscriptions_for_publications(
|
async def get_subscriptions_for_placement_posts(
|
||||||
publication_ids: list[uuid.UUID],
|
placement_post_ids: list[uuid.UUID],
|
||||||
*,
|
*,
|
||||||
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]:
|
||||||
if not publication_ids:
|
if not placement_post_ids:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
query = domain.Subscription.filter(publication_id__in=publication_ids)
|
query = domain.Subscription.filter(placement_post_id__in=placement_post_ids)
|
||||||
|
|
||||||
if date_from:
|
if date_from:
|
||||||
query = query.filter(created_at__gte=date_from)
|
query = query.filter(created_at__gte=date_from)
|
||||||
@@ -506,10 +508,10 @@ class Postgres(DatabaseBase):
|
|||||||
return (
|
return (
|
||||||
await domain.Subscription.filter(
|
await domain.Subscription.filter(
|
||||||
telegram_user_id=telegram_user_id,
|
telegram_user_id=telegram_user_id,
|
||||||
publication__placement__project_id=project_id,
|
placement_post__placement__project_id=project_id,
|
||||||
status=domain.SubscriptionStatus.ACTIVE,
|
status=domain.SubscriptionStatus.ACTIVE,
|
||||||
)
|
)
|
||||||
.prefetch_related('publication', 'telegram_user')
|
.prefetch_related('placement_post', 'telegram_user')
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -520,10 +522,10 @@ class Postgres(DatabaseBase):
|
|||||||
return (
|
return (
|
||||||
await domain.Subscription.filter(
|
await domain.Subscription.filter(
|
||||||
telegram_user_id=telegram_user_id,
|
telegram_user_id=telegram_user_id,
|
||||||
publication__placement__project_id=project_id,
|
placement_post__placement__project_id=project_id,
|
||||||
status=domain.SubscriptionStatus.ACTIVE,
|
status=domain.SubscriptionStatus.ACTIVE,
|
||||||
)
|
)
|
||||||
.prefetch_related('publication', 'telegram_user')
|
.prefetch_related('placement_post', 'telegram_user')
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -555,22 +557,22 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
# Count methods
|
# Count methods
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def count_publications_by_creative(creative_id: uuid.UUID) -> int:
|
async def count_placement_posts_by_creative(creative_id: uuid.UUID) -> int:
|
||||||
return await domain.Publication.filter(placement__creative_id=creative_id).count()
|
return await domain.PlacementPost.filter(placement__creative_id=creative_id).count()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def count_subscriptions_by_publication(publication_id: uuid.UUID) -> int:
|
async def count_subscriptions_by_placement_post(placement_post_id: uuid.UUID) -> int:
|
||||||
return await domain.Subscription.filter(publication_id=publication_id).count()
|
return await domain.Subscription.filter(placement_post_id=placement_post_id).count()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def count_publications_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]:
|
||||||
if not creative_ids:
|
if not creative_ids:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
from tortoise.functions import Count
|
from tortoise.functions import Count
|
||||||
|
|
||||||
results = (
|
results = (
|
||||||
await domain.Publication.filter(placement__creative_id__in=creative_ids)
|
await domain.PlacementPost.filter(placement__creative_id__in=creative_ids)
|
||||||
.group_by('placement__creative_id')
|
.group_by('placement__creative_id')
|
||||||
.annotate(count=Count('id'))
|
.annotate(count=Count('id'))
|
||||||
.values('placement__creative_id', 'count')
|
.values('placement__creative_id', 'count')
|
||||||
@@ -580,22 +582,22 @@ class Postgres(DatabaseBase):
|
|||||||
return {cid: counts.get(cid, 0) for cid in creative_ids}
|
return {cid: counts.get(cid, 0) for cid in creative_ids}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def count_subscriptions_by_publication_batch(publication_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]:
|
||||||
if not publication_ids:
|
if not placement_post_ids:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
from tortoise.functions import Count
|
from tortoise.functions import Count
|
||||||
|
|
||||||
results = (
|
results = (
|
||||||
await domain.Subscription.filter(publication_id__in=publication_ids)
|
await domain.Subscription.filter(placement_post_id__in=placement_post_ids)
|
||||||
.group_by('publication_id')
|
.group_by('placement_post_id')
|
||||||
.annotate(count=Count('id'))
|
.annotate(count=Count('id'))
|
||||||
.values('publication_id', 'count')
|
.values('placement_post_id', 'count')
|
||||||
)
|
)
|
||||||
|
|
||||||
counts = {row['publication_id']: row['count'] for row in results}
|
counts = {row['placement_post_id']: row['count'] for row in results}
|
||||||
return {pid: counts.get(pid, 0) for pid in publication_ids}
|
return {pid: counts.get(pid, 0) for pid in placement_post_ids}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def has_publications_for_creative(creative_id: uuid.UUID) -> bool:
|
async def has_placement_posts_for_creative(creative_id: uuid.UUID) -> bool:
|
||||||
return await domain.Publication.filter(placement__creative_id=creative_id).exists()
|
return await domain.PlacementPost.filter(placement__creative_id=creative_id).exists()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from src.controller.http_v1.auth import auth_router
|
|||||||
from src.controller.http_v1.channels import channels_router
|
from src.controller.http_v1.channels import channels_router
|
||||||
from src.controller.http_v1.creatives import creatives_router
|
from src.controller.http_v1.creatives import creatives_router
|
||||||
from src.controller.http_v1.internal import internal_router
|
from src.controller.http_v1.internal import internal_router
|
||||||
from src.controller.http_v1.placements import publications_router
|
from src.controller.http_v1.placements import placement_posts_router
|
||||||
from src.controller.http_v1.projects import projects_router
|
from src.controller.http_v1.projects import projects_router
|
||||||
from src.controller.http_v1.purchases import placements_user_router
|
from src.controller.http_v1.purchases import placements_user_router
|
||||||
from src.controller.http_v1.views import views_router
|
from src.controller.http_v1.views import views_router
|
||||||
@@ -23,7 +23,7 @@ api_v1_router.include_router(channels_router)
|
|||||||
api_v1_router.include_router(projects_router)
|
api_v1_router.include_router(projects_router)
|
||||||
api_v1_router.include_router(placements_user_router) # User-managed placements
|
api_v1_router.include_router(placements_user_router) # User-managed placements
|
||||||
api_v1_router.include_router(creatives_router)
|
api_v1_router.include_router(creatives_router)
|
||||||
api_v1_router.include_router(publications_router) # System-managed publications
|
api_v1_router.include_router(placement_posts_router) # System-managed placement_posts
|
||||||
api_v1_router.include_router(views_router)
|
api_v1_router.include_router(views_router)
|
||||||
api_v1_router.include_router(analytics_router)
|
api_v1_router.include_router(analytics_router)
|
||||||
api_v1_router.include_router(workspaces_router)
|
api_v1_router.include_router(workspaces_router)
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ from fastapi.routing import APIRouter
|
|||||||
from src import deps, dto
|
from src import deps, dto
|
||||||
from src.adapter.jwt import JWTPayload
|
from src.adapter.jwt import JWTPayload
|
||||||
|
|
||||||
# Publications router - System-managed actual published posts
|
# PlacementPosts router - System-managed actual published posts
|
||||||
publications_router = APIRouter(prefix='/workspaces/{workspace_id}/publications', tags=['publications'])
|
placement_posts_router = APIRouter(prefix='/workspaces/{workspace_id}/placement_posts', tags=['placement_posts'])
|
||||||
|
|
||||||
|
|
||||||
@publications_router.get('')
|
@placement_posts_router.get('')
|
||||||
async def list_publications(
|
async def list_placement_posts(
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
project_id: uuid.UUID | None = None,
|
project_id: uuid.UUID | None = None,
|
||||||
placement_channel_id: uuid.UUID | None = None,
|
placement_channel_id: uuid.UUID | None = None,
|
||||||
creative_id: uuid.UUID | None = None,
|
creative_id: uuid.UUID | None = None,
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
) -> dto.GetPublicationsOutput:
|
) -> dto.GetPlacementPostsOutput:
|
||||||
"""List all publications (system-managed actual posts) in workspace"""
|
"""List all placement_posts (system-managed actual posts) in workspace"""
|
||||||
input = dto.GetPublicationsInput(
|
input = dto.GetPlacementPostsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
@@ -30,24 +30,24 @@ async def list_publications(
|
|||||||
include_archived=include_archived,
|
include_archived=include_archived,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await deps.get_usecase().get_publications(input=input)
|
result = await deps.get_usecase().get_placement_posts(input=input)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@publications_router.get('/{publication_id}')
|
@placement_posts_router.get('/{placement_post_id}')
|
||||||
async def get_publication(
|
async def get_placement_post(
|
||||||
publication_id: uuid.UUID,
|
placement_post_id: uuid.UUID,
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.PublicationOutput:
|
) -> dto.PlacementPostOutput:
|
||||||
"""Get single publication by ID"""
|
"""Get single placement_post by ID"""
|
||||||
input = dto.GetPublicationInput(
|
input = dto.GetPlacementPostInput(
|
||||||
publication_id=publication_id,
|
placement_post_id=placement_post_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await deps.get_usecase().get_publication(input=input)
|
return await deps.get_usecase().get_placement_post(input=input)
|
||||||
|
|
||||||
|
|
||||||
# Publication creation/updates are handled by worker; API exposes only read endpoints.
|
# PlacementPost creation/updates are handled by worker; API exposes only read endpoints.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ async def get_views_history(
|
|||||||
to_date: datetime.datetime | None = None,
|
to_date: datetime.datetime | None = None,
|
||||||
) -> Page[dto.PostViewsHistoryOutput]:
|
) -> Page[dto.PostViewsHistoryOutput]:
|
||||||
input_data = dto.GetViewsHistoryInput(
|
input_data = dto.GetViewsHistoryInput(
|
||||||
publication_id=placement_id,
|
placement_post_id=placement_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
from_date=from_date,
|
from_date=from_date,
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FetchPublicationPostWorker(WorkerBase):
|
class FetchPlacementPostWorker(WorkerBase):
|
||||||
"""Worker that fetches posts from channels and creates Publications from Placements"""
|
"""Worker that fetches posts from channels and creates PlacementPosts from Placements"""
|
||||||
|
|
||||||
async def _cycle_func(self) -> None:
|
async def _cycle_func(self) -> None:
|
||||||
await deps.get_usecase().fetch_publication_post_cycle(self.config.INTERVAL_SECONDS)
|
await deps.get_usecase().fetch_placement_post_cycle(self.config.INTERVAL_SECONDS)
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ __all__ = (
|
|||||||
'Placement',
|
'Placement',
|
||||||
'PlacementStatus',
|
'PlacementStatus',
|
||||||
'PlacementType',
|
'PlacementType',
|
||||||
'Publication',
|
'PlacementPost',
|
||||||
'PublicationStatus',
|
'PlacementPostStatus',
|
||||||
'Creative',
|
'Creative',
|
||||||
'replace_invite_link_with_tag',
|
'replace_invite_link_with_tag',
|
||||||
'validate_media_size',
|
'validate_media_size',
|
||||||
@@ -49,7 +49,7 @@ __all__ = (
|
|||||||
'ProjectNotFound',
|
'ProjectNotFound',
|
||||||
'ProjectChannelConflict',
|
'ProjectChannelConflict',
|
||||||
'PlacementNotFound',
|
'PlacementNotFound',
|
||||||
'PublicationNotFound',
|
'PlacementPostNotFound',
|
||||||
'ChannelNotFound',
|
'ChannelNotFound',
|
||||||
'ChannelAlreadyExists',
|
'ChannelAlreadyExists',
|
||||||
'ChannelNoAdminRights',
|
'ChannelNoAdminRights',
|
||||||
@@ -83,9 +83,9 @@ from .error import (
|
|||||||
LoginTokenExpired,
|
LoginTokenExpired,
|
||||||
LoginTokenNotFound,
|
LoginTokenNotFound,
|
||||||
PlacementNotFound,
|
PlacementNotFound,
|
||||||
|
PlacementPostNotFound,
|
||||||
ProjectChannelConflict,
|
ProjectChannelConflict,
|
||||||
ProjectNotFound,
|
ProjectNotFound,
|
||||||
PublicationNotFound,
|
|
||||||
TelegramChannelNotFound,
|
TelegramChannelNotFound,
|
||||||
UserByUsernameNotFound,
|
UserByUsernameNotFound,
|
||||||
UserNotFound,
|
UserNotFound,
|
||||||
@@ -104,10 +104,10 @@ from .placement import (
|
|||||||
PlacementStatus,
|
PlacementStatus,
|
||||||
PlacementType,
|
PlacementType,
|
||||||
)
|
)
|
||||||
|
from .placement_post import PlacementPost, PlacementPostStatus, PostViewsAvailability
|
||||||
from .post import Post
|
from .post import Post
|
||||||
from .post_views_history import PostViewsHistory
|
from .post_views_history import PostViewsHistory
|
||||||
from .project import Project, ProjectStatus
|
from .project import Project, ProjectStatus
|
||||||
from .publication import PostViewsAvailability, Publication, PublicationStatus
|
|
||||||
from .subscription import Subscription, SubscriptionStatus
|
from .subscription import Subscription, SubscriptionStatus
|
||||||
from .telegram_user import TelegramUser
|
from .telegram_user import TelegramUser
|
||||||
from .user import User
|
from .user import User
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ def PlacementNotFound(placement_id: uuid.UUID | None = None) -> HTTPException:
|
|||||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Placement {placement_id} not found')
|
return HTTPException(status.HTTP_404_NOT_FOUND, f'Placement {placement_id} not found')
|
||||||
|
|
||||||
|
|
||||||
def PublicationNotFound(publication_id: uuid.UUID | None = None) -> HTTPException:
|
def PlacementPostNotFound(placement_post_id: uuid.UUID | None = None) -> HTTPException:
|
||||||
if publication_id is None:
|
if placement_post_id is None:
|
||||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Publication not found')
|
return HTTPException(status.HTTP_404_NOT_FOUND, 'PlacementPost not found')
|
||||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'Publication {publication_id} not found')
|
return HTTPException(status.HTTP_404_NOT_FOUND, f'PlacementPost {placement_post_id} not found')
|
||||||
|
|
||||||
|
|
||||||
def ChannelAlreadyExists(telegram_id: int) -> HTTPException:
|
def ChannelAlreadyExists(telegram_id: int) -> HTTPException:
|
||||||
@@ -90,7 +90,7 @@ def CreativeMediaTooLarge(max_bytes: int) -> HTTPException:
|
|||||||
def CreativeInUse(creative_id: uuid.UUID) -> HTTPException:
|
def CreativeInUse(creative_id: uuid.UUID) -> HTTPException:
|
||||||
return HTTPException(
|
return HTTPException(
|
||||||
status.HTTP_400_BAD_REQUEST,
|
status.HTTP_400_BAD_REQUEST,
|
||||||
f'Creative {creative_id} is used in active publications and cannot be deleted',
|
f'Creative {creative_id} is used in active placement_posts and cannot be deleted',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ if TYPE_CHECKING:
|
|||||||
from .post import Post
|
from .post import Post
|
||||||
from .project import Project
|
from .project import Project
|
||||||
|
|
||||||
__all__ = ['Publication', 'PublicationStatus', 'PostViewsAvailability']
|
__all__ = ['PlacementPost', 'PlacementPostStatus', 'PostViewsAvailability']
|
||||||
|
|
||||||
|
|
||||||
class PublicationStatus(str, enum.Enum):
|
class PlacementPostStatus(str, enum.Enum):
|
||||||
ACTIVE = 'active' # Пост найден и отслеживается
|
ACTIVE = 'active' # Пост найден и отслеживается
|
||||||
ARCHIVED = 'archived' # Вручную архивировано
|
ARCHIVED = 'archived' # Вручную архивировано
|
||||||
|
|
||||||
@@ -27,25 +27,25 @@ class PostViewsAvailability(str, enum.Enum):
|
|||||||
MANUAL = 'manual' # Просмотры вводятся вручную
|
MANUAL = 'manual' # Просмотры вводятся вручную
|
||||||
|
|
||||||
|
|
||||||
class Publication(TimestampedModel):
|
class PlacementPost(TimestampedModel):
|
||||||
wanted_placement_date = fields.DatetimeField(db_column='placement_date')
|
wanted_placement_date = fields.DatetimeField(db_column='placement_date')
|
||||||
cost = fields.FloatField(null=True)
|
cost = fields.FloatField(null=True)
|
||||||
comment = fields.TextField(null=True)
|
comment = fields.TextField(null=True)
|
||||||
|
|
||||||
status = fields.CharEnumField(PublicationStatus, default=PublicationStatus.ACTIVE)
|
status = fields.CharEnumField(PlacementPostStatus, default=PlacementPostStatus.ACTIVE)
|
||||||
|
|
||||||
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||||
'models.Project', related_name='publications', on_delete=fields.CASCADE, index=True
|
'models.Project', related_name='placement_posts', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
||||||
'models.Creative', related_name='publications', on_delete=fields.CASCADE, index=True
|
'models.Creative', related_name='placement_posts', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField(
|
placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField(
|
||||||
'models.Placement', related_name='publications', on_delete=fields.CASCADE, index=True
|
'models.Placement', related_name='placement_posts', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
post: fields.ForeignKeyRelation['Post'] | None = fields.ForeignKeyField(
|
post: fields.ForeignKeyRelation['Post'] | None = fields.ForeignKeyField(
|
||||||
'models.Post', related_name='publications', on_delete=fields.SET_NULL, null=True, index=True
|
'models.Post', related_name='placement_posts', on_delete=fields.SET_NULL, null=True, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -55,4 +55,4 @@ class Publication(TimestampedModel):
|
|||||||
placement_id: UUID
|
placement_id: UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'publication'
|
table = 'placement_post'
|
||||||
@@ -7,7 +7,7 @@ from tortoise import fields
|
|||||||
from .base import TimestampedModel
|
from .base import TimestampedModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .publication import Publication
|
from .placement_post import PlacementPost
|
||||||
from .telegram_user import TelegramUser
|
from .telegram_user import TelegramUser
|
||||||
|
|
||||||
|
|
||||||
@@ -21,17 +21,17 @@ class Subscription(TimestampedModel):
|
|||||||
status = fields.CharEnumField(SubscriptionStatus, default=SubscriptionStatus.ACTIVE)
|
status = fields.CharEnumField(SubscriptionStatus, default=SubscriptionStatus.ACTIVE)
|
||||||
unsubscribed_at = fields.DatetimeField(null=True)
|
unsubscribed_at = fields.DatetimeField(null=True)
|
||||||
|
|
||||||
publication: fields.ForeignKeyRelation['Publication'] = fields.ForeignKeyField(
|
placement_post: fields.ForeignKeyRelation['PlacementPost'] = fields.ForeignKeyField(
|
||||||
'models.Publication', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
'models.PlacementPost', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
telegram_user: fields.ForeignKeyRelation['TelegramUser'] = fields.ForeignKeyField(
|
telegram_user: fields.ForeignKeyRelation['TelegramUser'] = fields.ForeignKeyField(
|
||||||
'models.TelegramUser', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
'models.TelegramUser', related_name='subscriptions', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
publication_id: UUID
|
placement_post_id: UUID
|
||||||
telegram_user_id: UUID
|
telegram_user_id: UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'subscription'
|
table = 'subscription'
|
||||||
unique_together = (('publication_id', 'telegram_user_id'),)
|
unique_together = (('placement_post_id', 'telegram_user_id'),)
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ __all__ = (
|
|||||||
'GetPlacementsInput',
|
'GetPlacementsInput',
|
||||||
'GetPlacementsOutput',
|
'GetPlacementsOutput',
|
||||||
'GetPlacementInput',
|
'GetPlacementInput',
|
||||||
'PublicationOutput',
|
'PlacementPostOutput',
|
||||||
'GetPublicationsInput',
|
'GetPlacementPostsInput',
|
||||||
'GetPublicationsOutput',
|
'GetPlacementPostsOutput',
|
||||||
'GetPublicationInput',
|
'GetPlacementPostInput',
|
||||||
'CreativeButton',
|
'CreativeButton',
|
||||||
'CreativeOutput',
|
'CreativeOutput',
|
||||||
'GetCreativesInput',
|
'GetCreativesInput',
|
||||||
@@ -132,10 +132,10 @@ from .creative import (
|
|||||||
UpdateCreativeInput,
|
UpdateCreativeInput,
|
||||||
)
|
)
|
||||||
from .placement import (
|
from .placement import (
|
||||||
GetPublicationInput,
|
GetPlacementPostInput,
|
||||||
GetPublicationsInput,
|
GetPlacementPostsInput,
|
||||||
GetPublicationsOutput,
|
GetPlacementPostsOutput,
|
||||||
PublicationOutput,
|
PlacementPostOutput,
|
||||||
)
|
)
|
||||||
from .project import (
|
from .project import (
|
||||||
ArchiveProjectInput,
|
ArchiveProjectInput,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pydantic
|
|||||||
from src import domain
|
from src import domain
|
||||||
|
|
||||||
|
|
||||||
class PublicationOutput(pydantic.BaseModel):
|
class PlacementPostOutput(pydantic.BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
project_id: uuid.UUID
|
project_id: uuid.UUID
|
||||||
project_channel_title: str | None
|
project_channel_title: str | None
|
||||||
@@ -18,15 +18,15 @@ class PublicationOutput(pydantic.BaseModel):
|
|||||||
cost: float | None
|
cost: float | None
|
||||||
comment: str | None
|
comment: str | None
|
||||||
ad_post_url: str | None
|
ad_post_url: str | None
|
||||||
invite_link_type: domain.purchase.InviteLinkType
|
invite_link_type: domain.InviteLinkType
|
||||||
invite_link: str
|
invite_link: str
|
||||||
status: domain.PublicationStatus
|
status: domain.PlacementPostStatus
|
||||||
subscriptions_count: int
|
subscriptions_count: int
|
||||||
placement_id: uuid.UUID # Ссылка на Placement
|
placement_id: uuid.UUID # Ссылка на Placement
|
||||||
created_at: datetime.datetime
|
created_at: datetime.datetime
|
||||||
|
|
||||||
|
|
||||||
class GetPublicationsInput(pydantic.BaseModel):
|
class GetPlacementPostsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
project_id: uuid.UUID | None = None
|
project_id: uuid.UUID | None = None
|
||||||
@@ -35,11 +35,11 @@ class GetPublicationsInput(pydantic.BaseModel):
|
|||||||
include_archived: bool = False
|
include_archived: bool = False
|
||||||
|
|
||||||
|
|
||||||
class GetPublicationsOutput(pydantic.BaseModel):
|
class GetPlacementPostsOutput(pydantic.BaseModel):
|
||||||
publications: list[PublicationOutput]
|
placement_posts: list[PlacementPostOutput]
|
||||||
|
|
||||||
|
|
||||||
class GetPublicationInput(pydantic.BaseModel):
|
class GetPlacementPostInput(pydantic.BaseModel):
|
||||||
publication_id: uuid.UUID
|
placement_post_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ class ProjectOutput(pydantic.BaseModel):
|
|||||||
title: str
|
title: str
|
||||||
username: str | None
|
username: str | None
|
||||||
status: ProjectStatus
|
status: ProjectStatus
|
||||||
purchase_invite_type_default: domain.purchase.InviteLinkType
|
purchase_invite_type_default: domain.InviteLinkType
|
||||||
|
|
||||||
|
|
||||||
class UpdateProjectInviteLinkTypeInput(pydantic.BaseModel):
|
class UpdateProjectInviteLinkTypeInput(pydantic.BaseModel):
|
||||||
purchase_invite_type_default: domain.purchase.InviteLinkType
|
purchase_invite_type_default: domain.InviteLinkType
|
||||||
|
|
||||||
|
|
||||||
class GetWorkspaceProjectsInput(pydantic.BaseModel):
|
class GetWorkspaceProjectsInput(pydantic.BaseModel):
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class PostViewsHistoryOutput(pydantic.BaseModel):
|
|||||||
class GetViewsHistoryInput(pydantic.BaseModel):
|
class GetViewsHistoryInput(pydantic.BaseModel):
|
||||||
"""Получить историю просмотров для публикации."""
|
"""Получить историю просмотров для публикации."""
|
||||||
|
|
||||||
publication_id: uuid.UUID
|
placement_post_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
from_date: datetime.datetime | None = None # Фильтр: с какой даты
|
from_date: datetime.datetime | None = None # Фильтр: с какой даты
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class WorkspaceMemberOutput(pydantic.BaseModel):
|
|||||||
# Определяем тип и ID scope на основе заполненных полей
|
# Определяем тип и ID scope на основе заполненных полей
|
||||||
scope_type = None
|
scope_type = None
|
||||||
scope_id = None
|
scope_id = None
|
||||||
|
|
||||||
if scope.project_id:
|
if scope.project_id:
|
||||||
scope_type = domain.PermissionScopeType.PROJECT
|
scope_type = domain.PermissionScopeType.PROJECT
|
||||||
scope_id = scope.project_id
|
scope_id = scope.project_id
|
||||||
@@ -76,7 +76,7 @@ class WorkspaceMemberOutput(pydantic.BaseModel):
|
|||||||
elif scope.channel_id:
|
elif scope.channel_id:
|
||||||
scope_type = domain.PermissionScopeType.CHANNEL
|
scope_type = domain.PermissionScopeType.CHANNEL
|
||||||
scope_id = scope.channel_id
|
scope_id = scope.channel_id
|
||||||
|
|
||||||
if scope_type and scope_id:
|
if scope_type and scope_id:
|
||||||
permissions_map.setdefault(scope.permission, []).append(
|
permissions_map.setdefault(scope.permission, []).append(
|
||||||
WorkspacePermissionScopeOutput(type=scope_type, id=scope_id)
|
WorkspacePermissionScopeOutput(type=scope_type, id=scope_id)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from src.adapter.s3 import S3
|
|||||||
from src.adapter.telegram_bot import TelegramBot
|
from src.adapter.telegram_bot import TelegramBot
|
||||||
from src.config import settings
|
from src.config import settings
|
||||||
from src.controller.http_v1 import api_router
|
from src.controller.http_v1 import api_router
|
||||||
from src.controller.worker.fetch_placement_post import FetchPublicationPostWorker
|
from src.controller.worker.fetch_placement_post import FetchPlacementPostWorker
|
||||||
from src.usecase import Usecase
|
from src.usecase import Usecase
|
||||||
|
|
||||||
|
|
||||||
@@ -24,11 +24,11 @@ from src.usecase import Usecase
|
|||||||
async def lifespan(_: FastAPI) -> AsyncGenerator[None]:
|
async def lifespan(_: FastAPI) -> AsyncGenerator[None]:
|
||||||
await postgres.connect()
|
await postgres.connect()
|
||||||
await s3.connect()
|
await s3.connect()
|
||||||
await fetch_publication_post_worker.start()
|
await fetch_placement_post_worker.start()
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
await fetch_publication_post_worker.stop()
|
await fetch_placement_post_worker.stop()
|
||||||
await s3.close()
|
await s3.close()
|
||||||
await postgres.close()
|
await postgres.close()
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ usecase = Usecase(
|
|||||||
)
|
)
|
||||||
deps.set_usecase(usecase)
|
deps.set_usecase(usecase)
|
||||||
|
|
||||||
fetch_publication_post_worker = FetchPublicationPostWorker(config=WorkerConfig(INTERVAL_SECONDS=5))
|
fetch_placement_post_worker = FetchPlacementPostWorker(config=WorkerConfig(INTERVAL_SECONDS=5))
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ from .creative.delete_creative import delete_creative
|
|||||||
from .creative.get_creative import get_creative
|
from .creative.get_creative import get_creative
|
||||||
from .creative.get_creatives import get_creatives
|
from .creative.get_creatives import get_creatives
|
||||||
from .creative.update_creative import update_creative
|
from .creative.update_creative import update_creative
|
||||||
from .placement.fetch_publication_post_cycle import fetch_publication_post_cycle
|
from .placement.fetch_placement_post_cycle import fetch_placement_post_cycle
|
||||||
from .placement.get_publication import get_publication
|
from .placement.get_placement_post import get_placement_post
|
||||||
from .placement.get_publications import get_publications
|
from .placement.get_placement_posts import get_placement_posts
|
||||||
from .project.archive_project import archive_project
|
from .project.archive_project import archive_project
|
||||||
from .project.delete_project import delete_project
|
from .project.delete_project import delete_project
|
||||||
from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id
|
from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id
|
||||||
@@ -171,10 +171,10 @@ class Usecase:
|
|||||||
create_creative = create_creative
|
create_creative = create_creative
|
||||||
update_creative = update_creative
|
update_creative = update_creative
|
||||||
delete_creative = delete_creative
|
delete_creative = delete_creative
|
||||||
# Publication (system-managed) use cases
|
# PlacementPost (system-managed) use cases
|
||||||
get_publications = get_publications
|
get_placement_posts = get_placement_posts
|
||||||
get_publication = get_publication
|
get_placement_post = get_placement_post
|
||||||
fetch_publication_post_cycle = fetch_publication_post_cycle
|
fetch_placement_post_cycle = fetch_placement_post_cycle
|
||||||
handle_subscription = handle_subscription
|
handle_subscription = handle_subscription
|
||||||
handle_unsubscription = handle_unsubscription
|
handle_unsubscription = handle_unsubscription
|
||||||
get_views_history = get_views_history
|
get_views_history = get_views_history
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ async def get_channel_analytics(
|
|||||||
else:
|
else:
|
||||||
allowed_project_filter = allowed_project_ids
|
allowed_project_filter = allowed_project_ids
|
||||||
|
|
||||||
placements = await self.database.get_workspace_publications(
|
placements = await self.database.get_workspace_placement_posts(
|
||||||
input.workspace_id,
|
input.workspace_id,
|
||||||
input.project_id,
|
input.project_id,
|
||||||
include_archived=False,
|
include_archived=False,
|
||||||
@@ -38,8 +38,8 @@ async def get_channel_analytics(
|
|||||||
|
|
||||||
# Collect unique channels from placements
|
# Collect unique channels from placements
|
||||||
channel_map: dict[UUID, domain.Channel] = {}
|
channel_map: dict[UUID, domain.Channel] = {}
|
||||||
for publication in placements:
|
for placement_post in placements:
|
||||||
placement = publication.placement
|
placement = placement_post.placement
|
||||||
if placement and placement.channel:
|
if placement and placement.channel:
|
||||||
channel_map[placement.channel_id] = placement.channel
|
channel_map[placement.channel_id] = placement.channel
|
||||||
channels = list(channel_map.values())
|
channels = list(channel_map.values())
|
||||||
@@ -59,22 +59,22 @@ async def get_channel_analytics(
|
|||||||
|
|
||||||
# Batch fetch subscriptions counts
|
# Batch fetch subscriptions counts
|
||||||
placement_ids = [p.id for p in placements]
|
placement_ids = [p.id for p in placements]
|
||||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids)
|
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids)
|
||||||
|
|
||||||
for publication in placements:
|
for placement_post in placements:
|
||||||
placement = publication.placement
|
placement = placement_post.placement
|
||||||
if not placement:
|
if not placement:
|
||||||
continue
|
continue
|
||||||
stats = channel_stats.get(placement.channel_id)
|
stats = channel_stats.get(placement.channel_id)
|
||||||
if stats is None:
|
if stats is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
stats.total_cost += publication.cost if publication.cost is not None else 0
|
stats.total_cost += placement_post.cost if placement_post.cost is not None else 0
|
||||||
stats.total_subscriptions += subscriptions_counts.get(publication.id, 0)
|
stats.total_subscriptions += subscriptions_counts.get(placement_post.id, 0)
|
||||||
|
|
||||||
# Get views from batch data
|
# Get views from batch data
|
||||||
if publication.post and publication.post.id in views_map:
|
if placement_post.post and placement_post.post.id in views_map:
|
||||||
views_count = views_map[publication.post.id][0]
|
views_count = views_map[placement_post.post.id][0]
|
||||||
stats.total_views += views_count
|
stats.total_views += views_count
|
||||||
|
|
||||||
stats.placements_count += 1
|
stats.placements_count += 1
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ async def get_creatives_analytics(
|
|||||||
include_archived=False,
|
include_archived=False,
|
||||||
allowed_project_ids=allowed_project_ids,
|
allowed_project_ids=allowed_project_ids,
|
||||||
)
|
)
|
||||||
placements = await self.database.get_workspace_publications(
|
placements = await self.database.get_workspace_placement_posts(
|
||||||
input.workspace_id,
|
input.workspace_id,
|
||||||
input.project_id,
|
input.project_id,
|
||||||
include_archived=False,
|
include_archived=False,
|
||||||
@@ -55,7 +55,7 @@ async def get_creatives_analytics(
|
|||||||
|
|
||||||
# Batch fetch subscriptions counts
|
# Batch fetch subscriptions counts
|
||||||
placement_ids = [p.id for p in placements]
|
placement_ids = [p.id for p in placements]
|
||||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids)
|
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids)
|
||||||
|
|
||||||
for p in placements:
|
for p in placements:
|
||||||
stats = creative_stats.get(p.creative_id)
|
stats = creative_stats.get(p.creative_id)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async def get_overview_analytics(
|
|||||||
period_length = raw_duration if raw_duration.total_seconds() > 0 else datetime.timedelta(days=1)
|
period_length = raw_duration if raw_duration.total_seconds() > 0 else datetime.timedelta(days=1)
|
||||||
previous_period_start = input.date_from - period_length
|
previous_period_start = input.date_from - period_length
|
||||||
|
|
||||||
placements = await self.database.get_workspace_publications(
|
placements = await self.database.get_workspace_placement_posts(
|
||||||
input.workspace_id,
|
input.workspace_id,
|
||||||
project_id=input.project_id,
|
project_id=input.project_id,
|
||||||
include_archived=False,
|
include_archived=False,
|
||||||
@@ -64,18 +64,18 @@ async def get_overview_analytics(
|
|||||||
date_to=input.date_to,
|
date_to=input.date_to,
|
||||||
)
|
)
|
||||||
|
|
||||||
current_placements: list[domain.Publication] = []
|
current_placements: list[domain.PlacementPost] = []
|
||||||
previous_placements: list[domain.Publication] = []
|
previous_placements: list[domain.PlacementPost] = []
|
||||||
|
|
||||||
for publication in placements:
|
for placement_post in placements:
|
||||||
placement_date = publication.wanted_placement_date
|
placement_date = placement_post.wanted_placement_date
|
||||||
if placement_date >= input.date_from and placement_date <= input.date_to:
|
if placement_date >= input.date_from and placement_date <= input.date_to:
|
||||||
current_placements.append(publication)
|
current_placements.append(placement_post)
|
||||||
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(publication)
|
previous_placements.append(placement_post)
|
||||||
|
|
||||||
placement_ids = [p.id for p in placements]
|
placement_ids = [p.id for p in placements]
|
||||||
subscriptions = await self.database.get_subscriptions_for_publications(
|
subscriptions = await self.database.get_subscriptions_for_placement_posts(
|
||||||
placement_ids, date_from=previous_period_start, date_to=input.date_to
|
placement_ids, date_from=previous_period_start, date_to=input.date_to
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -89,30 +89,30 @@ async def get_overview_analytics(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
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.publication_id] += 1
|
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:
|
elif created_at >= previous_period_start and created_at < input.date_from:
|
||||||
subs_per_placement_previous[sub.publication_id] += 1
|
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.Publication], subs_per_placement: dict[UUID, int]
|
placement_list: list[domain.PlacementPost], subs_per_placement: 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
|
||||||
total_views = 0
|
total_views = 0
|
||||||
|
|
||||||
for publication in placement_list:
|
for placement_post in placement_list:
|
||||||
if publication.cost is not None:
|
if placement_post.cost is not None:
|
||||||
total_cost += publication.cost
|
total_cost += placement_post.cost
|
||||||
|
|
||||||
subs = subs_per_placement.get(publication.id, 0)
|
subs = subs_per_placement.get(placement_post.id, 0)
|
||||||
total_subscriptions += subs
|
total_subscriptions += subs
|
||||||
|
|
||||||
if publication.post and publication.post.id in views_map:
|
if placement_post.post and placement_post.post.id in views_map:
|
||||||
total_views += views_map[publication.post.id][0]
|
total_views += views_map[placement_post.post.id][0]
|
||||||
|
|
||||||
return total_cost, total_subscriptions, total_views
|
return total_cost, total_subscriptions, total_views
|
||||||
|
|
||||||
@@ -130,16 +130,16 @@ async def get_overview_analytics(
|
|||||||
project_spending_map: dict[UUID, float] = defaultdict(float)
|
project_spending_map: dict[UUID, float] = defaultdict(float)
|
||||||
project_meta: dict[UUID, domain.Project] = {}
|
project_meta: dict[UUID, domain.Project] = {}
|
||||||
|
|
||||||
for publication in current_placements:
|
for placement_post in current_placements:
|
||||||
placement_day = publication.wanted_placement_date.date()
|
placement_day = placement_post.wanted_placement_date.date()
|
||||||
project_meta[publication.project_id] = publication.project
|
project_meta[placement_post.project_id] = placement_post.project
|
||||||
|
|
||||||
cost_value = publication.cost or 0.0
|
cost_value = placement_post.cost or 0.0
|
||||||
cost_per_day[placement_day] += cost_value
|
cost_per_day[placement_day] += cost_value
|
||||||
project_spending_map[publication.project_id] += cost_value
|
project_spending_map[placement_post.project_id] += cost_value
|
||||||
subs = subs_per_placement_current.get(publication.id, 0)
|
subs = subs_per_placement_current.get(placement_post.id, 0)
|
||||||
|
|
||||||
placement = publication.placement
|
placement = placement_post.placement
|
||||||
if placement:
|
if placement:
|
||||||
channel_id = placement.channel_id
|
channel_id = placement.channel_id
|
||||||
if channel_id not in channel_stats:
|
if channel_id not in channel_stats:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async def get_placements_analytics(
|
|||||||
context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id)
|
context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id)
|
||||||
allowed_project_ids = None
|
allowed_project_ids = None
|
||||||
|
|
||||||
placements = await self.database.get_workspace_publications(
|
placements = await self.database.get_workspace_placement_posts(
|
||||||
input.workspace_id,
|
input.workspace_id,
|
||||||
input.project_id,
|
input.project_id,
|
||||||
include_archived=False,
|
include_archived=False,
|
||||||
@@ -50,7 +50,7 @@ async def get_placements_analytics(
|
|||||||
|
|
||||||
# Batch fetch subscriptions counts
|
# Batch fetch subscriptions counts
|
||||||
placement_ids = [p.id for p in placements]
|
placement_ids = [p.id for p in placements]
|
||||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids)
|
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
dto.PlacementAnalyticsOutput(
|
dto.PlacementAnalyticsOutput(
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ def _format_period_label(dt: datetime.datetime, grouping: dto.DateGrouping) -> s
|
|||||||
if week_start.month == week_end.month:
|
if week_start.month == week_end.month:
|
||||||
return f'{week_start.day}-{week_end.day} {month_names[week_start.month - 1]}'
|
return f'{week_start.day}-{week_end.day} {month_names[week_start.month - 1]}'
|
||||||
else:
|
else:
|
||||||
return f'{week_start.day} {month_names[week_start.month - 1]}-{week_end.day} {month_names[week_end.month - 1]}'
|
return f'{week_start.day} {month_names[week_start.month - 1]}-{week_end.day} {month_names[week_end.month - 1]}' # noqa: E501
|
||||||
case dto.DateGrouping.MONTH:
|
case dto.DateGrouping.MONTH:
|
||||||
# "дек 2024"
|
# "дек 2024"
|
||||||
month_names = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']
|
month_names = ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек']
|
||||||
@@ -60,20 +60,20 @@ def _format_period_label(dt: datetime.datetime, grouping: dto.DateGrouping) -> s
|
|||||||
raise ValueError('Invalid date grouping')
|
raise ValueError('Invalid date grouping')
|
||||||
|
|
||||||
|
|
||||||
def _get_grouping_date(publication: domain.Publication, date_grouping: dto.DateGroupingType) -> datetime.datetime:
|
def _get_grouping_date(placement_post: domain.PlacementPost, date_grouping: dto.DateGroupingType) -> datetime.datetime:
|
||||||
match date_grouping:
|
match date_grouping:
|
||||||
case dto.DateGroupingType.PLACEMENT_DATE:
|
case dto.DateGroupingType.PLACEMENT_DATE:
|
||||||
return publication.wanted_placement_date
|
return placement_post.wanted_placement_date
|
||||||
case dto.DateGroupingType.PURCHASE_DATE:
|
case dto.DateGroupingType.PURCHASE_DATE:
|
||||||
if publication.placement:
|
if placement_post.placement:
|
||||||
return publication.placement.created_at
|
return placement_post.placement.created_at
|
||||||
return publication.wanted_placement_date # Fallback
|
return placement_post.wanted_placement_date # Fallback
|
||||||
case dto.DateGroupingType.LINK_DATE:
|
case dto.DateGroupingType.LINK_DATE:
|
||||||
if publication.placement:
|
if placement_post.placement:
|
||||||
return publication.placement.created_at
|
return placement_post.placement.created_at
|
||||||
return publication.wanted_placement_date # Fallback
|
return placement_post.wanted_placement_date # Fallback
|
||||||
case _:
|
case _:
|
||||||
return publication.wanted_placement_date
|
return placement_post.wanted_placement_date
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -183,7 +183,7 @@ async def get_projects_analytics(
|
|||||||
else:
|
else:
|
||||||
allowed_project_ids = set(input.project_ids)
|
allowed_project_ids = set(input.project_ids)
|
||||||
|
|
||||||
placements = await self.database.get_workspace_publications(
|
placements = await self.database.get_workspace_placement_posts(
|
||||||
input.workspace_id,
|
input.workspace_id,
|
||||||
project_id=None,
|
project_id=None,
|
||||||
include_archived=False,
|
include_archived=False,
|
||||||
@@ -194,28 +194,28 @@ async def get_projects_analytics(
|
|||||||
|
|
||||||
# Фильтрация по датам на основе date_grouping
|
# Фильтрация по датам на основе date_grouping
|
||||||
filtered_placements = []
|
filtered_placements = []
|
||||||
for publication in placements:
|
for placement_post in placements:
|
||||||
grouping_date = _get_grouping_date(publication, input.date_grouping)
|
grouping_date = _get_grouping_date(placement_post, input.date_grouping)
|
||||||
if input.date_from and grouping_date < input.date_from:
|
if input.date_from and grouping_date < input.date_from:
|
||||||
continue
|
continue
|
||||||
if input.date_to and grouping_date > input.date_to:
|
if input.date_to and grouping_date > input.date_to:
|
||||||
continue
|
continue
|
||||||
filtered_placements.append(publication)
|
filtered_placements.append(placement_post)
|
||||||
|
|
||||||
# Batch fetch views data
|
# Batch fetch views data
|
||||||
post_ids = [p.post.id for p in filtered_placements if p.post]
|
post_ids = [p.post.id for p in filtered_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 {}
|
||||||
|
|
||||||
# Batch fetch subscriptions counts
|
# Batch fetch subscriptions counts
|
||||||
publication_ids = [p.id for p in filtered_placements]
|
placement_post_ids = [p.id for p in filtered_placements]
|
||||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(publication_ids)
|
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids)
|
||||||
|
|
||||||
# Группировка по периодам
|
# Группировка по периодам
|
||||||
period_data: dict[str, PeriodMetrics] = defaultdict(PeriodMetrics)
|
period_data: dict[str, PeriodMetrics] = defaultdict(PeriodMetrics)
|
||||||
total_metrics = PeriodMetrics()
|
total_metrics = PeriodMetrics()
|
||||||
|
|
||||||
for publication in filtered_placements:
|
for placement_post in filtered_placements:
|
||||||
grouping_date = _get_grouping_date(publication, input.date_grouping)
|
grouping_date = _get_grouping_date(placement_post, input.date_grouping)
|
||||||
period = _format_period(grouping_date, input.grouping)
|
period = _format_period(grouping_date, input.grouping)
|
||||||
pd = period_data[period]
|
pd = period_data[period]
|
||||||
|
|
||||||
@@ -223,25 +223,25 @@ async def get_projects_analytics(
|
|||||||
pd.purchases_count += 1
|
pd.purchases_count += 1
|
||||||
total_metrics.purchases_count += 1
|
total_metrics.purchases_count += 1
|
||||||
|
|
||||||
cost = publication.cost or 0.0
|
cost = placement_post.cost or 0.0
|
||||||
pd.total_cost += cost
|
pd.total_cost += cost
|
||||||
total_metrics.total_cost += cost
|
total_metrics.total_cost += cost
|
||||||
|
|
||||||
subs_count = subscriptions_counts.get(publication.id, 0)
|
subs_count = subscriptions_counts.get(placement_post.id, 0)
|
||||||
pd.total_subscriptions += subs_count
|
pd.total_subscriptions += subs_count
|
||||||
pd.clicks_count += subs_count
|
pd.clicks_count += subs_count
|
||||||
total_metrics.total_subscriptions += subs_count
|
total_metrics.total_subscriptions += subs_count
|
||||||
total_metrics.clicks_count += subs_count
|
total_metrics.clicks_count += subs_count
|
||||||
|
|
||||||
if publication.post and publication.post.id in views_map:
|
if placement_post.post and placement_post.post.id in views_map:
|
||||||
views_count = views_map[publication.post.id][0]
|
views_count = views_map[placement_post.post.id][0]
|
||||||
pd.total_views += views_count
|
pd.total_views += views_count
|
||||||
pd.reach_volume += views_count
|
pd.reach_volume += views_count
|
||||||
total_metrics.total_views += views_count
|
total_metrics.total_views += views_count
|
||||||
total_metrics.reach_volume += views_count
|
total_metrics.reach_volume += views_count
|
||||||
|
|
||||||
# Расчет скидок
|
# Расчет скидок
|
||||||
placement = publication.placement
|
placement = placement_post.placement
|
||||||
if placement and placement.cost_before_bargain and placement.cost_before_bargain > cost:
|
if placement and placement.cost_before_bargain and placement.cost_before_bargain > cost:
|
||||||
discount = placement.cost_before_bargain - cost
|
discount = placement.cost_before_bargain - cost
|
||||||
discount_percent = (
|
discount_percent = (
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ async def get_spending_analytics(
|
|||||||
context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id)
|
context.ensure_project_permission(domain.PermissionKey.ANALYTICS_READ, project.id)
|
||||||
allowed_project_ids = None
|
allowed_project_ids = None
|
||||||
|
|
||||||
placements = await self.database.get_workspace_publications(
|
placements = await self.database.get_workspace_placement_posts(
|
||||||
input.workspace_id,
|
input.workspace_id,
|
||||||
input.project_id,
|
input.project_id,
|
||||||
include_archived=False,
|
include_archived=False,
|
||||||
@@ -76,7 +76,7 @@ async def get_spending_analytics(
|
|||||||
|
|
||||||
# Batch fetch subscriptions counts
|
# Batch fetch subscriptions counts
|
||||||
placement_ids = [p.id for p in filtered]
|
placement_ids = [p.id for p in filtered]
|
||||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(placement_ids)
|
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_ids)
|
||||||
|
|
||||||
# Группировка по периодам
|
# Группировка по периодам
|
||||||
period_data: dict[str, PeriodData] = defaultdict(PeriodData)
|
period_data: dict[str, PeriodData] = defaultdict(PeriodData)
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def attach_channel_to_workspace(
|
async def attach_channel_to_workspace(self: 'Usecase', input: dto.AttachChannelToWorkspaceInput) -> dto.ProjectOutput:
|
||||||
self: 'Usecase', input: dto.AttachChannelToWorkspaceInput
|
|
||||||
) -> dto.ProjectOutput:
|
|
||||||
"""Привязать канал к workspace (вызывается из Golang бота после выбора workspace пользователем)"""
|
"""Привязать канал к workspace (вызывается из Golang бота после выбора workspace пользователем)"""
|
||||||
|
|
||||||
channel = await self.database.get_channel(channel_id=input.channel_id)
|
channel = await self.database.get_channel(channel_id=input.channel_id)
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ async def get_channel(self: 'Usecase', input: dto.GetChannelInput) -> dto.Channe
|
|||||||
telegram_id=channel.telegram_id,
|
telegram_id=channel.telegram_id,
|
||||||
title=channel.title,
|
title=channel.title,
|
||||||
username=channel.username,
|
username=channel.username,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> No
|
|||||||
|
|
||||||
context.ensure_project_permission(domain.PermissionKey.PROJECTS_WRITE, creative.project_id)
|
context.ensure_project_permission(domain.PermissionKey.PROJECTS_WRITE, creative.project_id)
|
||||||
|
|
||||||
has_publications = await self.database.has_publications_for_creative(creative.id)
|
has_placement_posts = await self.database.has_placement_posts_for_creative(creative.id)
|
||||||
if has_publications:
|
if has_placement_posts:
|
||||||
log.warning('Creative %s is used in publications and cannot be deleted', input.creative_id)
|
log.warning('Creative %s is used in placement_posts and cannot be deleted', input.creative_id)
|
||||||
raise domain.CreativeInUse(input.creative_id)
|
raise domain.CreativeInUse(input.creative_id)
|
||||||
|
|
||||||
media_key = creative.media_s3_key
|
media_key = creative.media_s3_key
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.Crea
|
|||||||
|
|
||||||
context.ensure_project_permission(domain.PermissionKey.PROJECTS_READ, creative.project_id)
|
context.ensure_project_permission(domain.PermissionKey.PROJECTS_READ, creative.project_id)
|
||||||
|
|
||||||
placements_count = await self.database.count_publications_by_creative(creative.id)
|
placements_count = await self.database.count_placement_posts_by_creative(creative.id)
|
||||||
|
|
||||||
return dto.CreativeOutput(
|
return dto.CreativeOutput(
|
||||||
id=creative.id,
|
id=creative.id,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> list[d
|
|||||||
)
|
)
|
||||||
|
|
||||||
creative_ids = [c.id for c in creatives]
|
creative_ids = [c.id for c in creatives]
|
||||||
placements_counts = await self.database.count_publications_by_creative_batch(creative_ids)
|
placements_counts = await self.database.count_placement_posts_by_creative_batch(creative_ids)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
dto.CreativeOutput(
|
dto.CreativeOutput(
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ async def update_creative(
|
|||||||
|
|
||||||
await self.database.update_creative(creative)
|
await self.database.update_creative(creative)
|
||||||
|
|
||||||
placements_count = await self.database.count_publications_by_creative(creative.id)
|
placements_count = await self.database.count_placement_posts_by_creative(creative.id)
|
||||||
|
|
||||||
return dto.CreativeOutput(
|
return dto.CreativeOutput(
|
||||||
id=creative.id,
|
id=creative.id,
|
||||||
|
|||||||
@@ -9,15 +9,13 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def fetch_publication_post_cycle(self: 'Usecase', interval_seconds: int) -> None:
|
async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) -> None:
|
||||||
"""Находит посты в каналах по invite_link из Placement и создает Publication"""
|
"""Находит посты в каналах по invite_link из Placement и создает PlacementPost"""
|
||||||
log.debug('Starting fetch_publication_post_cycle')
|
log.debug('Starting fetch_placement_post_cycle')
|
||||||
|
|
||||||
# Получаем все approved Placements с invite_link
|
# Получаем все approved Placements с invite_link
|
||||||
placements = (
|
placements = (
|
||||||
await domain.Placement.filter(
|
await domain.Placement.filter(status__in=[domain.PlacementStatus.APPROVED, domain.PlacementStatus.IN_PROGRESS])
|
||||||
status__in=[domain.PlacementStatus.APPROVED, domain.PlacementStatus.IN_PROGRESS]
|
|
||||||
)
|
|
||||||
.prefetch_related('channel', 'project')
|
.prefetch_related('channel', 'project')
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@@ -42,23 +40,23 @@ async def fetch_publication_post_cycle(self: 'Usecase', interval_seconds: int) -
|
|||||||
|
|
||||||
for post in posts:
|
for post in posts:
|
||||||
# Проверяем, не создана ли уже публикация для этого поста
|
# Проверяем, не создана ли уже публикация для этого поста
|
||||||
existing = await domain.Publication.filter(post_id=post.id).first()
|
existing = await domain.PlacementPost.filter(post_id=post.id).first()
|
||||||
if existing:
|
if existing:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
publication = domain.Publication(
|
placement_post = domain.PlacementPost(
|
||||||
placement_id=placement.id,
|
placement_id=placement.id,
|
||||||
post_id=post.id,
|
post_id=post.id,
|
||||||
status=domain.PublicationStatus.ACTIVE,
|
status=domain.PlacementPostStatus.ACTIVE,
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.database.create_publication(publication)
|
await self.database.create_placement_post(placement_post)
|
||||||
created_count += 1
|
created_count += 1
|
||||||
log.info(
|
log.info(
|
||||||
'Created publication %s for placement %s from post %s',
|
'Created placement_post %s for placement %s from post %s',
|
||||||
publication.id,
|
placement_post.id,
|
||||||
placement.id,
|
placement.id,
|
||||||
post.id,
|
post.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
log.debug('Fetch publication post cycle completed. Created %s publications', created_count)
|
log.debug('Fetch placement_post post cycle completed. Created %s placement_posts', created_count)
|
||||||
@@ -9,47 +9,47 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def get_publication(self: 'Usecase', input: dto.GetPublicationInput) -> dto.PublicationOutput:
|
async def get_placement_post(self: 'Usecase', input: dto.GetPlacementPostInput) -> dto.PlacementPostOutput:
|
||||||
"""Получить одну публикацию по ID (system-managed post)"""
|
"""Получить одну публикацию по ID (system-managed post)"""
|
||||||
context = await self.ensure_workspace_permission(
|
context = await self.ensure_workspace_permission(
|
||||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
||||||
)
|
)
|
||||||
|
|
||||||
publication = await self.database.get_publication(input.workspace_id, input.publication_id)
|
placement_post = await self.database.get_placement_post(input.workspace_id, input.placement_post_id)
|
||||||
if not publication:
|
if not placement_post:
|
||||||
log.warning('Publication %s not found for user %s', input.publication_id, input.user_id)
|
log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id)
|
||||||
raise domain.PublicationNotFound(input.publication_id)
|
raise domain.PlacementPostNotFound(input.placement_post_id)
|
||||||
|
|
||||||
placement = publication.placement
|
placement = placement_post.placement
|
||||||
if not placement:
|
if not placement:
|
||||||
log.error('Publication %s missing placement data', publication.id)
|
log.error('PlacementPost %s missing placement data', placement_post.id)
|
||||||
raise domain.PlacementNotFound(publication.placement_id)
|
raise domain.PlacementNotFound(placement_post.placement_id)
|
||||||
|
|
||||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
|
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
|
||||||
|
|
||||||
if not placement.channel or not placement.project or not placement.creative:
|
if not placement.channel or not placement.project or not placement.creative:
|
||||||
log.error('Publication %s placement missing related data', publication.id)
|
log.error('PlacementPost %s placement missing related data', placement_post.id)
|
||||||
raise domain.PlacementNotFound(placement.id)
|
raise domain.PlacementNotFound(placement.id)
|
||||||
|
|
||||||
ad_post_url = publication.post.url if publication.post else None
|
ad_post_url = placement_post.post.url if placement_post.post else None
|
||||||
subscriptions_count = await self.database.count_subscriptions_by_publication(publication.id)
|
subscriptions_count = await self.database.count_subscriptions_by_placement_post(placement_post.id)
|
||||||
|
|
||||||
return dto.PublicationOutput(
|
return dto.PlacementPostOutput(
|
||||||
id=publication.id,
|
id=placement_post.id,
|
||||||
project_id=placement.project_id,
|
project_id=placement.project_id,
|
||||||
project_channel_title=placement.project.channel.title if placement.project.channel else None,
|
project_channel_title=placement.project.channel.title if placement.project.channel else None,
|
||||||
placement_channel_id=placement.channel_id,
|
placement_channel_id=placement.channel_id,
|
||||||
placement_channel_title=placement.channel.title,
|
placement_channel_title=placement.channel.title,
|
||||||
creative_id=placement.creative_id,
|
creative_id=placement.creative_id,
|
||||||
creative_name=placement.creative.name,
|
creative_name=placement.creative.name,
|
||||||
placement_date=placement.placement_at or publication.created_at,
|
placement_date=placement.placement_at or placement_post.created_at,
|
||||||
cost=placement.cost_value,
|
cost=placement.cost_value,
|
||||||
comment=placement.comment,
|
comment=placement.comment,
|
||||||
ad_post_url=ad_post_url,
|
ad_post_url=ad_post_url,
|
||||||
invite_link_type=placement.invite_link_type,
|
invite_link_type=placement.invite_link_type,
|
||||||
invite_link=placement.invite_link,
|
invite_link=placement.invite_link,
|
||||||
status=publication.status,
|
status=placement_post.status,
|
||||||
subscriptions_count=subscriptions_count,
|
subscriptions_count=subscriptions_count,
|
||||||
placement_id=placement.id,
|
placement_id=placement.id,
|
||||||
created_at=publication.created_at,
|
created_at=placement_post.created_at,
|
||||||
)
|
)
|
||||||
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def get_publications(self: 'Usecase', input: dto.GetPublicationsInput) -> dto.GetPublicationsOutput:
|
async def get_placement_posts(self: 'Usecase', input: dto.GetPlacementPostsInput) -> dto.GetPlacementPostsOutput:
|
||||||
"""Получить список публикаций (system-managed posts)"""
|
"""Получить список публикаций (system-managed posts)"""
|
||||||
context = await self.ensure_workspace_permission(
|
context = await self.ensure_workspace_permission(
|
||||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
||||||
@@ -21,7 +21,7 @@ async def get_publications(self: 'Usecase', input: dto.GetPublicationsInput) ->
|
|||||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, input.project_id)
|
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, input.project_id)
|
||||||
allowed_project_ids = None
|
allowed_project_ids = None
|
||||||
|
|
||||||
publications = await self.database.get_workspace_publications(
|
placement_posts = await self.database.get_workspace_placement_posts(
|
||||||
input.workspace_id,
|
input.workspace_id,
|
||||||
project_id=input.project_id,
|
project_id=input.project_id,
|
||||||
placement_channel_id=input.placement_channel_id,
|
placement_channel_id=input.placement_channel_id,
|
||||||
@@ -30,42 +30,42 @@ async def get_publications(self: 'Usecase', input: dto.GetPublicationsInput) ->
|
|||||||
allowed_project_ids=allowed_project_ids,
|
allowed_project_ids=allowed_project_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
publication_ids = [p.id for p in publications]
|
placement_post_ids = [p.id for p in placement_posts]
|
||||||
subscriptions_counts = await self.database.count_subscriptions_by_publication_batch(publication_ids)
|
subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids)
|
||||||
|
|
||||||
publication_outputs = []
|
placement_post_outputs = []
|
||||||
for publication in publications:
|
for placement_post in placement_posts:
|
||||||
placement = publication.placement
|
placement = placement_post.placement
|
||||||
if not placement:
|
if not placement:
|
||||||
log.warning('Publication %s missing placement data', publication.id)
|
log.warning('PlacementPost %s missing placement data', placement_post.id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not placement.channel or not placement.project or not placement.creative:
|
if not placement.channel or not placement.project or not placement.creative:
|
||||||
log.warning('Publication %s placement missing related data', publication.id)
|
log.warning('PlacementPost %s placement missing related data', placement_post.id)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
ad_post_url = publication.post.url if publication.post else None
|
ad_post_url = placement_post.post.url if placement_post.post else None
|
||||||
|
|
||||||
publication_outputs.append(
|
placement_post_outputs.append(
|
||||||
dto.PublicationOutput(
|
dto.PlacementPostOutput(
|
||||||
id=publication.id,
|
id=placement_post.id,
|
||||||
project_id=placement.project_id,
|
project_id=placement.project_id,
|
||||||
project_channel_title=placement.project.channel.title if placement.project.channel else None,
|
project_channel_title=placement.project.channel.title if placement.project.channel else None,
|
||||||
placement_channel_id=placement.channel_id,
|
placement_channel_id=placement.channel_id,
|
||||||
placement_channel_title=placement.channel.title,
|
placement_channel_title=placement.channel.title,
|
||||||
creative_id=placement.creative_id,
|
creative_id=placement.creative_id,
|
||||||
creative_name=placement.creative.name,
|
creative_name=placement.creative.name,
|
||||||
placement_date=placement.placement_at or publication.created_at,
|
placement_date=placement.placement_at or placement_post.created_at,
|
||||||
cost=placement.cost_value,
|
cost=placement.cost_value,
|
||||||
comment=placement.comment,
|
comment=placement.comment,
|
||||||
ad_post_url=ad_post_url,
|
ad_post_url=ad_post_url,
|
||||||
invite_link_type=placement.invite_link_type,
|
invite_link_type=placement.invite_link_type,
|
||||||
invite_link=placement.invite_link,
|
invite_link=placement.invite_link,
|
||||||
status=publication.status,
|
status=placement_post.status,
|
||||||
subscriptions_count=subscriptions_counts.get(publication.id, 0),
|
subscriptions_count=subscriptions_counts.get(placement_post.id, 0),
|
||||||
placement_id=placement.id,
|
placement_id=placement.id,
|
||||||
created_at=publication.created_at,
|
created_at=placement_post.created_at,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return dto.GetPublicationsOutput(publications=publication_outputs)
|
return dto.GetPlacementPostsOutput(placement_posts=placement_post_outputs)
|
||||||
@@ -28,4 +28,3 @@ async def archive_project(self: 'Usecase', input: dto.ArchiveProjectInput) -> dt
|
|||||||
status=project.status,
|
status=project.status,
|
||||||
purchase_invite_type_default=project.purchase_invite_type_default,
|
purchase_invite_type_default=project.purchase_invite_type_default,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -14,4 +14,3 @@ async def delete_project(self: 'Usecase', workspace_id: uuid.UUID, project_id: u
|
|||||||
|
|
||||||
async with self.database.transaction():
|
async with self.database.transaction():
|
||||||
await self.database.delete_project(workspace_id, project_id)
|
await self.database.delete_project(workspace_id, project_id)
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ async def update_project_invite_link_type(
|
|||||||
self: 'Usecase',
|
self: 'Usecase',
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
project_id: uuid.UUID,
|
project_id: uuid.UUID,
|
||||||
purchase_invite_type_default: domain.purchase.InviteLinkType,
|
purchase_invite_type_default: domain.InviteLinkType,
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
) -> dto.ProjectOutput:
|
) -> dto.ProjectOutput:
|
||||||
await self.ensure_workspace_permission(
|
await self.ensure_workspace_permission(
|
||||||
|
|||||||
@@ -117,19 +117,15 @@ async def create_placements(
|
|||||||
status=channel_input.status or domain.PlacementStatus.PLANNED,
|
status=channel_input.status or domain.PlacementStatus.PLANNED,
|
||||||
comment=channel_input.comment,
|
comment=channel_input.comment,
|
||||||
# Приоритет: channel details > общие details
|
# Приоритет: channel details > общие details
|
||||||
placement_at=(channel_details.placement_at if channel_details else None) or (
|
placement_at=(channel_details.placement_at if channel_details else None)
|
||||||
details.placement_at if details else None
|
or (details.placement_at if details else None),
|
||||||
),
|
|
||||||
payment_at=details.payment_at if details else None,
|
payment_at=details.payment_at if details else None,
|
||||||
cost_type=(channel_details.cost.type if channel_details and channel_details.cost else None) or (
|
cost_type=(channel_details.cost.type if channel_details and channel_details.cost else None)
|
||||||
details.cost.type if details and details.cost else None
|
or (details.cost.type if details and details.cost else None),
|
||||||
),
|
cost_value=(channel_details.cost.value if channel_details and channel_details.cost else None)
|
||||||
cost_value=(channel_details.cost.value if channel_details and channel_details.cost else None) or (
|
or (details.cost.value if details and details.cost else None),
|
||||||
details.cost.value if details and details.cost else None
|
cost_before_bargain=(channel_details.cost_before_bargain if channel_details else None)
|
||||||
),
|
or (details.cost_before_bargain if details else None),
|
||||||
cost_before_bargain=(channel_details.cost_before_bargain if channel_details else None) or (
|
|
||||||
details.cost_before_bargain if details else None
|
|
||||||
),
|
|
||||||
placement_type=details.placement_type if details else None,
|
placement_type=details.placement_type if details else None,
|
||||||
format=(channel_details.format if channel_details else None) or (details.format if details else None),
|
format=(channel_details.format if channel_details else None) or (details.format if details else None),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,13 +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:
|
||||||
publication = await self.database.get_publication_by_invite_link(invite_link)
|
placement_post = await self.database.get_placement_post_by_invite_link(invite_link)
|
||||||
if not publication:
|
if not placement_post:
|
||||||
log.warning('Publication not found for invite_link: %s', invite_link)
|
log.warning('PlacementPost not found for invite_link: %s', invite_link)
|
||||||
return
|
return
|
||||||
|
|
||||||
if not publication.placement:
|
if not placement_post.placement:
|
||||||
log.error('Publication %s has no placement', publication.id)
|
log.error('PlacementPost %s has no placement', placement_post.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id)
|
subscriber = await self.database.get_telegram_user(telegram_id=user_telegram_id)
|
||||||
@@ -42,7 +42,7 @@ async def handle_subscription(
|
|||||||
await self.database.update_telegram_user(subscriber)
|
await self.database.update_telegram_user(subscriber)
|
||||||
|
|
||||||
active_subscription = await self.database.get_active_subscription_by_subscriber_and_project(
|
active_subscription = await self.database.get_active_subscription_by_subscriber_and_project(
|
||||||
subscriber.id, publication.placement.project_id
|
subscriber.id, placement_post.placement.project_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if active_subscription:
|
if active_subscription:
|
||||||
@@ -50,18 +50,20 @@ async def handle_subscription(
|
|||||||
# Это не должно случиться (Telegram не даст подписаться дважды),
|
# Это не должно случиться (Telegram не даст подписаться дважды),
|
||||||
# но если случилось - логируем и игнорируем
|
# но если случилось - логируем и игнорируем
|
||||||
log.warning(
|
log.warning(
|
||||||
'User %s (telegram_id: %s) already has active subscription to channel %s via publication %s, '
|
'User %s (telegram_id: %s) already has active subscription to channel %s via placement_post %s, '
|
||||||
'ignoring new subscription attempt via publication %s',
|
'ignoring new subscription attempt via placement_post %s',
|
||||||
subscriber.id,
|
subscriber.id,
|
||||||
user_telegram_id,
|
user_telegram_id,
|
||||||
publication.placement.project_id,
|
placement_post.placement.project_id,
|
||||||
active_subscription.publication_id,
|
active_subscription.placement_post_id,
|
||||||
publication.id,
|
placement_post.id,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Проверяем, была ли раньше подписка через ЭТУ публикацию (для реактивации)
|
# Проверяем, была ли раньше подписка через ЭТУ публикацию (для реактивации)
|
||||||
existing_sub = await self.database.get_subscription_by_subscriber_and_publication(subscriber.id, publication.id)
|
existing_sub = await self.database.get_subscription_by_subscriber_and_placement_post(
|
||||||
|
subscriber.id, placement_post.id
|
||||||
|
)
|
||||||
|
|
||||||
if existing_sub and existing_sub.status == domain.SubscriptionStatus.UNSUBSCRIBED:
|
if existing_sub and existing_sub.status == domain.SubscriptionStatus.UNSUBSCRIBED:
|
||||||
existing_sub.status = domain.SubscriptionStatus.ACTIVE
|
existing_sub.status = domain.SubscriptionStatus.ACTIVE
|
||||||
@@ -69,24 +71,24 @@ async def handle_subscription(
|
|||||||
await self.database.update_subscription(existing_sub)
|
await self.database.update_subscription(existing_sub)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same publication %s',
|
'Subscription reactivated: subscriber %s (telegram_id: %s) resubscribed via same placement_post %s',
|
||||||
subscriber.id,
|
subscriber.id,
|
||||||
user_telegram_id,
|
user_telegram_id,
|
||||||
publication.id,
|
placement_post.id,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
subscription = domain.Subscription(
|
subscription = domain.Subscription(
|
||||||
publication_id=publication.id,
|
placement_post_id=placement_post.id,
|
||||||
telegram_user_id=subscriber.id,
|
telegram_user_id=subscriber.id,
|
||||||
invite_link=invite_link,
|
invite_link=invite_link,
|
||||||
)
|
)
|
||||||
await self.database.create_subscription(subscription)
|
await self.database.create_subscription(subscription)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
'Subscription created: subscriber %s (telegram_id: %s) subscribed via publication %s (invite_link: %s)',
|
'Subscription created: subscriber %s (telegram_id: %s) subscribed via placement_post %s (invite_link: %s)',
|
||||||
subscriber.id,
|
subscriber.id,
|
||||||
user_telegram_id,
|
user_telegram_id,
|
||||||
publication.id,
|
placement_post.id,
|
||||||
invite_link,
|
invite_link,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ async def handle_unsubscription(self: 'Usecase', user_telegram_id: int, channel_
|
|||||||
await self.database.update_subscription(subscription)
|
await self.database.update_subscription(subscription)
|
||||||
|
|
||||||
log.info(
|
log.info(
|
||||||
'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from publication %s',
|
'Subscription marked as unsubscribed: subscriber %s (telegram_id: %s) unsubscribed from placement_post %s',
|
||||||
subscriber.id,
|
subscriber.id,
|
||||||
user_telegram_id,
|
user_telegram_id,
|
||||||
subscription.publication_id,
|
subscription.placement_post_id,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,22 +14,22 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) ->
|
|||||||
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ
|
||||||
)
|
)
|
||||||
|
|
||||||
publication = await self.database.get_publication(input.workspace_id, input.publication_id)
|
placement_post = await self.database.get_placement_post(input.workspace_id, input.placement_post_id)
|
||||||
if not publication:
|
if not placement_post:
|
||||||
log.warning('Publication %s not found for user %s', input.publication_id, input.user_id)
|
log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id)
|
||||||
raise domain.PublicationNotFound(input.publication_id)
|
raise domain.PlacementPostNotFound(input.placement_post_id)
|
||||||
|
|
||||||
if not publication.placement:
|
if not placement_post.placement:
|
||||||
log.error('Publication %s has no placement', publication.id)
|
log.error('PlacementPost %s has no placement', placement_post.id)
|
||||||
raise domain.PlacementNotFound(publication.placement_id)
|
raise domain.PlacementNotFound(placement_post.placement_id)
|
||||||
|
|
||||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, publication.placement.project_id)
|
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement_post.placement.project_id)
|
||||||
|
|
||||||
if not publication.post:
|
if not placement_post.post:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
histories = await self.database.get_views_history(
|
histories = await self.database.get_views_history(
|
||||||
publication.post.id,
|
placement_post.post.id,
|
||||||
from_date=input.from_date,
|
from_date=input.from_date,
|
||||||
to_date=input.to_date,
|
to_date=input.to_date,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -33,4 +33,4 @@ async def accept_workspace_invite(
|
|||||||
else:
|
else:
|
||||||
await self.database.add_user_to_workspace(invite.workspace_id, user_id)
|
await self.database.add_user_to_workspace(invite.workspace_id, user_id)
|
||||||
|
|
||||||
return dto.WorkspaceInviteOutput.from_domain(invite)
|
return dto.WorkspaceInviteOutput.from_domain(invite)
|
||||||
|
|||||||
Reference in New Issue
Block a user