90% сеньоров плачат на таких коммитах
This commit is contained in:
@@ -1,16 +1,51 @@
|
|||||||
from tortoise import BaseDBAsyncClient
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
# ruff: noqa
|
|
||||||
RUN_IN_TRANSACTION = True
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
return """
|
return """
|
||||||
CREATE TABLE IF NOT EXISTS "telegram_state" (
|
CREATE TABLE IF NOT EXISTS "channel" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"telegram_id" BIGINT UNIQUE,
|
||||||
|
"title" VARCHAR(255),
|
||||||
|
"username" VARCHAR(255),
|
||||||
|
"access_hash" BIGINT,
|
||||||
|
"pts" INT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_channel_telegra_414c3b" ON "channel" ("telegram_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_channel_usernam_7fd87a" ON "channel" ("username");
|
||||||
|
CREATE TABLE IF NOT EXISTS "post" (
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"message_id" INT NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"deleted_from_channel_at" TIMESTAMPTZ,
|
||||||
|
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_post_channel_0a2cba" UNIQUE ("channel_id", "message_id")
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_post_channel_197bed" ON "post" ("channel_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "post_views_history" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
|
"views_count" INT NOT NULL,
|
||||||
|
"fetched_at" TIMESTAMPTZ NOT NULL,
|
||||||
|
"post_id" UUID NOT NULL REFERENCES "post" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_post_views__fetched_103eb5" ON "post_views_history" ("fetched_at");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_post_views__post_id_dfb1ca" ON "post_views_history" ("post_id");
|
||||||
|
CREATE TABLE IF NOT EXISTS "telegram_state" (
|
||||||
|
"id" UUID NOT NULL PRIMARY KEY,
|
||||||
|
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"telegram_id" BIGINT NOT NULL UNIQUE,
|
"telegram_id" BIGINT NOT NULL UNIQUE,
|
||||||
"state" VARCHAR(26) NOT NULL,
|
"state" VARCHAR(26) NOT NULL,
|
||||||
"context" JSONB NOT NULL
|
"context" JSONB NOT NULL
|
||||||
@@ -18,10 +53,10 @@ async def upgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
CREATE INDEX IF NOT EXISTS "idx_telegram_st_telegra_750503" ON "telegram_state" ("telegram_id");
|
CREATE INDEX IF NOT EXISTS "idx_telegram_st_telegra_750503" ON "telegram_state" ("telegram_id");
|
||||||
COMMENT ON COLUMN "telegram_state"."state" IS 'CREATIVE_WAITING_WORKSPACE: creative_waiting_workspace\nCREATIVE_WAITING_CHANNEL: creative_waiting_channel\nCREATIVE_WAITING_NAME: creative_waiting_name\nCREATIVE_WAITING_TEXT: creative_waiting_text\nPROJECT_WAITING_WORKSPACE: project_waiting_workspace';
|
COMMENT ON COLUMN "telegram_state"."state" IS 'CREATIVE_WAITING_WORKSPACE: creative_waiting_workspace\nCREATIVE_WAITING_CHANNEL: creative_waiting_channel\nCREATIVE_WAITING_NAME: creative_waiting_name\nCREATIVE_WAITING_TEXT: creative_waiting_text\nPROJECT_WAITING_WORKSPACE: project_waiting_workspace';
|
||||||
CREATE TABLE IF NOT EXISTS "user" (
|
CREATE TABLE IF NOT EXISTS "user" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"telegram_id" BIGINT NOT NULL UNIQUE,
|
"telegram_id" BIGINT NOT NULL UNIQUE,
|
||||||
"username" VARCHAR(255),
|
"username" VARCHAR(255),
|
||||||
"first_name" VARCHAR(255),
|
"first_name" VARCHAR(255),
|
||||||
@@ -29,10 +64,10 @@ CREATE TABLE IF NOT EXISTS "user" (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS "idx_user_telegra_66ffbd" ON "user" ("telegram_id");
|
CREATE INDEX IF NOT EXISTS "idx_user_telegra_66ffbd" ON "user" ("telegram_id");
|
||||||
CREATE TABLE IF NOT EXISTS "login_token" (
|
CREATE TABLE IF NOT EXISTS "login_token" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"token" VARCHAR(255) NOT NULL UNIQUE,
|
"token" VARCHAR(255) NOT NULL UNIQUE,
|
||||||
"expires_at" TIMESTAMPTZ NOT NULL,
|
"expires_at" TIMESTAMPTZ NOT NULL,
|
||||||
"used_at" TIMESTAMPTZ,
|
"used_at" TIMESTAMPTZ,
|
||||||
@@ -40,32 +75,17 @@ CREATE TABLE IF NOT EXISTS "login_token" (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS "idx_login_token_token_2582d2" ON "login_token" ("token");
|
CREATE INDEX IF NOT EXISTS "idx_login_token_token_2582d2" ON "login_token" ("token");
|
||||||
CREATE TABLE IF NOT EXISTS "workspace" (
|
CREATE TABLE IF NOT EXISTS "workspace" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"name" VARCHAR(255) NOT NULL
|
"name" VARCHAR(255) NOT NULL
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS "channel" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"telegram_id" BIGINT UNIQUE,
|
|
||||||
"username" VARCHAR(255),
|
|
||||||
"title" VARCHAR(255),
|
|
||||||
"verification_status" VARCHAR(10) NOT NULL DEFAULT 'unverified',
|
|
||||||
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_channel_telegra_414c3b" ON "channel" ("telegram_id");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_channel_usernam_7fd87a" ON "channel" ("username");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_channel_workspa_4a6a23" ON "channel" ("workspace_id");
|
|
||||||
COMMENT ON COLUMN "channel"."verification_status" IS 'UNVERIFIED: unverified\nVERIFIED: verified\nFAILED: failed';
|
|
||||||
CREATE TABLE IF NOT EXISTS "project" (
|
CREATE TABLE IF NOT EXISTS "project" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
"channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||||
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
|
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
|
||||||
@@ -75,10 +95,10 @@ CREATE INDEX IF NOT EXISTS "idx_project_channel_ce2466" ON "project" ("channel_i
|
|||||||
CREATE INDEX IF NOT EXISTS "idx_project_workspa_2354fd" ON "project" ("workspace_id");
|
CREATE INDEX IF NOT EXISTS "idx_project_workspa_2354fd" ON "project" ("workspace_id");
|
||||||
COMMENT ON COLUMN "project"."status" IS 'ACTIVE: active\nINACTIVE: inactive\nARCHIVED: archived';
|
COMMENT ON COLUMN "project"."status" IS 'ACTIVE: active\nINACTIVE: inactive\nARCHIVED: archived';
|
||||||
CREATE TABLE IF NOT EXISTS "creative" (
|
CREATE TABLE IF NOT EXISTS "creative" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"name" VARCHAR(255) NOT NULL,
|
"name" VARCHAR(255) NOT NULL,
|
||||||
"text" TEXT NOT NULL,
|
"text" TEXT NOT NULL,
|
||||||
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
@@ -90,50 +110,35 @@ CREATE INDEX IF NOT EXISTS "idx_creative_project_0766a6" ON "creative" ("project
|
|||||||
CREATE INDEX IF NOT EXISTS "idx_creative_workspa_2de546" ON "creative" ("workspace_id");
|
CREATE INDEX IF NOT EXISTS "idx_creative_workspa_2de546" ON "creative" ("workspace_id");
|
||||||
COMMENT ON COLUMN "creative"."status" IS 'ACTIVE: active\nARCHIVED: archived';
|
COMMENT ON COLUMN "creative"."status" IS 'ACTIVE: active\nARCHIVED: archived';
|
||||||
CREATE TABLE IF NOT EXISTS "placement" (
|
CREATE TABLE IF NOT EXISTS "placement" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"placement_date" TIMESTAMPTZ NOT NULL,
|
"placement_date" TIMESTAMPTZ NOT NULL,
|
||||||
"cost" DOUBLE PRECISION,
|
"cost" DOUBLE PRECISION,
|
||||||
"comment" TEXT,
|
"comment" TEXT,
|
||||||
"ad_post_url" VARCHAR(512),
|
|
||||||
"invite_link_type" VARCHAR(8) NOT NULL,
|
"invite_link_type" VARCHAR(8) NOT NULL,
|
||||||
"invite_link" VARCHAR(512) NOT NULL,
|
"invite_link" VARCHAR(512) NOT NULL,
|
||||||
"external_channel_message_id" INT,
|
|
||||||
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
"subscriptions_count" INT NOT NULL DEFAULT 0,
|
"subscriptions_count" INT NOT NULL DEFAULT 0,
|
||||||
"views_count" INT,
|
|
||||||
"views_availability" VARCHAR(11) NOT NULL DEFAULT 'unknown',
|
|
||||||
"last_views_fetch_at" TIMESTAMPTZ,
|
|
||||||
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
|
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
|
||||||
"placement_channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
"placement_channel_id" UUID NOT NULL REFERENCES "channel" ("id") ON DELETE CASCADE,
|
||||||
|
"post_id" UUID REFERENCES "post" ("id") ON DELETE SET NULL,
|
||||||
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
|
||||||
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE
|
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_creativ_32111d" ON "placement" ("creative_id");
|
CREATE INDEX IF NOT EXISTS "idx_placement_creativ_32111d" ON "placement" ("creative_id");
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_placeme_20cf53" ON "placement" ("placement_channel_id");
|
CREATE INDEX IF NOT EXISTS "idx_placement_placeme_20cf53" ON "placement" ("placement_channel_id");
|
||||||
|
CREATE INDEX IF NOT EXISTS "idx_placement_post_id_e4a16e" ON "placement" ("post_id");
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_project_2c6aa4" ON "placement" ("project_id");
|
CREATE INDEX IF NOT EXISTS "idx_placement_project_2c6aa4" ON "placement" ("project_id");
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_workspa_632b20" ON "placement" ("workspace_id");
|
CREATE INDEX IF NOT EXISTS "idx_placement_workspa_632b20" ON "placement" ("workspace_id");
|
||||||
COMMENT ON COLUMN "placement"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval';
|
COMMENT ON COLUMN "placement"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval';
|
||||||
COMMENT ON COLUMN "placement"."status" IS 'PENDING: pending\nACTIVE: active\nDELETED: deleted\nARCHIVED: archived';
|
COMMENT ON COLUMN "placement"."status" IS 'PENDING: pending\nACTIVE: active\nDELETED: deleted\nARCHIVED: archived';
|
||||||
COMMENT ON COLUMN "placement"."views_availability" IS 'UNKNOWN: unknown\nAVAILABLE: available\nUNAVAILABLE: unavailable\nMANUAL: manual';
|
|
||||||
CREATE TABLE IF NOT EXISTS "placement_views_history" (
|
|
||||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"deleted_at" TIMESTAMPTZ,
|
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"views_count" INT NOT NULL,
|
|
||||||
"fetched_at" TIMESTAMPTZ NOT NULL,
|
|
||||||
"placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_v_fetched_4c8f51" ON "placement_views_history" ("fetched_at");
|
|
||||||
CREATE INDEX IF NOT EXISTS "idx_placement_v_placeme_89e0f4" ON "placement_views_history" ("placement_id");
|
|
||||||
CREATE TABLE IF NOT EXISTS "purchase_plan" (
|
CREATE TABLE IF NOT EXISTS "purchase_plan" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
|
||||||
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
|
"project_id" UUID NOT NULL REFERENCES "project" ("id") ON DELETE CASCADE,
|
||||||
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE
|
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE
|
||||||
@@ -143,10 +148,10 @@ CREATE INDEX IF NOT EXISTS "idx_purchase_pl_workspa_e2459d" ON "purchase_plan" (
|
|||||||
CREATE INDEX IF NOT EXISTS "idx_purchase_pl_project_a3c246" ON "purchase_plan" ("project_id", "status");
|
CREATE INDEX IF NOT EXISTS "idx_purchase_pl_project_a3c246" ON "purchase_plan" ("project_id", "status");
|
||||||
COMMENT ON COLUMN "purchase_plan"."status" IS 'ACTIVE: active\nARCHIVED: archived';
|
COMMENT ON COLUMN "purchase_plan"."status" IS 'ACTIVE: active\nARCHIVED: archived';
|
||||||
CREATE TABLE IF NOT EXISTS "purchase_plan_channel" (
|
CREATE TABLE IF NOT EXISTS "purchase_plan_channel" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"status" VARCHAR(11) NOT NULL DEFAULT 'planned',
|
"status" VARCHAR(11) NOT NULL DEFAULT 'planned',
|
||||||
"planned_cost" DOUBLE PRECISION,
|
"planned_cost" DOUBLE PRECISION,
|
||||||
"comment" TEXT,
|
"comment" TEXT,
|
||||||
@@ -158,10 +163,10 @@ CREATE INDEX IF NOT EXISTS "idx_purchase_pl_channel_4cfc1c" ON "purchase_plan_ch
|
|||||||
CREATE INDEX IF NOT EXISTS "idx_purchase_pl_purchas_d20693" ON "purchase_plan_channel" ("purchase_plan_id");
|
CREATE INDEX IF NOT EXISTS "idx_purchase_pl_purchas_d20693" ON "purchase_plan_channel" ("purchase_plan_id");
|
||||||
COMMENT ON COLUMN "purchase_plan_channel"."status" IS 'PLANNED: planned\nAPPROVED: approved\nREJECTED: rejected\nIN_PROGRESS: in_progress\nCOMPLETED: completed';
|
COMMENT ON COLUMN "purchase_plan_channel"."status" IS 'PLANNED: planned\nAPPROVED: approved\nREJECTED: rejected\nIN_PROGRESS: in_progress\nCOMPLETED: completed';
|
||||||
CREATE TABLE IF NOT EXISTS "subscription" (
|
CREATE TABLE IF NOT EXISTS "subscription" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"invite_link" VARCHAR(512) NOT NULL,
|
"invite_link" VARCHAR(512) NOT NULL,
|
||||||
"status" VARCHAR(12) NOT NULL DEFAULT 'active',
|
"status" VARCHAR(12) NOT NULL DEFAULT 'active',
|
||||||
"unsubscribed_at" TIMESTAMPTZ,
|
"unsubscribed_at" TIMESTAMPTZ,
|
||||||
@@ -173,10 +178,10 @@ CREATE INDEX IF NOT EXISTS "idx_subscriptio_placeme_5decb2" ON "subscription" ("
|
|||||||
CREATE INDEX IF NOT EXISTS "idx_subscriptio_subscri_e75729" ON "subscription" ("subscriber_id");
|
CREATE INDEX IF NOT EXISTS "idx_subscriptio_subscri_e75729" ON "subscription" ("subscriber_id");
|
||||||
COMMENT ON COLUMN "subscription"."status" IS 'ACTIVE: active\nUNSUBSCRIBED: unsubscribed';
|
COMMENT ON COLUMN "subscription"."status" IS 'ACTIVE: active\nUNSUBSCRIBED: unsubscribed';
|
||||||
CREATE TABLE IF NOT EXISTS "workspace_invite" (
|
CREATE TABLE IF NOT EXISTS "workspace_invite" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"status" VARCHAR(8) NOT NULL DEFAULT 'pending',
|
"status" VARCHAR(8) NOT NULL DEFAULT 'pending',
|
||||||
"invited_by_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
"invited_by_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
@@ -188,10 +193,10 @@ CREATE INDEX IF NOT EXISTS "idx_workspace_i_user_id_a96e1e" ON "workspace_invite
|
|||||||
CREATE INDEX IF NOT EXISTS "idx_workspace_i_workspa_71e9d2" ON "workspace_invite" ("workspace_id");
|
CREATE INDEX IF NOT EXISTS "idx_workspace_i_workspa_71e9d2" ON "workspace_invite" ("workspace_id");
|
||||||
COMMENT ON COLUMN "workspace_invite"."status" IS 'PENDING: pending\nACCEPTED: accepted\nREVOKED: revoked';
|
COMMENT ON COLUMN "workspace_invite"."status" IS 'PENDING: pending\nACCEPTED: accepted\nREVOKED: revoked';
|
||||||
CREATE TABLE IF NOT EXISTS "workspace_user" (
|
CREATE TABLE IF NOT EXISTS "workspace_user" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
|
||||||
"status" VARCHAR(7) NOT NULL DEFAULT 'active',
|
"status" VARCHAR(7) NOT NULL DEFAULT 'active',
|
||||||
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
|
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
|
||||||
@@ -199,32 +204,32 @@ CREATE TABLE IF NOT EXISTS "workspace_user" (
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS "idx_workspace_u_user_id_246318" ON "workspace_user" ("user_id");
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_user_id_246318" ON "workspace_user" ("user_id");
|
||||||
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_a403b0" ON "workspace_user" ("workspace_id");
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_a403b0" ON "workspace_user" ("workspace_id");
|
||||||
COMMENT ON COLUMN "workspace_user"."status" IS 'ACTIVE: active\nINVITED: invited\nREMOVED: removed';
|
COMMENT ON COLUMN "workspace_user"."status" IS 'ACTIVE: active\nINVITED: invited\nBLOCKED: blocked';
|
||||||
CREATE TABLE IF NOT EXISTS "workspace_user_permission" (
|
CREATE TABLE IF NOT EXISTS "workspace_user_permission" (
|
||||||
"id" SERIAL 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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"permission" VARCHAR(16) NOT NULL,
|
"permission" VARCHAR(24) NOT NULL,
|
||||||
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE,
|
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE,
|
||||||
CONSTRAINT "uid_workspace_u_workspa_86f574" UNIQUE ("workspace_user_id", "permission")
|
CONSTRAINT "uid_workspace_u_workspa_86f574" UNIQUE ("workspace_user_id", "permission")
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_b9b7b0" ON "workspace_user_permission" ("workspace_user_id");
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_b9b7b0" ON "workspace_user_permission" ("workspace_user_id");
|
||||||
COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'PROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nADMIN_FULL: admin_full';
|
COMMENT ON COLUMN "workspace_user_permission"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write';
|
||||||
CREATE TABLE IF NOT EXISTS "workspace_user_permission_scope" (
|
CREATE TABLE IF NOT EXISTS "workspace_user_permission_scope" (
|
||||||
|
"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,
|
||||||
"deleted_at" TIMESTAMPTZ,
|
"deleted_at" TIMESTAMPTZ,
|
||||||
"id" UUID NOT NULL PRIMARY KEY,
|
"permission" VARCHAR(24) NOT NULL,
|
||||||
"permission" VARCHAR(16) NOT NULL,
|
|
||||||
"scope_type" VARCHAR(9) NOT NULL,
|
"scope_type" VARCHAR(9) NOT NULL,
|
||||||
"scope_id" UUID NOT NULL,
|
"scope_id" UUID NOT NULL,
|
||||||
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE,
|
"workspace_user_id" UUID NOT NULL REFERENCES "workspace_user" ("id") ON DELETE CASCADE,
|
||||||
CONSTRAINT "uid_workspace_u_workspa_488ed3" UNIQUE ("workspace_user_id", "permission", "scope_type", "scope_id")
|
CONSTRAINT "uid_workspace_u_workspa_488ed3" UNIQUE ("workspace_user_id", "permission", "scope_type", "scope_id")
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_82a35a" ON "workspace_user_permission_scope" ("workspace_user_id");
|
CREATE INDEX IF NOT EXISTS "idx_workspace_u_workspa_82a35a" ON "workspace_user_permission_scope" ("workspace_user_id");
|
||||||
COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'PROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nADMIN_FULL: admin_full';
|
COMMENT ON COLUMN "workspace_user_permission_scope"."permission" IS 'ADMIN_FULL: admin_full\nPROJECTS_READ: projects_read\nPROJECTS_WRITE: projects_write\nCREATIVES_READ: creatives_read\nCREATIVES_WRITE: creatives_write\nPLACEMENTS_READ: placements_read\nPLACEMENTS_WRITE: placements_write\nANALYTICS_READ: analytics_read\nANALYTICS_WITHOUT_CLICKS: analytics_without_clicks\nCHANNELS_READ: channels_read\nCHANNELS_WRITE: channels_write';
|
||||||
COMMENT ON COLUMN "workspace_user_permission_scope"."scope_type" IS 'WORKSPACE: workspace\nPROJECT: project';
|
COMMENT ON COLUMN "workspace_user_permission_scope"."scope_type" IS 'PROJECT: project\nCREATIVE: creative\nPLACEMENT: placement\nCHANNEL: channel';
|
||||||
CREATE TABLE IF NOT EXISTS "aerich" (
|
CREATE TABLE IF NOT EXISTS "aerich" (
|
||||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||||
"version" VARCHAR(255) NOT NULL,
|
"version" VARCHAR(255) NOT NULL,
|
||||||
@@ -239,73 +244,74 @@ async def downgrade(db: BaseDBAsyncClient) -> str:
|
|||||||
|
|
||||||
|
|
||||||
MODELS_STATE = (
|
MODELS_STATE = (
|
||||||
'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"
|
||||||
'IXWrBwut7SW8G0cI/f4/Nf+ymg=='
|
"T7teeG4vJc31z43AnSwLvCA9eWXKRi9QZc+jSQU/J3Ro5ADRKb6dAB4dHmYAkJRKvnqb5j"
|
||||||
|
"HuO2Ldj/vOfUe41vuO1vYR3o2bjX7/H/FMJEI="
|
||||||
)
|
)
|
||||||
@@ -177,22 +177,17 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
async def get_channel(
|
async def get_channel(
|
||||||
self,
|
self,
|
||||||
workspace_id: uuid.UUID,
|
|
||||||
channel_id: uuid.UUID | None = None,
|
channel_id: uuid.UUID | None = None,
|
||||||
telegram_id: int | None = None,
|
telegram_id: int | None = None,
|
||||||
username: str | None = None,
|
username: str | None = None,
|
||||||
) -> domain.Channel | None:
|
) -> domain.Channel | None:
|
||||||
query = domain.Channel.filter(workspace_id=workspace_id)
|
|
||||||
if channel_id:
|
if channel_id:
|
||||||
query = query.filter(id=channel_id)
|
return await domain.Channel.get_or_none(id=channel_id)
|
||||||
if telegram_id:
|
if telegram_id:
|
||||||
query = query.filter(telegram_id=telegram_id)
|
return await domain.Channel.get_or_none(telegram_id=telegram_id)
|
||||||
if username:
|
if username:
|
||||||
query = query.filter(username=username)
|
return await domain.Channel.filter(username__iexact=username).first()
|
||||||
return await query.first()
|
return None
|
||||||
|
|
||||||
async def get_workspace_channels(self, workspace_id: uuid.UUID) -> list[domain.Channel]:
|
|
||||||
return await domain.Channel.filter(workspace_id=workspace_id).all()
|
|
||||||
|
|
||||||
async def create_channel(self, channel: domain.Channel) -> None:
|
async def create_channel(self, channel: domain.Channel) -> None:
|
||||||
await channel.save()
|
await channel.save()
|
||||||
@@ -345,7 +340,7 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
async def get_placement(self, workspace_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None:
|
async def get_placement(self, workspace_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None:
|
||||||
return await domain.Placement.get_or_none(id=placement_id, workspace_id=workspace_id).prefetch_related(
|
return await domain.Placement.get_or_none(id=placement_id, workspace_id=workspace_id).prefetch_related(
|
||||||
'project', 'project__channel', 'placement_channel', 'creative'
|
'project', 'project__channel', 'placement_channel', 'creative', 'post', 'post__channel'
|
||||||
)
|
)
|
||||||
|
|
||||||
async def create_placement(self, placement: domain.Placement) -> None:
|
async def create_placement(self, placement: domain.Placement) -> None:
|
||||||
@@ -380,7 +375,9 @@ class Postgres(DatabaseBase):
|
|||||||
query = query.filter(status=domain.PlacementStatus.ACTIVE)
|
query = query.filter(status=domain.PlacementStatus.ACTIVE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
await query.prefetch_related('project', 'project__channel', 'placement_channel', 'creative')
|
await query.prefetch_related(
|
||||||
|
'project', 'project__channel', 'placement_channel', 'creative', 'post', 'post__channel'
|
||||||
|
)
|
||||||
.order_by('-placement_date')
|
.order_by('-placement_date')
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
@@ -430,17 +427,28 @@ class Postgres(DatabaseBase):
|
|||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
async def create_placement_views_history(self, history: domain.PlacementViewsHistory) -> None:
|
# Post methods
|
||||||
|
async def create_post(self, post: domain.Post) -> None:
|
||||||
|
await post.save()
|
||||||
|
|
||||||
|
async def get_post(self, post_id: uuid.UUID) -> domain.Post | None:
|
||||||
|
return await domain.Post.get_or_none(id=post_id).prefetch_related('channel')
|
||||||
|
|
||||||
|
async def get_post_by_channel_and_message(self, channel_id: uuid.UUID, message_id: int) -> domain.Post | None:
|
||||||
|
return await domain.Post.get_or_none(channel_id=channel_id, message_id=message_id).prefetch_related('channel')
|
||||||
|
|
||||||
|
# Post views history
|
||||||
|
async def create_post_views_history(self, history: domain.PostViewsHistory) -> None:
|
||||||
await history.save()
|
await history.save()
|
||||||
|
|
||||||
async def get_views_history(
|
async def get_views_history(
|
||||||
self,
|
self,
|
||||||
placement_id: uuid.UUID,
|
post_id: uuid.UUID,
|
||||||
*,
|
*,
|
||||||
from_date: datetime.datetime | None = None,
|
from_date: datetime.datetime | None = None,
|
||||||
to_date: datetime.datetime | None = None,
|
to_date: datetime.datetime | None = None,
|
||||||
) -> list[domain.PlacementViewsHistory]:
|
) -> list[domain.PostViewsHistory]:
|
||||||
query = domain.PlacementViewsHistory.filter(placement_id=placement_id)
|
query = domain.PostViewsHistory.filter(post_id=post_id)
|
||||||
|
|
||||||
if from_date:
|
if from_date:
|
||||||
query = query.filter(fetched_at__gte=from_date)
|
query = query.filter(fetched_at__gte=from_date)
|
||||||
@@ -449,6 +457,26 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
return await query.order_by('fetched_at').all()
|
return await query.order_by('fetched_at').all()
|
||||||
|
|
||||||
|
async def get_latest_views_data(self, post_id: uuid.UUID) -> tuple[int, datetime.datetime] | None:
|
||||||
|
latest = await domain.PostViewsHistory.filter(post_id=post_id).order_by('-fetched_at').first()
|
||||||
|
if latest:
|
||||||
|
return (latest.views_count, latest.fetched_at)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def get_latest_views_data_batch(
|
||||||
|
self, post_ids: list[uuid.UUID]
|
||||||
|
) -> dict[uuid.UUID, tuple[int, datetime.datetime]]:
|
||||||
|
if not post_ids:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
results: dict[uuid.UUID, tuple[int, datetime.datetime]] = {}
|
||||||
|
for post_id in post_ids:
|
||||||
|
latest = await domain.PostViewsHistory.filter(post_id=post_id).order_by('-fetched_at').first()
|
||||||
|
if latest:
|
||||||
|
results[post_id] = (latest.views_count, latest.fetched_at)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
async def get_telegram_state(self, telegram_id: int) -> domain.TelegramState | None:
|
async def get_telegram_state(self, telegram_id: int) -> domain.TelegramState | None:
|
||||||
return await domain.TelegramState.get_or_none(telegram_id=telegram_id)
|
return await domain.TelegramState.get_or_none(telegram_id=telegram_id)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ __all__ = (
|
|||||||
'PermissionKey',
|
'PermissionKey',
|
||||||
'PermissionScopeType',
|
'PermissionScopeType',
|
||||||
'Channel',
|
'Channel',
|
||||||
'ChannelVerificationStatus',
|
|
||||||
'Project',
|
'Project',
|
||||||
'ProjectStatus',
|
'ProjectStatus',
|
||||||
'PurchasePlan',
|
'PurchasePlan',
|
||||||
@@ -22,8 +21,9 @@ __all__ = (
|
|||||||
'PurchasePlanChannelStatus',
|
'PurchasePlanChannelStatus',
|
||||||
'Creative',
|
'Creative',
|
||||||
'Placement',
|
'Placement',
|
||||||
|
'Post',
|
||||||
'Subscription',
|
'Subscription',
|
||||||
'PlacementViewsHistory',
|
'PostViewsHistory',
|
||||||
'TelegramState',
|
'TelegramState',
|
||||||
'TelegramStateEnum',
|
'TelegramStateEnum',
|
||||||
'ChannelNotFound',
|
'ChannelNotFound',
|
||||||
@@ -55,7 +55,7 @@ __all__ = (
|
|||||||
'UserByUsernameNotFound',
|
'UserByUsernameNotFound',
|
||||||
)
|
)
|
||||||
|
|
||||||
from .channel import Channel, ChannelVerificationStatus
|
from .channel import Channel
|
||||||
from .creative import Creative, CreativeStatus
|
from .creative import Creative, CreativeStatus
|
||||||
from .error import (
|
from .error import (
|
||||||
ChannelAlreadyExists,
|
ChannelAlreadyExists,
|
||||||
@@ -80,7 +80,8 @@ from .error import (
|
|||||||
)
|
)
|
||||||
from .login_token import LoginToken
|
from .login_token import LoginToken
|
||||||
from .placement import InviteLinkType, Placement, PlacementStatus, PostViewsAvailability
|
from .placement import InviteLinkType, Placement, PlacementStatus, PostViewsAvailability
|
||||||
from .placement_views_history import PlacementViewsHistory
|
from .post import Post
|
||||||
|
from .post_views_history import PostViewsHistory
|
||||||
from .project import Project, ProjectStatus
|
from .project import Project, ProjectStatus
|
||||||
from .purchase_plan import (
|
from .purchase_plan import (
|
||||||
PurchasePlan,
|
PurchasePlan,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from tortoise.models import Model
|
|||||||
|
|
||||||
|
|
||||||
class TimestampedModel(Model):
|
class TimestampedModel(Model):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
created_at = fields.DatetimeField(auto_now_add=True)
|
created_at = fields.DatetimeField(auto_now_add=True)
|
||||||
updated_at = fields.DatetimeField(auto_now=True)
|
updated_at = fields.DatetimeField(auto_now=True)
|
||||||
deleted_at = fields.DatetimeField(null=True)
|
deleted_at = fields.DatetimeField(null=True)
|
||||||
|
|||||||
@@ -1,34 +1,15 @@
|
|||||||
import enum
|
|
||||||
import uuid
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from tortoise import fields
|
from tortoise import fields
|
||||||
|
|
||||||
from .base import TimestampedModel
|
from .base import TimestampedModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .workspace import Workspace
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelVerificationStatus(str, enum.Enum):
|
|
||||||
UNVERIFIED = 'unverified'
|
|
||||||
VERIFIED = 'verified'
|
|
||||||
FAILED = 'failed'
|
|
||||||
|
|
||||||
|
|
||||||
class Channel(TimestampedModel):
|
class Channel(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
telegram_id = fields.BigIntField(null=True, unique=True, index=True)
|
telegram_id = fields.BigIntField(null=True, unique=True, index=True)
|
||||||
username = fields.CharField(max_length=255, null=True, index=True)
|
title = fields.CharField(null=True, max_length=255)
|
||||||
title = fields.CharField(max_length=255, null=True)
|
username = fields.CharField(null=True, max_length=255, index=True)
|
||||||
verification_status = fields.CharEnumField(ChannelVerificationStatus, default=ChannelVerificationStatus.UNVERIFIED)
|
|
||||||
|
|
||||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
access_hash = fields.BigIntField(null=True)
|
||||||
'models.Workspace', related_name='channels', on_delete=fields.CASCADE, index=True
|
pts = fields.IntField(null=True)
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
workspace_id: uuid.UUID
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'channel'
|
table = 'channel'
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class CreativeStatus(str, enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
class Creative(TimestampedModel):
|
class Creative(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
name = fields.CharField(max_length=255)
|
name = fields.CharField(max_length=255)
|
||||||
text = fields.TextField()
|
text = fields.TextField()
|
||||||
status = fields.CharEnumField(CreativeStatus, default=CreativeStatus.ACTIVE)
|
status = fields.CharEnumField(CreativeStatus, default=CreativeStatus.ACTIVE)
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class LoginToken(TimestampedModel):
|
class LoginToken(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
token = fields.CharField(max_length=255, unique=True, index=True)
|
token = fields.CharField(max_length=255, unique=True, index=True)
|
||||||
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
||||||
'models.User', related_name='login_tokens', on_delete=fields.CASCADE
|
'models.User', related_name='login_tokens', on_delete=fields.CASCADE
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from .base import TimestampedModel
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .channel import Channel
|
from .channel import Channel
|
||||||
from .creative import Creative
|
from .creative import Creative
|
||||||
|
from .post import Post
|
||||||
from .project import Project
|
from .project import Project
|
||||||
from .workspace import Workspace
|
from .workspace import Workspace
|
||||||
|
|
||||||
@@ -33,22 +34,14 @@ class PostViewsAvailability(str, enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
class Placement(TimestampedModel):
|
class Placement(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
placement_date = fields.DatetimeField()
|
placement_date = fields.DatetimeField()
|
||||||
cost = fields.FloatField(null=True)
|
cost = fields.FloatField(null=True)
|
||||||
comment = fields.TextField(null=True)
|
comment = fields.TextField(null=True)
|
||||||
ad_post_url = fields.CharField(max_length=512, null=True)
|
|
||||||
invite_link_type = fields.CharEnumField(InviteLinkType)
|
invite_link_type = fields.CharEnumField(InviteLinkType)
|
||||||
invite_link = fields.CharField(max_length=512)
|
invite_link = fields.CharField(max_length=512)
|
||||||
external_channel_message_id = fields.IntField(null=True)
|
|
||||||
|
|
||||||
status = fields.CharEnumField(PlacementStatus, default=PlacementStatus.ACTIVE)
|
status = fields.CharEnumField(PlacementStatus, default=PlacementStatus.ACTIVE)
|
||||||
|
|
||||||
subscriptions_count = fields.IntField(default=0)
|
subscriptions_count = fields.IntField(default=0)
|
||||||
views_count = fields.IntField(null=True)
|
|
||||||
|
|
||||||
views_availability = fields.CharEnumField(PostViewsAvailability, default=PostViewsAvailability.UNKNOWN)
|
|
||||||
last_views_fetch_at = fields.DatetimeField(null=True)
|
|
||||||
|
|
||||||
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
|
||||||
'models.Project', related_name='placements', on_delete=fields.CASCADE, index=True
|
'models.Project', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||||
@@ -62,12 +55,16 @@ class Placement(TimestampedModel):
|
|||||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
'models.Workspace', related_name='placements', on_delete=fields.CASCADE, index=True
|
'models.Workspace', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
post: fields.ForeignKeyRelation['Post'] | None = fields.ForeignKeyField(
|
||||||
|
'models.Post', related_name='placements', on_delete=fields.SET_NULL, null=True, index=True
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
project_id: UUID
|
project_id: UUID
|
||||||
placement_channel_id: UUID
|
placement_channel_id: UUID
|
||||||
creative_id: UUID
|
creative_id: UUID
|
||||||
workspace_id: UUID
|
workspace_id: UUID
|
||||||
|
post_id: UUID | None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'placement'
|
table = 'placement'
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
from typing import TYPE_CHECKING
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from tortoise import fields
|
|
||||||
|
|
||||||
from .base import TimestampedModel
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .placement import Placement
|
|
||||||
|
|
||||||
|
|
||||||
class PlacementViewsHistory(TimestampedModel):
|
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
|
|
||||||
views_count = fields.IntField() # Количество просмотров на момент снимка
|
|
||||||
fetched_at = fields.DatetimeField(index=True)
|
|
||||||
|
|
||||||
placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField(
|
|
||||||
'models.Placement', related_name='views_histories', on_delete=fields.CASCADE, index=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
placement_id: UUID
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
table = 'placement_views_history'
|
|
||||||
26
src/domain/post.py
Normal file
26
src/domain/post.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from tortoise import fields
|
||||||
|
|
||||||
|
from .base import TimestampedModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .channel import Channel
|
||||||
|
|
||||||
|
|
||||||
|
class Post(TimestampedModel):
|
||||||
|
message_id = fields.IntField()
|
||||||
|
text = fields.TextField()
|
||||||
|
deleted_from_channel_at = fields.DatetimeField(null=True)
|
||||||
|
|
||||||
|
channel: fields.ForeignKeyRelation['Channel'] = fields.ForeignKeyField(
|
||||||
|
'models.Channel', related_name='posts', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
channel_id: uuid.UUID
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'post'
|
||||||
|
unique_together = (('channel_id', 'message_id'),)
|
||||||
24
src/domain/post_views_history.py
Normal file
24
src/domain/post_views_history.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from tortoise import fields
|
||||||
|
|
||||||
|
from .base import TimestampedModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .post import Post
|
||||||
|
|
||||||
|
|
||||||
|
class PostViewsHistory(TimestampedModel):
|
||||||
|
views_count = fields.IntField() # Количество просмотров на момент снимка
|
||||||
|
fetched_at = fields.DatetimeField(index=True)
|
||||||
|
|
||||||
|
post: fields.ForeignKeyRelation['Post'] = fields.ForeignKeyField(
|
||||||
|
'models.Post', related_name='views_histories', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
post_id: UUID
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'post_views_history'
|
||||||
@@ -18,7 +18,6 @@ class ProjectStatus(str, enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
class Project(TimestampedModel):
|
class Project(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
status = fields.CharEnumField(ProjectStatus, default=ProjectStatus.ACTIVE)
|
status = fields.CharEnumField(ProjectStatus, default=ProjectStatus.ACTIVE)
|
||||||
|
|
||||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class PurchasePlanChannelStatus(str, enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
class PurchasePlan(TimestampedModel):
|
class PurchasePlan(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
status = fields.CharEnumField(PurchasePlanStatus, default=PurchasePlanStatus.ACTIVE)
|
status = fields.CharEnumField(PurchasePlanStatus, default=PurchasePlanStatus.ACTIVE)
|
||||||
|
|
||||||
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
@@ -42,7 +41,6 @@ class PurchasePlan(TimestampedModel):
|
|||||||
|
|
||||||
|
|
||||||
class PurchasePlanChannel(TimestampedModel):
|
class PurchasePlanChannel(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
status = fields.CharEnumField(PurchasePlanChannelStatus, default=PurchasePlanChannelStatus.PLANNED)
|
status = fields.CharEnumField(PurchasePlanChannelStatus, default=PurchasePlanChannelStatus.PLANNED)
|
||||||
planned_cost = fields.FloatField(null=True)
|
planned_cost = fields.FloatField(null=True)
|
||||||
comment = fields.TextField(null=True)
|
comment = fields.TextField(null=True)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ class SubscriptionStatus(str, enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
class Subscription(TimestampedModel):
|
class Subscription(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
invite_link = fields.CharField(max_length=512, index=True)
|
invite_link = fields.CharField(max_length=512, index=True)
|
||||||
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)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class TelegramStateEnum(str, enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
class TelegramState(TimestampedModel):
|
class TelegramState(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
telegram_id = fields.BigIntField(unique=True, index=True)
|
||||||
state = fields.CharEnumField(TelegramStateEnum)
|
state = fields.CharEnumField(TelegramStateEnum)
|
||||||
context = fields.JSONField(default=dict)
|
context = fields.JSONField(default=dict)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from .base import TimestampedModel
|
|||||||
|
|
||||||
|
|
||||||
class User(TimestampedModel):
|
class User(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
telegram_id = fields.BigIntField(unique=True, index=True)
|
||||||
username = fields.CharField(max_length=255, null=True)
|
username = fields.CharField(max_length=255, null=True)
|
||||||
first_name = fields.CharField(max_length=255, null=True)
|
first_name = fields.CharField(max_length=255, null=True)
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ class WorkspaceUserStatus(str, enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceUser(TimestampedModel):
|
class WorkspaceUser(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
status = fields.CharEnumField(WorkspaceUserStatus, default=WorkspaceUserStatus.ACTIVE)
|
status = fields.CharEnumField(WorkspaceUserStatus, default=WorkspaceUserStatus.ACTIVE)
|
||||||
|
|
||||||
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
||||||
@@ -53,7 +52,6 @@ class WorkspaceInviteStatus(str, enum.Enum):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceInvite(TimestampedModel):
|
class WorkspaceInvite(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
status = fields.CharEnumField(WorkspaceInviteStatus, default=WorkspaceInviteStatus.PENDING)
|
status = fields.CharEnumField(WorkspaceInviteStatus, default=WorkspaceInviteStatus.PENDING)
|
||||||
|
|
||||||
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
||||||
@@ -114,7 +112,6 @@ class WorkspaceUserPermission(TimestampedModel):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceUserPermissionScope(TimestampedModel):
|
class WorkspaceUserPermissionScope(TimestampedModel):
|
||||||
id = fields.UUIDField(pk=True)
|
|
||||||
permission = fields.CharEnumField(PermissionKey)
|
permission = fields.CharEnumField(PermissionKey)
|
||||||
scope_type = fields.CharEnumField(PermissionScopeType)
|
scope_type = fields.CharEnumField(PermissionScopeType)
|
||||||
scope_id = fields.UUIDField()
|
scope_id = fields.UUIDField()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ __all__ = (
|
|||||||
'CreatePlacementInput',
|
'CreatePlacementInput',
|
||||||
'UpdatePlacementInput',
|
'UpdatePlacementInput',
|
||||||
'DeletePlacementInput',
|
'DeletePlacementInput',
|
||||||
'PlacementViewsHistoryOutput',
|
'PostViewsHistoryOutput',
|
||||||
'GetViewsHistoryInput',
|
'GetViewsHistoryInput',
|
||||||
'GetViewsHistoryOutput',
|
'GetViewsHistoryOutput',
|
||||||
'UpdateViewsManuallyInput',
|
'UpdateViewsManuallyInput',
|
||||||
@@ -123,7 +123,7 @@ from .validate_login_token import ValidateLoginTokenInput, ValidateLoginTokenOut
|
|||||||
from .views import (
|
from .views import (
|
||||||
GetViewsHistoryInput,
|
GetViewsHistoryInput,
|
||||||
GetViewsHistoryOutput,
|
GetViewsHistoryOutput,
|
||||||
PlacementViewsHistoryOutput,
|
PostViewsHistoryOutput,
|
||||||
UpdateViewsManuallyInput,
|
UpdateViewsManuallyInput,
|
||||||
)
|
)
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
|
|||||||
@@ -2,15 +2,12 @@ import uuid
|
|||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
from src.domain.channel import ChannelVerificationStatus
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelOutput(pydantic.BaseModel):
|
class ChannelOutput(pydantic.BaseModel):
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
telegram_id: int | None
|
telegram_id: int | None
|
||||||
title: str | None
|
title: str | None
|
||||||
username: str | None
|
username: str | None
|
||||||
verification_status: ChannelVerificationStatus
|
|
||||||
|
|
||||||
|
|
||||||
class GetChannelsInput(pydantic.BaseModel):
|
class GetChannelsInput(pydantic.BaseModel):
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import uuid
|
|||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
|
|
||||||
class PlacementViewsHistoryOutput(pydantic.BaseModel):
|
class PostViewsHistoryOutput(pydantic.BaseModel):
|
||||||
"""История просмотров placement."""
|
"""История просмотров поста."""
|
||||||
|
|
||||||
id: uuid.UUID
|
id: uuid.UUID
|
||||||
placement_id: uuid.UUID
|
post_id: uuid.UUID
|
||||||
views_count: int
|
views_count: int
|
||||||
fetched_at: datetime.datetime
|
fetched_at: datetime.datetime
|
||||||
created_at: datetime.datetime
|
created_at: datetime.datetime
|
||||||
@@ -27,7 +27,7 @@ class GetViewsHistoryInput(pydantic.BaseModel):
|
|||||||
class GetViewsHistoryOutput(pydantic.BaseModel):
|
class GetViewsHistoryOutput(pydantic.BaseModel):
|
||||||
"""История просмотров."""
|
"""История просмотров."""
|
||||||
|
|
||||||
histories: list[PlacementViewsHistoryOutput]
|
histories: list[PostViewsHistoryOutput]
|
||||||
|
|
||||||
|
|
||||||
class UpdateViewsManuallyInput(pydantic.BaseModel):
|
class UpdateViewsManuallyInput(pydantic.BaseModel):
|
||||||
|
|||||||
@@ -122,14 +122,11 @@ class Database(typing.Protocol):
|
|||||||
|
|
||||||
async def get_channel(
|
async def get_channel(
|
||||||
self,
|
self,
|
||||||
workspace_id: UUID,
|
|
||||||
channel_id: UUID | None = None,
|
channel_id: UUID | None = None,
|
||||||
telegram_id: int | None = None,
|
telegram_id: int | None = None,
|
||||||
username: str | None = None,
|
username: str | None = None,
|
||||||
) -> domain.Channel | None: ...
|
) -> domain.Channel | None: ...
|
||||||
|
|
||||||
async def get_workspace_channels(self, workspace_id: UUID) -> list[domain.Channel]: ...
|
|
||||||
|
|
||||||
async def create_channel(self, channel: domain.Channel) -> None: ...
|
async def create_channel(self, channel: domain.Channel) -> None: ...
|
||||||
|
|
||||||
async def update_channel(self, channel: domain.Channel) -> None: ...
|
async def update_channel(self, channel: domain.Channel) -> None: ...
|
||||||
@@ -232,15 +229,27 @@ class Database(typing.Protocol):
|
|||||||
self, subscriber_id: UUID, project_id: UUID
|
self, subscriber_id: UUID, project_id: UUID
|
||||||
) -> domain.Subscription | None: ...
|
) -> domain.Subscription | None: ...
|
||||||
|
|
||||||
async def create_placement_views_history(self, history: domain.PlacementViewsHistory) -> None: ...
|
# Post methods
|
||||||
|
async def create_post(self, post: domain.Post) -> None: ...
|
||||||
|
|
||||||
|
async def get_post(self, post_id: UUID) -> domain.Post | None: ...
|
||||||
|
|
||||||
|
async def get_post_by_channel_and_message(self, channel_id: UUID, message_id: int) -> domain.Post | None: ...
|
||||||
|
|
||||||
|
# Post views history
|
||||||
|
async def create_post_views_history(self, history: domain.PostViewsHistory) -> None: ...
|
||||||
|
|
||||||
async def get_views_history(
|
async def get_views_history(
|
||||||
self,
|
self,
|
||||||
placement_id: UUID,
|
post_id: UUID,
|
||||||
*,
|
*,
|
||||||
from_date: datetime.datetime | None = None,
|
from_date: datetime.datetime | None = None,
|
||||||
to_date: datetime.datetime | None = None,
|
to_date: datetime.datetime | None = None,
|
||||||
) -> list[domain.PlacementViewsHistory]: ...
|
) -> list[domain.PostViewsHistory]: ...
|
||||||
|
|
||||||
|
async def get_latest_views_data(self, post_id: UUID) -> tuple[int, datetime.datetime] | None: ...
|
||||||
|
|
||||||
|
async def get_latest_views_data_batch(self, post_ids: list[UUID]) -> dict[UUID, tuple[int, datetime.datetime]]: ...
|
||||||
|
|
||||||
async def get_telegram_state(self, telegram_id: int) -> domain.TelegramState | None: ...
|
async def get_telegram_state(self, telegram_id: int) -> domain.TelegramState | None: ...
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ async def get_channel_analytics(self: 'Usecase', input: dto.GetChannelAnalyticsI
|
|||||||
else:
|
else:
|
||||||
allowed_project_filter = allowed_project_ids
|
allowed_project_filter = allowed_project_ids
|
||||||
if allowed_project_ids is None:
|
if allowed_project_ids is None:
|
||||||
channels = await self.database.get_workspace_channels(input.workspace_id)
|
# Получить каналы через Projects
|
||||||
|
projects = await self.database.get_workspace_projects(input.workspace_id)
|
||||||
|
channels = [p.channel for p in projects]
|
||||||
else:
|
else:
|
||||||
channels = []
|
channels = []
|
||||||
|
|
||||||
@@ -57,6 +59,10 @@ async def get_channel_analytics(self: 'Usecase', input: dto.GetChannelAnalyticsI
|
|||||||
|
|
||||||
channel_stats: dict[UUID, ChannelStats] = {ch.id: ChannelStats() for ch in channels}
|
channel_stats: dict[UUID, ChannelStats] = {ch.id: ChannelStats() for ch in channels}
|
||||||
|
|
||||||
|
# Batch fetch views data for all posts
|
||||||
|
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 {}
|
||||||
|
|
||||||
for p in placements:
|
for p in placements:
|
||||||
stats = channel_stats.get(p.placement_channel_id)
|
stats = channel_stats.get(p.placement_channel_id)
|
||||||
if stats is None:
|
if stats is None:
|
||||||
@@ -64,7 +70,12 @@ async def get_channel_analytics(self: 'Usecase', input: dto.GetChannelAnalyticsI
|
|||||||
|
|
||||||
stats.total_cost += p.cost if p.cost is not None else 0
|
stats.total_cost += p.cost if p.cost is not None else 0
|
||||||
stats.total_subscriptions += p.subscriptions_count
|
stats.total_subscriptions += p.subscriptions_count
|
||||||
stats.total_views += p.views_count if p.views_count is not None else 0
|
|
||||||
|
# Get views from batch data
|
||||||
|
if p.post and p.post.id in views_map:
|
||||||
|
views_count = views_map[p.post.id][0]
|
||||||
|
stats.total_views += views_count
|
||||||
|
|
||||||
stats.placements_count += 1
|
stats.placements_count += 1
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ async def get_creatives_analytics(
|
|||||||
|
|
||||||
creative_stats: dict[UUID, CreativeStats] = {cr.id: CreativeStats() for cr in creatives}
|
creative_stats: dict[UUID, CreativeStats] = {cr.id: CreativeStats() for cr in creatives}
|
||||||
|
|
||||||
|
# Batch fetch views data for all posts
|
||||||
|
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 {}
|
||||||
|
|
||||||
for p in placements:
|
for p in placements:
|
||||||
stats = creative_stats.get(p.creative_id)
|
stats = creative_stats.get(p.creative_id)
|
||||||
if stats is None:
|
if stats is None:
|
||||||
@@ -56,7 +60,12 @@ async def get_creatives_analytics(
|
|||||||
|
|
||||||
stats.total_cost += p.cost if p.cost is not None else 0
|
stats.total_cost += p.cost if p.cost is not None else 0
|
||||||
stats.total_subscriptions += p.subscriptions_count
|
stats.total_subscriptions += p.subscriptions_count
|
||||||
stats.total_views += p.views_count if p.views_count is not None else 0
|
|
||||||
|
# Get views from batch data
|
||||||
|
if p.post and p.post.id in views_map:
|
||||||
|
views_count = views_map[p.post.id][0]
|
||||||
|
stats.total_views += views_count
|
||||||
|
|
||||||
stats.placements_count += 1
|
stats.placements_count += 1
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ async def get_placements_analytics(
|
|||||||
allowed_project_ids=allowed_project_ids,
|
allowed_project_ids=allowed_project_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Batch fetch views data for all posts
|
||||||
|
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 {}
|
||||||
|
|
||||||
result = [
|
result = [
|
||||||
dto.PlacementAnalyticsOutput(
|
dto.PlacementAnalyticsOutput(
|
||||||
id=p.id,
|
id=p.id,
|
||||||
@@ -56,9 +60,9 @@ async def get_placements_analytics(
|
|||||||
placement_date=p.placement_date,
|
placement_date=p.placement_date,
|
||||||
cost=p.cost,
|
cost=p.cost,
|
||||||
subscriptions_count=p.subscriptions_count,
|
subscriptions_count=p.subscriptions_count,
|
||||||
views_count=p.views_count,
|
views_count=views_map[p.post.id][0] if p.post and p.post.id in views_map else None,
|
||||||
cpf=_calculate_cpf(p.cost, p.subscriptions_count),
|
cpf=_calculate_cpf(p.cost, p.subscriptions_count),
|
||||||
cpm=_calculate_cpm(p.cost, p.views_count),
|
cpm=_calculate_cpm(p.cost, views_map[p.post.id][0] if p.post and p.post.id in views_map else None),
|
||||||
)
|
)
|
||||||
for p in placements
|
for p in placements
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ async def get_spending_analytics(
|
|||||||
subscriptions: int = 0
|
subscriptions: int = 0
|
||||||
views: int = 0
|
views: int = 0
|
||||||
|
|
||||||
|
# Batch fetch views data for all posts
|
||||||
|
post_ids = [p.post.id for p in filtered if p.post]
|
||||||
|
views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {}
|
||||||
|
|
||||||
# Группировка по периодам
|
# Группировка по периодам
|
||||||
period_data: dict[str, PeriodData] = defaultdict(PeriodData)
|
period_data: dict[str, PeriodData] = defaultdict(PeriodData)
|
||||||
|
|
||||||
@@ -84,9 +88,11 @@ async def get_spending_analytics(
|
|||||||
total_subs += p.subscriptions_count
|
total_subs += p.subscriptions_count
|
||||||
pd.subscriptions += p.subscriptions_count
|
pd.subscriptions += p.subscriptions_count
|
||||||
|
|
||||||
if p.views_count is not None:
|
# Get views from batch data
|
||||||
total_views += p.views_count
|
if p.post and p.post.id in views_map:
|
||||||
pd.views += p.views_count
|
views_count = views_map[p.post.id][0]
|
||||||
|
total_views += views_count
|
||||||
|
pd.views += views_count
|
||||||
|
|
||||||
avg_cpf = total_cost / total_subs if total_subs > 0 and total_cost > 0 else None
|
avg_cpf = total_cost / total_subs if total_subs > 0 and total_cost > 0 else None
|
||||||
avg_cpm = (total_cost / total_views * 1000) if total_views > 0 and total_cost > 0 else None
|
avg_cpm = (total_cost / total_views * 1000) if total_views > 0 and total_cost > 0 else None
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ async def get_channels(self: 'Usecase', input: dto.GetChannelsInput) -> dto.GetC
|
|||||||
telegram_id=channel.telegram_id,
|
telegram_id=channel.telegram_id,
|
||||||
title=channel.title,
|
title=channel.title,
|
||||||
username=channel.username,
|
username=channel.username,
|
||||||
verification_status=channel.verification_status,
|
|
||||||
)
|
)
|
||||||
for channel in channels
|
for channel in channels
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ async def update_creative(
|
|||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
) -> dto.CreativeOutput:
|
) -> dto.CreativeOutput:
|
||||||
context = await self.ensure_workspace_permission(
|
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PROJECTS_WRITE)
|
||||||
workspace_id, user_id, domain.PermissionKey.PROJECTS_WRITE
|
|
||||||
)
|
|
||||||
|
|
||||||
creative = await self.database.get_creative(workspace_id, creative_id)
|
creative = await self.database.get_creative(workspace_id, creative_id)
|
||||||
if not creative:
|
if not creative:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ async def create_placement(
|
|||||||
if not project:
|
if not project:
|
||||||
raise domain.ProjectNotFound(input.project_id)
|
raise domain.ProjectNotFound(input.project_id)
|
||||||
|
|
||||||
placement_channel = await self.database.get_channel(workspace_id, channel_id=input.placement_channel_id)
|
placement_channel = await self.database.get_channel(channel_id=input.placement_channel_id)
|
||||||
if not placement_channel:
|
if not placement_channel:
|
||||||
raise domain.ChannelNotFound(input.placement_channel_id)
|
raise domain.ChannelNotFound(input.placement_channel_id)
|
||||||
|
|
||||||
@@ -39,6 +39,8 @@ async def create_placement(
|
|||||||
if project.channel.telegram_id is None:
|
if project.channel.telegram_id is None:
|
||||||
raise domain.ChannelNotFound(project.channel.id)
|
raise domain.ChannelNotFound(project.channel.id)
|
||||||
|
|
||||||
|
print(f'Телега id: {project.channel.telegram_id}')
|
||||||
|
|
||||||
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
|
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
|
||||||
|
|
||||||
placement = domain.Placement(
|
placement = domain.Placement(
|
||||||
@@ -71,13 +73,13 @@ async def create_placement(
|
|||||||
placement_date=placement.placement_date,
|
placement_date=placement.placement_date,
|
||||||
cost=placement.cost,
|
cost=placement.cost,
|
||||||
comment=placement.comment,
|
comment=placement.comment,
|
||||||
ad_post_url=placement.ad_post_url,
|
ad_post_url=None,
|
||||||
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=placement.status,
|
status=placement.status,
|
||||||
subscriptions_count=placement.subscriptions_count,
|
subscriptions_count=placement.subscriptions_count,
|
||||||
views_count=placement.views_count,
|
views_count=None,
|
||||||
views_availability=placement.views_availability,
|
views_availability=domain.PostViewsAvailability.UNKNOWN,
|
||||||
last_views_fetch_at=placement.last_views_fetch_at,
|
last_views_fetch_at=None,
|
||||||
created_at=placement.created_at,
|
created_at=placement.created_at,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
|
|
||||||
|
from .helpers import generate_post_url
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
|
|
||||||
@@ -21,6 +23,21 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl
|
|||||||
|
|
||||||
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
|
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id)
|
||||||
|
|
||||||
|
# Compute post-related fields
|
||||||
|
ad_post_url = None
|
||||||
|
views_count = None
|
||||||
|
views_availability = domain.PostViewsAvailability.UNKNOWN
|
||||||
|
last_views_fetch_at = None
|
||||||
|
|
||||||
|
if placement.post:
|
||||||
|
ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id)
|
||||||
|
|
||||||
|
# Get latest views data
|
||||||
|
views_data = await self.database.get_latest_views_data(placement.post.id)
|
||||||
|
if views_data:
|
||||||
|
views_count, last_views_fetch_at = views_data
|
||||||
|
views_availability = domain.PostViewsAvailability.AVAILABLE
|
||||||
|
|
||||||
return dto.PlacementOutput(
|
return dto.PlacementOutput(
|
||||||
id=placement.id,
|
id=placement.id,
|
||||||
project_id=placement.project_id,
|
project_id=placement.project_id,
|
||||||
@@ -32,13 +49,13 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl
|
|||||||
placement_date=placement.placement_date,
|
placement_date=placement.placement_date,
|
||||||
cost=placement.cost,
|
cost=placement.cost,
|
||||||
comment=placement.comment,
|
comment=placement.comment,
|
||||||
ad_post_url=placement.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=placement.status,
|
status=placement.status,
|
||||||
subscriptions_count=placement.subscriptions_count,
|
subscriptions_count=placement.subscriptions_count,
|
||||||
views_count=placement.views_count,
|
views_count=views_count,
|
||||||
views_availability=placement.views_availability,
|
views_availability=views_availability,
|
||||||
last_views_fetch_at=placement.last_views_fetch_at,
|
last_views_fetch_at=last_views_fetch_at,
|
||||||
created_at=placement.created_at,
|
created_at=placement.created_at,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
|
|
||||||
|
from .helpers import generate_post_url
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
|
|
||||||
@@ -26,8 +28,27 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
|
|||||||
allowed_project_ids=allowed_project_ids,
|
allowed_project_ids=allowed_project_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
return dto.GetPlacementsOutput(
|
# Batch fetch views data for all posts
|
||||||
placements=[
|
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 {}
|
||||||
|
|
||||||
|
# Build output DTOs
|
||||||
|
placement_outputs = []
|
||||||
|
for placement in placements:
|
||||||
|
# Compute post-related fields
|
||||||
|
ad_post_url = None
|
||||||
|
views_count = None
|
||||||
|
views_availability = domain.PostViewsAvailability.UNKNOWN
|
||||||
|
last_views_fetch_at = None
|
||||||
|
|
||||||
|
if placement.post:
|
||||||
|
ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id)
|
||||||
|
views_data = views_map.get(placement.post.id)
|
||||||
|
if views_data:
|
||||||
|
views_count, last_views_fetch_at = views_data
|
||||||
|
views_availability = domain.PostViewsAvailability.AVAILABLE
|
||||||
|
|
||||||
|
placement_outputs.append(
|
||||||
dto.PlacementOutput(
|
dto.PlacementOutput(
|
||||||
id=placement.id,
|
id=placement.id,
|
||||||
project_id=placement.project_id,
|
project_id=placement.project_id,
|
||||||
@@ -39,16 +60,16 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.
|
|||||||
placement_date=placement.placement_date,
|
placement_date=placement.placement_date,
|
||||||
cost=placement.cost,
|
cost=placement.cost,
|
||||||
comment=placement.comment,
|
comment=placement.comment,
|
||||||
ad_post_url=placement.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=placement.status,
|
status=placement.status,
|
||||||
subscriptions_count=placement.subscriptions_count,
|
subscriptions_count=placement.subscriptions_count,
|
||||||
views_count=placement.views_count,
|
views_count=views_count,
|
||||||
views_availability=placement.views_availability,
|
views_availability=views_availability,
|
||||||
last_views_fetch_at=placement.last_views_fetch_at,
|
last_views_fetch_at=last_views_fetch_at,
|
||||||
created_at=placement.created_at,
|
created_at=placement.created_at,
|
||||||
)
|
)
|
||||||
for placement in placements
|
)
|
||||||
]
|
|
||||||
)
|
return dto.GetPlacementsOutput(placements=placement_outputs)
|
||||||
|
|||||||
7
src/usecase/placement/helpers.py
Normal file
7
src/usecase/placement/helpers.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
def generate_post_url(channel_username: str | None, message_id: int) -> str | None:
|
||||||
|
"""Generate Telegram post URL from channel username and message ID."""
|
||||||
|
if not channel_username:
|
||||||
|
return None
|
||||||
|
# Remove @ prefix if present
|
||||||
|
username = channel_username.lstrip('@')
|
||||||
|
return f'https://t.me/{username}/{message_id}'
|
||||||
@@ -4,6 +4,8 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from src import domain, dto
|
from src import domain, dto
|
||||||
|
|
||||||
|
from .helpers import generate_post_url
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .. import Usecase
|
from .. import Usecase
|
||||||
|
|
||||||
@@ -17,9 +19,7 @@ async def update_placement(
|
|||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
) -> dto.PlacementOutput:
|
) -> dto.PlacementOutput:
|
||||||
context = await self.ensure_workspace_permission(
|
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||||
workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE
|
|
||||||
)
|
|
||||||
|
|
||||||
placement = await self.database.get_placement(workspace_id, placement_id)
|
placement = await self.database.get_placement(workspace_id, placement_id)
|
||||||
if not placement:
|
if not placement:
|
||||||
@@ -39,6 +39,21 @@ async def update_placement(
|
|||||||
|
|
||||||
await self.database.update_placement(placement)
|
await self.database.update_placement(placement)
|
||||||
|
|
||||||
|
# Compute post-related fields
|
||||||
|
ad_post_url = None
|
||||||
|
views_count = None
|
||||||
|
views_availability = domain.PostViewsAvailability.UNKNOWN
|
||||||
|
last_views_fetch_at = None
|
||||||
|
|
||||||
|
if placement.post:
|
||||||
|
ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id)
|
||||||
|
|
||||||
|
# Get latest views data
|
||||||
|
views_data = await self.database.get_latest_views_data(placement.post.id)
|
||||||
|
if views_data:
|
||||||
|
views_count, last_views_fetch_at = views_data
|
||||||
|
views_availability = domain.PostViewsAvailability.AVAILABLE
|
||||||
|
|
||||||
return dto.PlacementOutput(
|
return dto.PlacementOutput(
|
||||||
id=placement.id,
|
id=placement.id,
|
||||||
project_id=placement.project_id,
|
project_id=placement.project_id,
|
||||||
@@ -50,13 +65,13 @@ async def update_placement(
|
|||||||
placement_date=placement.placement_date,
|
placement_date=placement.placement_date,
|
||||||
cost=placement.cost,
|
cost=placement.cost,
|
||||||
comment=placement.comment,
|
comment=placement.comment,
|
||||||
ad_post_url=placement.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=placement.status,
|
status=placement.status,
|
||||||
subscriptions_count=placement.subscriptions_count,
|
subscriptions_count=placement.subscriptions_count,
|
||||||
views_count=placement.views_count,
|
views_count=views_count,
|
||||||
views_availability=placement.views_availability,
|
views_availability=views_availability,
|
||||||
last_views_fetch_at=placement.last_views_fetch_at,
|
last_views_fetch_at=last_views_fetch_at,
|
||||||
created_at=placement.created_at,
|
created_at=placement.created_at,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ async def get_workspace_projects(
|
|||||||
|
|
||||||
allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.PROJECTS_READ)
|
allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.PROJECTS_READ)
|
||||||
|
|
||||||
projects = await self.database.get_workspace_projects(
|
projects = await self.database.get_workspace_projects(input.workspace_id, allowed_project_ids=allowed_project_ids)
|
||||||
input.workspace_id, allowed_project_ids=allowed_project_ids
|
|
||||||
)
|
|
||||||
|
|
||||||
return dto.GetWorkspaceProjectsOutput(
|
return dto.GetWorkspaceProjectsOutput(
|
||||||
projects=[
|
projects=[
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ async def tg_add_project_1(self: 'Usecase', input: dto.ConnectProjectInput) -> d
|
|||||||
if len(workspaces) == 1:
|
if len(workspaces) == 1:
|
||||||
workspace = workspaces[0]
|
workspace = workspaces[0]
|
||||||
|
|
||||||
channel = await self.database.get_channel(workspace.id, telegram_id=input.telegram_id)
|
channel = await self.database.get_channel(telegram_id=input.telegram_id)
|
||||||
if channel:
|
if channel:
|
||||||
channel.title = input.title
|
channel.title = input.title
|
||||||
channel.username = input.username
|
channel.username = input.username
|
||||||
@@ -80,8 +80,6 @@ async def tg_add_project_1(self: 'Usecase', input: dto.ConnectProjectInput) -> d
|
|||||||
telegram_id=input.telegram_id,
|
telegram_id=input.telegram_id,
|
||||||
title=input.title,
|
title=input.title,
|
||||||
username=input.username,
|
username=input.username,
|
||||||
verification_status=domain.ChannelVerificationStatus.VERIFIED,
|
|
||||||
workspace_id=workspace.id,
|
|
||||||
)
|
)
|
||||||
await self.database.create_channel(channel)
|
await self.database.create_channel(channel)
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ async def tg_add_project_2(
|
|||||||
return
|
return
|
||||||
|
|
||||||
telegram_channel_id = int(channel_data['telegram_id'])
|
telegram_channel_id = int(channel_data['telegram_id'])
|
||||||
channel = await self.database.get_channel(workspace.id, telegram_id=telegram_channel_id)
|
channel = await self.database.get_channel(telegram_id=telegram_channel_id)
|
||||||
if channel:
|
if channel:
|
||||||
channel.title = str(channel_data['title'])
|
channel.title = str(channel_data['title'])
|
||||||
channel.username = channel_data.get('username')
|
channel.username = channel_data.get('username')
|
||||||
@@ -76,7 +76,6 @@ async def tg_add_project_2(
|
|||||||
telegram_id=telegram_channel_id,
|
telegram_id=telegram_channel_id,
|
||||||
title=str(channel_data['title']),
|
title=str(channel_data['title']),
|
||||||
username=channel_data.get('username'),
|
username=channel_data.get('username'),
|
||||||
workspace_id=workspace.id,
|
|
||||||
)
|
)
|
||||||
await self.database.create_channel(channel)
|
await self.database.create_channel(channel)
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ async def attach_channel_to_purchase_plan(
|
|||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
input: dto.AttachChannelToPurchasePlanInput,
|
input: dto.AttachChannelToPurchasePlanInput,
|
||||||
) -> dto.PurchasePlanChannelOutput:
|
) -> dto.PurchasePlanChannelOutput:
|
||||||
context = await self.ensure_workspace_permission(
|
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||||
workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE
|
|
||||||
)
|
|
||||||
|
|
||||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
if not project:
|
if not project:
|
||||||
@@ -30,21 +28,15 @@ async def attach_channel_to_purchase_plan(
|
|||||||
plan = await self.ensure_active_purchase_plan(project)
|
plan = await self.ensure_active_purchase_plan(project)
|
||||||
|
|
||||||
# Поиск канала по username
|
# Поиск канала по username
|
||||||
channel = await self.database.get_channel(workspace_id, username=input.username)
|
channel = await self.database.get_channel(username=input.username)
|
||||||
|
|
||||||
if not channel:
|
if not channel:
|
||||||
# Создать неподтвержденный канал
|
# Создать канал с nullable telegram_id/title (заполнятся парсером позже)
|
||||||
channel = domain.Channel(
|
channel = domain.Channel(username=input.username)
|
||||||
workspace_id=workspace_id,
|
|
||||||
username=input.username,
|
|
||||||
telegram_id=None,
|
|
||||||
title=None,
|
|
||||||
verification_status=domain.ChannelVerificationStatus.UNVERIFIED,
|
|
||||||
)
|
|
||||||
await self.database.create_channel(channel)
|
await self.database.create_channel(channel)
|
||||||
log.info('Created unverified channel with username %s', input.username)
|
log.info('Created channel with username @%s (pending parser update)', input.username)
|
||||||
else:
|
else:
|
||||||
log.info('Found existing channel %s for username %s', channel.id, input.username)
|
log.info('Found existing channel %s for username @%s', channel.id, input.username)
|
||||||
|
|
||||||
plan_channel = await self.database.add_channel_to_purchase_plan(
|
plan_channel = await self.database.add_channel_to_purchase_plan(
|
||||||
plan.id,
|
plan.id,
|
||||||
@@ -66,6 +58,5 @@ async def attach_channel_to_purchase_plan(
|
|||||||
telegram_id=channel.telegram_id,
|
telegram_id=channel.telegram_id,
|
||||||
title=channel.title,
|
title=channel.title,
|
||||||
username=channel.username,
|
username=channel.username,
|
||||||
verification_status=channel.verification_status,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ async def get_purchase_plan_channels(
|
|||||||
telegram_id=plan_channel.channel.telegram_id,
|
telegram_id=plan_channel.channel.telegram_id,
|
||||||
title=plan_channel.channel.title,
|
title=plan_channel.channel.title,
|
||||||
username=plan_channel.channel.username,
|
username=plan_channel.channel.username,
|
||||||
verification_status=plan_channel.channel.verification_status,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for plan_channel in plan_channels
|
for plan_channel in plan_channels
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ async def remove_purchase_plan_channel(
|
|||||||
workspace_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
) -> None:
|
) -> None:
|
||||||
context = await self.ensure_workspace_permission(
|
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||||
workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE
|
|
||||||
)
|
|
||||||
|
|
||||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
if not project:
|
if not project:
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ async def update_purchase_plan_channel(
|
|||||||
user_id: uuid.UUID,
|
user_id: uuid.UUID,
|
||||||
input: dto.UpdatePurchasePlanChannelInput,
|
input: dto.UpdatePurchasePlanChannelInput,
|
||||||
) -> dto.PurchasePlanChannelOutput:
|
) -> dto.PurchasePlanChannelOutput:
|
||||||
context = await self.ensure_workspace_permission(
|
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||||
workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE
|
|
||||||
)
|
|
||||||
|
|
||||||
project = await self.database.get_project(workspace_id, project_id=project_id)
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
if not project:
|
if not project:
|
||||||
@@ -56,7 +54,7 @@ async def update_purchase_plan_channel(
|
|||||||
|
|
||||||
channel: domain.Channel | None = plan_channel.channel
|
channel: domain.Channel | None = plan_channel.channel
|
||||||
if channel is None:
|
if channel is None:
|
||||||
channel = await self.database.get_channel(workspace_id, channel_id=plan_channel.channel_id)
|
channel = await self.database.get_channel(channel_id=plan_channel.channel_id)
|
||||||
if channel is None:
|
if channel is None:
|
||||||
raise domain.ChannelNotFound(plan_channel.channel_id)
|
raise domain.ChannelNotFound(plan_channel.channel_id)
|
||||||
|
|
||||||
@@ -70,6 +68,5 @@ async def update_purchase_plan_channel(
|
|||||||
telegram_id=channel.telegram_id,
|
telegram_id=channel.telegram_id,
|
||||||
title=channel.title,
|
title=channel.title,
|
||||||
username=channel.username,
|
username=channel.username,
|
||||||
verification_status=channel.verification_status,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,17 +21,20 @@ async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) ->
|
|||||||
|
|
||||||
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.post:
|
||||||
|
return dto.GetViewsHistoryOutput(histories=[])
|
||||||
|
|
||||||
histories = await self.database.get_views_history(
|
histories = await self.database.get_views_history(
|
||||||
input.placement_id,
|
placement.post.id,
|
||||||
from_date=input.from_date,
|
from_date=input.from_date,
|
||||||
to_date=input.to_date,
|
to_date=input.to_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
return dto.GetViewsHistoryOutput(
|
return dto.GetViewsHistoryOutput(
|
||||||
histories=[
|
histories=[
|
||||||
dto.PlacementViewsHistoryOutput(
|
dto.PostViewsHistoryOutput(
|
||||||
id=history.id,
|
id=history.id,
|
||||||
placement_id=history.placement_id,
|
post_id=history.post_id,
|
||||||
views_count=history.views_count,
|
views_count=history.views_count,
|
||||||
fetched_at=history.fetched_at,
|
fetched_at=history.fetched_at,
|
||||||
created_at=history.created_at,
|
created_at=history.created_at,
|
||||||
|
|||||||
@@ -17,6 +17,4 @@ async def get_workspace_invites(
|
|||||||
|
|
||||||
invites = await self.database.get_workspace_invites(workspace_id)
|
invites = await self.database.get_workspace_invites(workspace_id)
|
||||||
|
|
||||||
return dto.GetWorkspaceInvitesOutput(
|
return dto.GetWorkspaceInvitesOutput(invites=[workspace_invite_to_dto(invite) for invite in invites])
|
||||||
invites=[workspace_invite_to_dto(invite) for invite in invites]
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -17,6 +17,4 @@ async def get_workspace_members(
|
|||||||
|
|
||||||
members = await self.database.get_workspace_members(workspace_id)
|
members = await self.database.get_workspace_members(workspace_id)
|
||||||
|
|
||||||
return dto.GetWorkspaceMembersOutput(
|
return dto.GetWorkspaceMembersOutput(members=[workspace_member_to_dto(member) for member in members])
|
||||||
members=[workspace_member_to_dto(member) for member in members]
|
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user