feat: add workspace
This commit is contained in:
124
migrations/models/2_20251213220918_update.py
Normal file
124
migrations/models/2_20251213220918_update.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "target_channel" DROP CONSTRAINT IF EXISTS "fk_target_c_user_f3145593";
|
||||||
|
ALTER TABLE "placement" DROP CONSTRAINT IF EXISTS "fk_placemen_user_004a0a62";
|
||||||
|
DROP INDEX IF EXISTS "uid_external_ch_user_id_289e6a";
|
||||||
|
ALTER TABLE "external_channel" DROP CONSTRAINT IF EXISTS "fk_external_user_23b1e9dc";
|
||||||
|
ALTER TABLE "creative" DROP CONSTRAINT IF EXISTS "fk_creative_user_7868b532";
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace" (
|
||||||
|
"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,
|
||||||
|
"name" VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS "workspace_user" (
|
||||||
|
"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,
|
||||||
|
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE,
|
||||||
|
"workspace_id" UUID NOT NULL REFERENCES "workspace" ("id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "uid_workspace_u_workspa_13c46c" UNIQUE ("workspace_id", "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");
|
||||||
|
ALTER TABLE "creative" RENAME COLUMN "user_id" TO "workspace_id";
|
||||||
|
ALTER TABLE "external_channel" RENAME COLUMN "user_id" TO "workspace_id";
|
||||||
|
ALTER TABLE "placement" RENAME COLUMN "user_id" TO "workspace_id";
|
||||||
|
ALTER TABLE "target_channel" RENAME COLUMN "user_id" TO "workspace_id";
|
||||||
|
ALTER TABLE "telegram_state" ALTER COLUMN "state" TYPE VARCHAR(26) USING "state"::VARCHAR(26);
|
||||||
|
COMMENT ON COLUMN "telegram_state"."state" IS 'CREATIVE_WAITING_WORKSPACE: creative_waiting_workspace
|
||||||
|
CREATIVE_WAITING_CHANNEL: creative_waiting_channel
|
||||||
|
CREATIVE_WAITING_NAME: creative_waiting_name
|
||||||
|
CREATIVE_WAITING_TEXT: creative_waiting_text';
|
||||||
|
ALTER TABLE "creative" ADD CONSTRAINT "fk_creative_workspac_ac3ef81f" FOREIGN KEY ("workspace_id") REFERENCES "workspace" ("id") ON DELETE CASCADE;
|
||||||
|
ALTER TABLE "external_channel" ADD CONSTRAINT "fk_external_workspac_5266308f" FOREIGN KEY ("workspace_id") REFERENCES "workspace" ("id") ON DELETE CASCADE;
|
||||||
|
ALTER TABLE "placement" ADD CONSTRAINT "fk_placemen_workspac_c3bc4808" FOREIGN KEY ("workspace_id") REFERENCES "workspace" ("id") ON DELETE CASCADE;
|
||||||
|
ALTER TABLE "target_channel" ADD CONSTRAINT "fk_target_c_workspac_8a2fd2e3" FOREIGN KEY ("workspace_id") REFERENCES "workspace" ("id") ON DELETE CASCADE;"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "external_channel" DROP CONSTRAINT IF EXISTS "fk_external_workspac_5266308f";
|
||||||
|
ALTER TABLE "target_channel" DROP CONSTRAINT IF EXISTS "fk_target_c_workspac_8a2fd2e3";
|
||||||
|
ALTER TABLE "placement" DROP CONSTRAINT IF EXISTS "fk_placemen_workspac_c3bc4808";
|
||||||
|
ALTER TABLE "creative" DROP CONSTRAINT IF EXISTS "fk_creative_workspac_ac3ef81f";
|
||||||
|
ALTER TABLE "creative" RENAME COLUMN "workspace_id" TO "user_id";
|
||||||
|
ALTER TABLE "placement" RENAME COLUMN "workspace_id" TO "user_id";
|
||||||
|
ALTER TABLE "target_channel" RENAME COLUMN "workspace_id" TO "user_id";
|
||||||
|
COMMENT ON COLUMN "telegram_state"."state" IS 'CREATIVE_WAITING_CHANNEL: creative_waiting_channel
|
||||||
|
CREATIVE_WAITING_NAME: creative_waiting_name
|
||||||
|
CREATIVE_WAITING_TEXT: creative_waiting_text';
|
||||||
|
ALTER TABLE "telegram_state" ALTER COLUMN "state" TYPE VARCHAR(24) USING "state"::VARCHAR(24);
|
||||||
|
ALTER TABLE "external_channel" RENAME COLUMN "workspace_id" TO "user_id";
|
||||||
|
DROP TABLE IF EXISTS "workspace_user";
|
||||||
|
DROP TABLE IF EXISTS "workspace";
|
||||||
|
ALTER TABLE "creative" ADD CONSTRAINT "fk_creative_user_7868b532" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE;
|
||||||
|
ALTER TABLE "placement" ADD CONSTRAINT "fk_placemen_user_004a0a62" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE;
|
||||||
|
ALTER TABLE "target_channel" ADD CONSTRAINT "fk_target_c_user_f3145593" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE;
|
||||||
|
ALTER TABLE "external_channel" ADD CONSTRAINT "fk_external_user_23b1e9dc" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "uid_external_ch_user_id_289e6a" ON "external_channel" ("user_id", "telegram_id");"""
|
||||||
|
|
||||||
|
|
||||||
|
MODELS_STATE = (
|
||||||
|
"eJztXWtv4jgU/SuIT7NSd9TS51arlVLKzLBDoSrQrnZmFLngQtTgsHn0odH897VDno4DJC"
|
||||||
|
"SQhPtlHo5vHufa5p7ja/tnfaaNsWp8bOoYmcoLrl/WftYJmrF/RK4d1OpoPvevsAITPap2"
|
||||||
|
"5VGw1qNh6mhk0vInpBqYFo2xMdKVualohJYSS1VZoTaiFRUy8YssovxnYdnUJticYp1e+P"
|
||||||
|
"aDFitkjN+w4f53/iw/KVgdh15XGbNn2+Wy+T63y4bD9vUnuyZ73KM80lRrRvza83dzqhGv"
|
||||||
|
"umUp44/Mhl2bYIJ1ZOJx4DPYWzpf7BYt3pgWmLqFvVcd+wVj/IQslYFR//PJIiOGQc1+Ev"
|
||||||
|
"vj5K96AnhGGmHQKsRkWPz8tfgq/5vt0jp7VPOLdPfh+Ow3+ys1w5zo9kUbkfov2xCZaGFq"
|
||||||
|
"4+oDabsSj2VkRgG9pldMZYbFoIYtOXDHjulH9x9pQHYLfJT9FubC7MKXDtM6/YZxj6jvjg"
|
||||||
|
"eXYDxo37T6A+nmln3JzDD+U22IpEGLXWnYpe9c6YeFSzTaPxYdx7tJ7aE9+FJj/6392+u2"
|
||||||
|
"eMd59Qb/1tk7IcvUZKK9ymgcaGxuqQsMrek71pqPUzo2bAmO3aljnZf3/UoHY5zOr2HLDP"
|
||||||
|
"zqvO0W3VoSN7qfvbSD2n9HXNicIl3sPrc+5zgKT0G74Ay9ySomE3NK/9s4PV3ivHvpzv4N"
|
||||||
|
"o7U4j3SdS43FtV8hCE38JugFA1oqhtCtXxYIlzX31j+DUEt3gfpwI/3zW6i1d3rdz271AL"
|
||||||
|
"DNTu+Kw9MwkWkZ4kbZItbMRrVNXxGREY6g61tvD986jTydQDSMcV1qDtr3rcvaosJ3wtCh"
|
||||||
|
"Bde0RB9NadGieSRsxhdrNOKL2CZ8wTfguYpGeIbpoyiOFhE05jaJacsiUw53ZVGaB+6HG4"
|
||||||
|
"wLE/aQ3xtHJ+cnF8dnJxe0iv0iXsn5EpDb3QE/CCCd8gd5NEWEYFVOxgyExlkShSiYEaaQ"
|
||||||
|
"2xixkhb4IL5q+rMxpy0qIX683Z5Ax3jp07OQUHmIRGH8pOlYmZCv+D0ykHK4OWz8IXivos"
|
||||||
|
"Lnl/qP0NGrR9ojbYR+5yIUtYGW+k3pulVf0qUzAHJg37Dp36+0YAoHLDGirJE+otHzK9LH"
|
||||||
|
"ckxr9X9FoihfObafvt5hFdkfFAvwrXufQgdSEXRtjLSGFsAmhFr00qwx40sQQRP7rdmz2Z"
|
||||||
|
"McTFpvJtYJUt1mJ5Db+CoHy1Q37FQO9gtQ30B9i4ILIg2ob+BYUN9AfYvtoCZ1x0RHMyHf"
|
||||||
|
"uVImsbybM8yLcmccXjqk+49G4/j4vHF4fHZxenJ+fnpx6LHv6KVlNPyq/Zkx8ZD7BNRcMd"
|
||||||
|
"VEGqdnUBaFbgsip2WwqC+ZVhy0SQXl6sEl6waaP5DBN4tgGS8ac2Y5wVl66dh6ZO/5iPXk"
|
||||||
|
"UqbQNtXAun2cs1YzQYgDIW63QhzIRrnLRnFKpwDNG0TeBxr7c1tS52pxJbfWan+CzGlkkQ"
|
||||||
|
"/SWbOiTMqtx+tiCxQ13XbFM36P4uy0f89fTiX+Rk41c6pr1mQauI1IiRN2JlouB/H3Gk+s"
|
||||||
|
"aNjRJgoZaM+Y1AV6YeDqwTKpUGX1ZNOrCCohqIQgJoFKCI4FlRBUwgQqofsLuraA5RpkI2"
|
||||||
|
"DlHYttQXXBb3OFeiRFXwhblnOMq1JnsIx0P1UGjGcFcqGeUFgKmOSrKW3Rb+lFJYZGBnrS"
|
||||||
|
"0LlNYVFbqSUFmkVSGSnPnBtfUBKw55DaFE+e56FqQJ2BOpc1+qgMwwLqXFHHAnWuaqjp/Y"
|
||||||
|
"rKDPKkzoxal7OjVsmjI3o/QeyraihmXt814Fz3xCzK1v+ue8OrTqt2e9dqtvvtXjfsHvsi"
|
||||||
|
"K6IFyiIQvmtJHU4KGWkzN6xcN/kkYAKJJ8LEEzSWWUOXLV2wUCReqOPMSgJuWK87PWqsod"
|
||||||
|
"fRWrF6nX0tjKdCXmgLllWFPC/gEIK6ejWo6D47zuqr39Ju2m5e1ubWo6qMvhPp9vaudy91"
|
||||||
|
"LmuUDeraC7Lbwo7XhAaAS9KgObNyJlDm0qIj0/ozbBhoIs6wik1SW3GXPU1Xq9KK8dtW97"
|
||||||
|
"rd/UyHB0zGFEQ6PnBryK9bndaALSF3uEZBV5U7+ZT2h6XOxuSt93Jt+YuCX5MjyFnt6dCw"
|
||||||
|
"QAG9IIV+k0LD4ve0w4T4TlscMizyTEkREYwZw+7Xbu+he1lzqtAR4V5qdyRKB+iQsHhjlY"
|
||||||
|
"4cw26g3CKBKzdSd8gikBkiVrr44+hojeHj6Ch2/GCXwq5TEY2MF6g/YXM0TaHGxNwCZJld"
|
||||||
|
"k3hn676Es4Cc2Z5kly+NIpMBGGO+h0DCfi2wX0uBZvRhr5Es9hpZNlJmgKlgn4zSohrzS7"
|
||||||
|
"Aa1+CuuxviGdzmt7RAcjHJagBhRRgPYXYrwhah/lQxTE1XcFbLwu7ZXb/YN30vpn4Zu0Qs"
|
||||||
|
"VpDZDJp+4FYlQ2QreV+hFrMsB4xvWmvkg8nBRr4wguwwyA4ra25CZZKIIDusoo6F7LCqyp"
|
||||||
|
"C7m1jZRX/MeGbFFtRT9YKw5XZGt+2JQKXrBX5kmSyk4+1AxQut2diQ2me8KcmOqD3fRoq0"
|
||||||
|
"SqfvbfhUF1C0wNWDZbzMCNcDKgZUDCJ2oGLgWKBiQMXKuhNu7jte7GQj3ILu4ZpvMncu24"
|
||||||
|
"k8KbphykmxDFsBmqFEv6Rghoz2G8sII11nqhRmA/OeDQzBEs8wPdhWcky/JrBMYJlARoBl"
|
||||||
|
"gmOBZQLLXJ9lFntVa67bKuayqLVKCy/5ZZbDbn941W/eta/YwkqLeEJ/qsWVa6G/BPwo9s"
|
||||||
|
"E3SvOLEzWH4QlmYss0Eys++SUhdhHDPQQPFkNBLsDG8GWUCyDu1BmAGJ7QLy2KkfEKDrLe"
|
||||||
|
"zuKy3SSphKEWqIgRX8TLiNF2AEIiCInZBP6gN4GQCI4FIXFPmDqkq8C5zYXNByhBzk/5Th"
|
||||||
|
"CqstTd7rolCnHLCrmnIGw4k5U8BbtQHOS2C4W7JciGaVXZbo+ytZQq4YTGnp7QDNtv5HZm"
|
||||||
|
"tfi85RCkiU+tzmCDp6KdWy34JP7katHp3+Fzq2M2i+JPrhaKqRmeW72gYcvlUode9Wm8he"
|
||||||
|
"siuTRU4WCpXOpSNcOrC3IpyKWgqoFcCo4FuRTkUpBLiySXelFKWn2qCNJpvXnXkpgUJT9I"
|
||||||
|
"7UG7+1l+6N197d9KzdZlzdtq8xUpJnWA7FH27yRiR+OEbrfVEVg5YafApivdiB7DEBbUZi"
|
||||||
|
"dDCWqbNL5NI5U1ztYRLfleHNAsz3i1jD7OfZlwm/i73+vGBDq+CdcShoR66NtYGZkHNVUx"
|
||||||
|
"zB+5yZZ+MPloKSrF1PjIHptTPMmwCA2jkePA+JO/uPGR3eAq2Sq8PBNG7EOdBcTHPew5nu"
|
||||||
|
"+4p0oDywGWA8HwroNhYDkVdSywHGA5wHLSspyC5jOUJDMk1U4RqjZRKGLaM9503qrD7jRg"
|
||||||
|
"Nyrmb8xa83j+PDFrVxsi4k2du+F5iUDJk8X4KQUCKhPKN4jnM6EUByA1QGog9t117Aukpq"
|
||||||
|
"KOBVJTVVKTNNjeKNCuTg52qkgbEgcTZHclQSSXk/sgo3LHWIgS11IDksOaZqCnlaencbNt"
|
||||||
|
"EeDWoKlyPhNw3yKZ7ew57J8/gMamCeGAxu472wEaW1HHAo2tKo11f/MS/I4FTPZkvSAstU"
|
||||||
|
"wNHSy1zHypJd99M4AvOxqzI+QCY1KRdqmSsK6MpnUBEXKuHCxjQMivU5hZuthcjXVTNBzv"
|
||||||
|
"FSBDY8MTP+PZzAvWDUc+WFcdDpjsuUAcHN1Y10gAolO9nAAeHR6uASCtFb8VMrsmyHQXbb"
|
||||||
|
"W5ItNdeILv1jLdc4tVqpHT/ut/5sP+/g=="
|
||||||
|
)
|
||||||
@@ -19,6 +19,55 @@ class Postgres(DatabaseBase):
|
|||||||
async def create_user(self, user: domain.User) -> None:
|
async def create_user(self, user: domain.User) -> None:
|
||||||
await user.save()
|
await user.save()
|
||||||
|
|
||||||
|
async def create_workspace(self, workspace: domain.Workspace) -> None:
|
||||||
|
await workspace.save()
|
||||||
|
|
||||||
|
async def update_workspace(self, workspace: domain.Workspace) -> None:
|
||||||
|
await workspace.save()
|
||||||
|
|
||||||
|
async def delete_workspace(self, workspace_id: uuid.UUID) -> None:
|
||||||
|
await domain.Workspace.filter(id=workspace_id).delete()
|
||||||
|
|
||||||
|
async def add_user_to_workspace(
|
||||||
|
self, workspace_id: uuid.UUID, user_id: uuid.UUID, *, is_owner: bool = False
|
||||||
|
) -> None:
|
||||||
|
await domain.WorkspaceUser.create(workspace_id=workspace_id, user_id=user_id, is_owner=is_owner)
|
||||||
|
|
||||||
|
async def get_user_workspaces(self, user_id: uuid.UUID) -> list[domain.WorkspaceUser]:
|
||||||
|
return (
|
||||||
|
await domain.WorkspaceUser.filter(user_id=user_id)
|
||||||
|
.prefetch_related('workspace')
|
||||||
|
.order_by('created_at')
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_workspace(self, workspace_id: uuid.UUID) -> domain.Workspace | None:
|
||||||
|
return await domain.Workspace.get_or_none(id=workspace_id)
|
||||||
|
|
||||||
|
async def get_workspace_for_user(self, workspace_id: uuid.UUID, user_id: uuid.UUID) -> domain.Workspace | None:
|
||||||
|
membership = (
|
||||||
|
await domain.WorkspaceUser.filter(workspace_id=workspace_id, user_id=user_id)
|
||||||
|
.prefetch_related('workspace')
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return membership.workspace if membership else None
|
||||||
|
|
||||||
|
async def get_default_workspace_for_user(self, user_id: uuid.UUID) -> domain.Workspace | None:
|
||||||
|
membership = (
|
||||||
|
await domain.WorkspaceUser.filter(user_id=user_id)
|
||||||
|
.prefetch_related('workspace')
|
||||||
|
.order_by('created_at')
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return membership.workspace if membership else None
|
||||||
|
|
||||||
|
async def get_workspace_membership(
|
||||||
|
self, workspace_id: uuid.UUID, user_id: uuid.UUID
|
||||||
|
) -> domain.WorkspaceUser | None:
|
||||||
|
return await domain.WorkspaceUser.get_or_none(workspace_id=workspace_id, user_id=user_id).prefetch_related(
|
||||||
|
'workspace'
|
||||||
|
)
|
||||||
|
|
||||||
async def create_login_token(self, login_token: domain.LoginToken) -> None:
|
async def create_login_token(self, login_token: domain.LoginToken) -> None:
|
||||||
await login_token.save()
|
await login_token.save()
|
||||||
|
|
||||||
@@ -46,12 +95,12 @@ class Postgres(DatabaseBase):
|
|||||||
raise domain.LoginTokenAlreadyUsed() # либо нет токена, либо он уже использован
|
raise domain.LoginTokenAlreadyUsed() # либо нет токена, либо он уже использован
|
||||||
|
|
||||||
async def get_target_channel(
|
async def get_target_channel(
|
||||||
self, user_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
self, workspace_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
||||||
) -> domain.TargetChannel | None:
|
) -> domain.TargetChannel | None:
|
||||||
if not channel_id and not telegram_id:
|
if not channel_id and not telegram_id:
|
||||||
raise ValueError('Either channel_id or telegram_id must be provided')
|
raise ValueError('Either channel_id or telegram_id must be provided')
|
||||||
|
|
||||||
q = domain.TargetChannel.filter(user_id=user_id)
|
q = domain.TargetChannel.filter(workspace_id=workspace_id)
|
||||||
if channel_id:
|
if channel_id:
|
||||||
q = q.filter(id=channel_id)
|
q = q.filter(id=channel_id)
|
||||||
if telegram_id:
|
if telegram_id:
|
||||||
@@ -59,8 +108,17 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
return await q.first()
|
return await q.first()
|
||||||
|
|
||||||
|
async def get_target_channel_for_user_by_telegram(
|
||||||
|
self, user_id: uuid.UUID, telegram_id: int
|
||||||
|
) -> domain.TargetChannel | None:
|
||||||
|
return (
|
||||||
|
await domain.TargetChannel.filter(telegram_id=telegram_id, workspace__workspace_users__user_id=user_id)
|
||||||
|
.prefetch_related('workspace')
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None:
|
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None:
|
||||||
existing = await self.get_target_channel(user_id=channel.user_id, telegram_id=channel.telegram_id)
|
existing = await self.get_target_channel(workspace_id=channel.workspace_id, telegram_id=channel.telegram_id)
|
||||||
|
|
||||||
if not existing:
|
if not existing:
|
||||||
await channel.save()
|
await channel.save()
|
||||||
@@ -68,22 +126,24 @@ class Postgres(DatabaseBase):
|
|||||||
|
|
||||||
existing.title = channel.title
|
existing.title = channel.title
|
||||||
existing.username = channel.username
|
existing.username = channel.username
|
||||||
existing.user_id = channel.user_id
|
existing.workspace_id = channel.workspace_id
|
||||||
existing.status = channel.status
|
existing.status = channel.status
|
||||||
existing.deleted_at = None
|
existing.deleted_at = None
|
||||||
existing.updated_at = timezone.now()
|
existing.updated_at = timezone.now()
|
||||||
await existing.save()
|
await existing.save()
|
||||||
|
|
||||||
async def get_user_target_channels(self, user_id: uuid.UUID) -> list[domain.TargetChannel]:
|
async def get_workspace_target_channels(self, workspace_id: uuid.UUID) -> list[domain.TargetChannel]:
|
||||||
return await domain.TargetChannel.filter(user_id=user_id, status=domain.TargetChannelStatus.ACTIVE).all()
|
return await domain.TargetChannel.filter(
|
||||||
|
workspace_id=workspace_id, status=domain.TargetChannelStatus.ACTIVE
|
||||||
|
).all()
|
||||||
|
|
||||||
async def update_target_channel(self, channel: domain.TargetChannel) -> None:
|
async def update_target_channel(self, channel: domain.TargetChannel) -> None:
|
||||||
await channel.save()
|
await channel.save()
|
||||||
|
|
||||||
async def get_external_channel(
|
async def get_external_channel(
|
||||||
self, user_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
self, workspace_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
|
||||||
) -> domain.ExternalChannel | None:
|
) -> domain.ExternalChannel | None:
|
||||||
query = domain.ExternalChannel.filter(user_id=user_id)
|
query = domain.ExternalChannel.filter(workspace_id=workspace_id)
|
||||||
if channel_id:
|
if channel_id:
|
||||||
query = query.filter(id=channel_id)
|
query = query.filter(id=channel_id)
|
||||||
if telegram_id:
|
if telegram_id:
|
||||||
@@ -94,8 +154,8 @@ class Postgres(DatabaseBase):
|
|||||||
async def get_external_channels_for_target(self, target_channel_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
async def get_external_channels_for_target(self, target_channel_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
||||||
return await domain.ExternalChannel.filter(target_channels__id=target_channel_id).all()
|
return await domain.ExternalChannel.filter(target_channels__id=target_channel_id).all()
|
||||||
|
|
||||||
async def get_user_external_channels(self, user_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
async def get_workspace_external_channels(self, workspace_id: uuid.UUID) -> list[domain.ExternalChannel]:
|
||||||
return await domain.ExternalChannel.filter(user_id=user_id).all()
|
return await domain.ExternalChannel.filter(workspace_id=workspace_id).all()
|
||||||
|
|
||||||
async def add_external_channel_to_targets(
|
async def add_external_channel_to_targets(
|
||||||
self, external_channel: domain.ExternalChannel, target_channel_ids: list[uuid.UUID]
|
self, external_channel: domain.ExternalChannel, target_channel_ids: list[uuid.UUID]
|
||||||
@@ -121,16 +181,18 @@ class Postgres(DatabaseBase):
|
|||||||
async def update_external_channel(self, channel: domain.ExternalChannel) -> None:
|
async def update_external_channel(self, channel: domain.ExternalChannel) -> None:
|
||||||
await channel.save()
|
await channel.save()
|
||||||
|
|
||||||
async def get_creative(self, user_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
|
async def get_creative(self, workspace_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
|
||||||
return await domain.Creative.get_or_none(id=creative_id, user_id=user_id).prefetch_related('target_channel')
|
return await domain.Creative.get_or_none(id=creative_id, workspace_id=workspace_id).prefetch_related(
|
||||||
|
'target_channel'
|
||||||
|
)
|
||||||
|
|
||||||
async def update_creative(self, creative: domain.Creative) -> None:
|
async def update_creative(self, creative: domain.Creative) -> None:
|
||||||
await creative.save()
|
await creative.save()
|
||||||
|
|
||||||
async def get_user_creatives(
|
async def get_workspace_creatives(
|
||||||
self, user_id: uuid.UUID, target_channel_id: uuid.UUID | None = None, include_archived: bool = False
|
self, workspace_id: uuid.UUID, target_channel_id: uuid.UUID | None = None, include_archived: bool = False
|
||||||
) -> list[domain.Creative]:
|
) -> list[domain.Creative]:
|
||||||
query = domain.Creative.filter(user_id=user_id)
|
query = domain.Creative.filter(workspace_id=workspace_id)
|
||||||
|
|
||||||
if target_channel_id:
|
if target_channel_id:
|
||||||
query = query.filter(target_channel_id=target_channel_id)
|
query = query.filter(target_channel_id=target_channel_id)
|
||||||
@@ -143,8 +205,8 @@ class Postgres(DatabaseBase):
|
|||||||
async def delete_creative(self, creative_id: uuid.UUID) -> None:
|
async def delete_creative(self, creative_id: uuid.UUID) -> None:
|
||||||
await domain.Creative.filter(id=creative_id).delete()
|
await domain.Creative.filter(id=creative_id).delete()
|
||||||
|
|
||||||
async def get_placement(self, user_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, user_id=user_id).prefetch_related(
|
return await domain.Placement.get_or_none(id=placement_id, workspace_id=workspace_id).prefetch_related(
|
||||||
'target_channel', 'external_channel', 'creative'
|
'target_channel', 'external_channel', 'creative'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -154,15 +216,15 @@ class Postgres(DatabaseBase):
|
|||||||
async def update_placement(self, placement: domain.Placement) -> None:
|
async def update_placement(self, placement: domain.Placement) -> None:
|
||||||
await placement.save()
|
await placement.save()
|
||||||
|
|
||||||
async def get_user_placements(
|
async def get_workspace_placements(
|
||||||
self,
|
self,
|
||||||
user_id: uuid.UUID,
|
workspace_id: uuid.UUID,
|
||||||
target_channel_id: uuid.UUID | None = None,
|
target_channel_id: uuid.UUID | None = None,
|
||||||
external_channel_id: uuid.UUID | None = None,
|
external_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,
|
||||||
) -> list[domain.Placement]:
|
) -> list[domain.Placement]:
|
||||||
query = domain.Placement.filter(user_id=user_id)
|
query = domain.Placement.filter(workspace_id=workspace_id)
|
||||||
|
|
||||||
if target_channel_id:
|
if target_channel_id:
|
||||||
query = query.filter(target_channel_id=target_channel_id)
|
query = query.filter(target_channel_id=target_channel_id)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from src.controller.http.external_channels import external_channels_router
|
|||||||
from src.controller.http.placements import placements_router
|
from src.controller.http.placements import placements_router
|
||||||
from src.controller.http.target_channels import target_channels_router
|
from src.controller.http.target_channels import target_channels_router
|
||||||
from src.controller.http.views import views_router
|
from src.controller.http.views import views_router
|
||||||
|
from src.controller.http.workspaces import workspaces_router
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
||||||
@@ -19,5 +20,6 @@ api_v1_router.include_router(creatives_router)
|
|||||||
api_v1_router.include_router(placements_router)
|
api_v1_router.include_router(placements_router)
|
||||||
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_router.include_router(api_v1_router)
|
api_router.include_router(api_v1_router)
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ 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
|
||||||
|
|
||||||
analytics_router = APIRouter(prefix='/analytics', tags=['analytics'])
|
analytics_router = APIRouter(prefix='/workspaces/{workspace_id}/analytics', tags=['analytics'])
|
||||||
|
|
||||||
|
|
||||||
@analytics_router.get('/placements')
|
@analytics_router.get('/placements')
|
||||||
async def get_placements_analytics(
|
async def get_placements_analytics(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
target_channel_id: uuid.UUID | None = None,
|
||||||
) -> dto.GetPlacementsAnalyticsOutput:
|
) -> dto.GetPlacementsAnalyticsOutput:
|
||||||
input = dto.GetPlacementsAnalyticsInput(
|
input = dto.GetPlacementsAnalyticsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
target_channel_id=target_channel_id,
|
||||||
)
|
)
|
||||||
return await deps.get_usecase().get_placements_analytics(input)
|
return await deps.get_usecase().get_placements_analytics(input)
|
||||||
@@ -25,11 +27,13 @@ async def get_placements_analytics(
|
|||||||
|
|
||||||
@analytics_router.get('/creatives')
|
@analytics_router.get('/creatives')
|
||||||
async def get_creatives_analytics(
|
async def get_creatives_analytics(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
target_channel_id: uuid.UUID | None = None,
|
||||||
) -> dto.GetCreativesAnalyticsOutput:
|
) -> dto.GetCreativesAnalyticsOutput:
|
||||||
input = dto.GetCreativesAnalyticsInput(
|
input = dto.GetCreativesAnalyticsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
target_channel_id=target_channel_id,
|
||||||
)
|
)
|
||||||
return await deps.get_usecase().get_creatives_analytics(input)
|
return await deps.get_usecase().get_creatives_analytics(input)
|
||||||
@@ -37,11 +41,13 @@ async def get_creatives_analytics(
|
|||||||
|
|
||||||
@analytics_router.get('/external-channels')
|
@analytics_router.get('/external-channels')
|
||||||
async def get_external_channels_analytics(
|
async def get_external_channels_analytics(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
target_channel_id: uuid.UUID | None = None,
|
||||||
) -> dto.GetExternalChannelsAnalyticsOutput:
|
) -> dto.GetExternalChannelsAnalyticsOutput:
|
||||||
input = dto.GetExternalChannelsAnalyticsInput(
|
input = dto.GetExternalChannelsAnalyticsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
target_channel_id=target_channel_id,
|
||||||
)
|
)
|
||||||
return await deps.get_usecase().get_external_channels_analytics(input)
|
return await deps.get_usecase().get_external_channels_analytics(input)
|
||||||
@@ -49,6 +55,7 @@ async def get_external_channels_analytics(
|
|||||||
|
|
||||||
@analytics_router.get('/spending')
|
@analytics_router.get('/spending')
|
||||||
async def get_spending_analytics(
|
async def get_spending_analytics(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
target_channel_id: uuid.UUID | None = None,
|
||||||
date_from: datetime.datetime | None = None,
|
date_from: datetime.datetime | None = None,
|
||||||
@@ -57,6 +64,7 @@ async def get_spending_analytics(
|
|||||||
) -> dto.GetSpendingAnalyticsOutput:
|
) -> dto.GetSpendingAnalyticsOutput:
|
||||||
input = dto.GetSpendingAnalyticsInput(
|
input = dto.GetSpendingAnalyticsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
target_channel_id=target_channel_id,
|
||||||
date_from=date_from,
|
date_from=date_from,
|
||||||
date_to=date_to,
|
date_to=date_to,
|
||||||
|
|||||||
@@ -7,17 +7,19 @@ 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
|
||||||
|
|
||||||
creatives_router = APIRouter(prefix='/creatives', tags=['creatives'])
|
creatives_router = APIRouter(prefix='/workspaces/{workspace_id}/creatives', tags=['creatives'])
|
||||||
|
|
||||||
|
|
||||||
@creatives_router.get('')
|
@creatives_router.get('')
|
||||||
async def list_creatives(
|
async def list_creatives(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
target_channel_id: uuid.UUID | None = None,
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
) -> dto.GetCreativesOutput:
|
) -> dto.GetCreativesOutput:
|
||||||
input = dto.GetCreativesInput(
|
input = dto.GetCreativesInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
target_channel_id=target_channel_id,
|
||||||
include_archived=include_archived,
|
include_archived=include_archived,
|
||||||
)
|
)
|
||||||
@@ -28,11 +30,13 @@ async def list_creatives(
|
|||||||
@creatives_router.get('/{creative_id}')
|
@creatives_router.get('/{creative_id}')
|
||||||
async def get_creative(
|
async def get_creative(
|
||||||
creative_id: uuid.UUID,
|
creative_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.CreativeOutput:
|
) -> dto.CreativeOutput:
|
||||||
input = dto.GetCreativeInput(
|
input = dto.GetCreativeInput(
|
||||||
creative_id=creative_id,
|
creative_id=creative_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await deps.get_usecase().get_creative(input=input)
|
return await deps.get_usecase().get_creative(input=input)
|
||||||
@@ -41,32 +45,37 @@ async def get_creative(
|
|||||||
@creatives_router.post('')
|
@creatives_router.post('')
|
||||||
async def create_creative(
|
async def create_creative(
|
||||||
request: dto.CreateCreativeInput,
|
request: dto.CreateCreativeInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.CreativeOutput:
|
) -> dto.CreativeOutput:
|
||||||
return await deps.get_usecase().create_creative(request, current_user.user_id)
|
return await deps.get_usecase().create_creative(request, current_user.user_id, workspace_id)
|
||||||
|
|
||||||
|
|
||||||
@creatives_router.patch('/{creative_id}')
|
@creatives_router.patch('/{creative_id}')
|
||||||
async def update_creative(
|
async def update_creative(
|
||||||
creative_id: uuid.UUID,
|
creative_id: uuid.UUID,
|
||||||
request: dto.UpdateCreativeInput,
|
request: dto.UpdateCreativeInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.CreativeOutput:
|
) -> dto.CreativeOutput:
|
||||||
return await deps.get_usecase().update_creative(
|
return await deps.get_usecase().update_creative(
|
||||||
creative_id=creative_id,
|
creative_id=creative_id,
|
||||||
input=request,
|
input=request,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@creatives_router.delete('/{creative_id}')
|
@creatives_router.delete('/{creative_id}')
|
||||||
async def delete_creative(
|
async def delete_creative(
|
||||||
creative_id: uuid.UUID,
|
creative_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)],
|
||||||
) -> None:
|
) -> None:
|
||||||
input = dto.DeleteCreativeInput(
|
input = dto.DeleteCreativeInput(
|
||||||
creative_id=creative_id,
|
creative_id=creative_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
await deps.get_usecase().delete_creative(input)
|
await deps.get_usecase().delete_creative(input)
|
||||||
|
|||||||
@@ -7,18 +7,20 @@ 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
|
||||||
|
|
||||||
external_channels_router = APIRouter(prefix='/external_channels', tags=['external_channels'])
|
external_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/external_channels', tags=['external_channels'])
|
||||||
|
|
||||||
|
|
||||||
@external_channels_router.get('/target/{target_channel_id}')
|
@external_channels_router.get('/target/{target_channel_id}')
|
||||||
async def get_external_channels(
|
async def get_external_channels(
|
||||||
target_channel_id: uuid.UUID,
|
target_channel_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.GetExternalChannelsOutput:
|
) -> dto.GetExternalChannelsOutput:
|
||||||
"""Get all external channels for a specific target channel."""
|
"""Get all external channels for a specific target channel."""
|
||||||
input = dto.GetExternalChannelsInput(
|
input = dto.GetExternalChannelsInput(
|
||||||
target_channel_id=target_channel_id,
|
target_channel_id=target_channel_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await deps.get_usecase().get_external_channels(input=input)
|
return await deps.get_usecase().get_external_channels(input=input)
|
||||||
@@ -27,12 +29,14 @@ async def get_external_channels(
|
|||||||
@external_channels_router.post('')
|
@external_channels_router.post('')
|
||||||
async def create_external_channel(
|
async def create_external_channel(
|
||||||
request: dto.CreateExternalChannelInput,
|
request: dto.CreateExternalChannelInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.ExternalChannelOutput:
|
) -> dto.ExternalChannelOutput:
|
||||||
"""Create a new external channel and link it to target channels."""
|
"""Create a new external channel and link it to target channels."""
|
||||||
return await deps.get_usecase().create_external_channel(
|
return await deps.get_usecase().create_external_channel(
|
||||||
input=request,
|
input=request,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -40,6 +44,7 @@ async def create_external_channel(
|
|||||||
async def update_external_channel(
|
async def update_external_channel(
|
||||||
channel_id: uuid.UUID,
|
channel_id: uuid.UUID,
|
||||||
request: dto.UpdateExternalChannelInput,
|
request: dto.UpdateExternalChannelInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.ExternalChannelOutput:
|
) -> dto.ExternalChannelOutput:
|
||||||
"""Update external channel fields (title, username, description, subscribers_count)."""
|
"""Update external channel fields (title, username, description, subscribers_count)."""
|
||||||
@@ -47,6 +52,7 @@ async def update_external_channel(
|
|||||||
channel_id=channel_id,
|
channel_id=channel_id,
|
||||||
input=request,
|
input=request,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -54,6 +60,7 @@ async def update_external_channel(
|
|||||||
async def update_external_channel_links(
|
async def update_external_channel_links(
|
||||||
channel_id: uuid.UUID,
|
channel_id: uuid.UUID,
|
||||||
request: dto.UpdateExternalChannelLinksInput,
|
request: dto.UpdateExternalChannelLinksInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.ExternalChannelOutput:
|
) -> dto.ExternalChannelOutput:
|
||||||
"""
|
"""
|
||||||
@@ -65,18 +72,21 @@ async def update_external_channel_links(
|
|||||||
channel_id=channel_id,
|
channel_id=channel_id,
|
||||||
input=request,
|
input=request,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@external_channels_router.delete('/{channel_id}')
|
@external_channels_router.delete('/{channel_id}')
|
||||||
async def delete_external_channel(
|
async def delete_external_channel(
|
||||||
channel_id: uuid.UUID,
|
channel_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)],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Delete an external channel."""
|
"""Delete an external channel."""
|
||||||
input = dto.DeleteExternalChannelInput(
|
input = dto.DeleteExternalChannelInput(
|
||||||
channel_id=channel_id,
|
channel_id=channel_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
await deps.get_usecase().delete_external_channel(input=input)
|
await deps.get_usecase().delete_external_channel(input=input)
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ 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
|
||||||
|
|
||||||
placements_router = APIRouter(prefix='/placements', tags=['placements'])
|
placements_router = APIRouter(prefix='/workspaces/{workspace_id}/placements', tags=['placements'])
|
||||||
|
|
||||||
|
|
||||||
@placements_router.get('')
|
@placements_router.get('')
|
||||||
async def list_placements(
|
async def list_placements(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
target_channel_id: uuid.UUID | None = None,
|
target_channel_id: uuid.UUID | None = None,
|
||||||
external_channel_id: uuid.UUID | None = None,
|
external_channel_id: uuid.UUID | None = None,
|
||||||
@@ -20,6 +21,7 @@ async def list_placements(
|
|||||||
) -> dto.GetPlacementsOutput:
|
) -> dto.GetPlacementsOutput:
|
||||||
input = dto.GetPlacementsInput(
|
input = dto.GetPlacementsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
target_channel_id=target_channel_id,
|
target_channel_id=target_channel_id,
|
||||||
external_channel_id=external_channel_id,
|
external_channel_id=external_channel_id,
|
||||||
creative_id=creative_id,
|
creative_id=creative_id,
|
||||||
@@ -32,11 +34,13 @@ async def list_placements(
|
|||||||
@placements_router.get('/{placement_id}')
|
@placements_router.get('/{placement_id}')
|
||||||
async def get_placement(
|
async def get_placement(
|
||||||
placement_id: uuid.UUID,
|
placement_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.PlacementOutput:
|
) -> dto.PlacementOutput:
|
||||||
input = dto.GetPlacementInput(
|
input = dto.GetPlacementInput(
|
||||||
placement_id=placement_id,
|
placement_id=placement_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await deps.get_usecase().get_placement(input=input)
|
return await deps.get_usecase().get_placement(input=input)
|
||||||
@@ -45,32 +49,37 @@ async def get_placement(
|
|||||||
@placements_router.post('')
|
@placements_router.post('')
|
||||||
async def create_placement(
|
async def create_placement(
|
||||||
request: dto.CreatePlacementInput,
|
request: dto.CreatePlacementInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.PlacementOutput:
|
) -> dto.PlacementOutput:
|
||||||
return await deps.get_usecase().create_placement(request, current_user.user_id)
|
return await deps.get_usecase().create_placement(request, current_user.user_id, workspace_id)
|
||||||
|
|
||||||
|
|
||||||
@placements_router.patch('/{placement_id}')
|
@placements_router.patch('/{placement_id}')
|
||||||
async def update_placement(
|
async def update_placement(
|
||||||
placement_id: uuid.UUID,
|
placement_id: uuid.UUID,
|
||||||
request: dto.UpdatePlacementInput,
|
request: dto.UpdatePlacementInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.PlacementOutput:
|
) -> dto.PlacementOutput:
|
||||||
return await deps.get_usecase().update_placement(
|
return await deps.get_usecase().update_placement(
|
||||||
placement_id=placement_id,
|
placement_id=placement_id,
|
||||||
input=request,
|
input=request,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@placements_router.delete('/{placement_id}')
|
@placements_router.delete('/{placement_id}')
|
||||||
async def delete_placement(
|
async def delete_placement(
|
||||||
placement_id: uuid.UUID,
|
placement_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)],
|
||||||
) -> None:
|
) -> None:
|
||||||
input = dto.DeletePlacementInput(
|
input = dto.DeletePlacementInput(
|
||||||
placement_id=placement_id,
|
placement_id=placement_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
await deps.get_usecase().delete_placement(input)
|
await deps.get_usecase().delete_placement(input)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import uuid
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
@@ -6,15 +7,17 @@ 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
|
||||||
|
|
||||||
target_channels_router = APIRouter(prefix='/target_channels', tags=['target_channels'])
|
target_channels_router = APIRouter(prefix='/workspaces/{workspace_id}/target_channels', tags=['target_channels'])
|
||||||
|
|
||||||
|
|
||||||
@target_channels_router.get('')
|
@target_channels_router.get('')
|
||||||
async def get_user_target_chans(
|
async def get_user_target_chans(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
) -> dto.GetUserTargetChansOutput:
|
) -> dto.GetUserTargetChansOutput:
|
||||||
input = dto.GetUserTargetChansInput(
|
input = dto.GetUserTargetChansInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await deps.get_usecase().get_user_target_chans(input=input)
|
return await deps.get_usecase().get_user_target_chans(input=input)
|
||||||
|
|||||||
@@ -8,12 +8,13 @@ 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
|
||||||
|
|
||||||
views_router = APIRouter(prefix='/placements/{placement_id}/views', tags=['views'])
|
views_router = APIRouter(prefix='/workspaces/{workspace_id}/placements/{placement_id}/views', tags=['views'])
|
||||||
|
|
||||||
|
|
||||||
@views_router.get('/history')
|
@views_router.get('/history')
|
||||||
async def get_views_history(
|
async def get_views_history(
|
||||||
placement_id: uuid.UUID,
|
placement_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)],
|
||||||
from_date: datetime.datetime | None = None,
|
from_date: datetime.datetime | None = None,
|
||||||
to_date: datetime.datetime | None = None,
|
to_date: datetime.datetime | None = None,
|
||||||
@@ -21,23 +22,9 @@ async def get_views_history(
|
|||||||
input_data = dto.GetViewsHistoryInput(
|
input_data = dto.GetViewsHistoryInput(
|
||||||
placement_id=placement_id,
|
placement_id=placement_id,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
from_date=from_date,
|
from_date=from_date,
|
||||||
to_date=to_date,
|
to_date=to_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await deps.get_usecase().get_views_history(input=input_data)
|
return await deps.get_usecase().get_views_history(input=input_data)
|
||||||
|
|
||||||
|
|
||||||
@views_router.post('/manual')
|
|
||||||
async def update_views_manually(
|
|
||||||
placement_id: uuid.UUID,
|
|
||||||
views_count: int,
|
|
||||||
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
|
||||||
) -> dto.PlacementOutput:
|
|
||||||
input_data = dto.UpdateViewsManuallyInput(
|
|
||||||
placement_id=placement_id,
|
|
||||||
user_id=current_user.user_id,
|
|
||||||
views_count=views_count,
|
|
||||||
)
|
|
||||||
|
|
||||||
return await deps.get_usecase().update_views_manually(input=input_data)
|
|
||||||
|
|||||||
42
src/controller/http/workspaces.py
Normal file
42
src/controller/http/workspaces.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi.routing import APIRouter
|
||||||
|
|
||||||
|
from src import deps, dto
|
||||||
|
from src.adapter.jwt import JWTPayload
|
||||||
|
|
||||||
|
workspaces_router = APIRouter(prefix='/workspaces', tags=['workspaces'])
|
||||||
|
|
||||||
|
|
||||||
|
@workspaces_router.get('')
|
||||||
|
async def list_workspaces(
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> dto.GetWorkspacesOutput:
|
||||||
|
return await deps.get_usecase().get_workspaces(current_user.user_id)
|
||||||
|
|
||||||
|
|
||||||
|
@workspaces_router.post('')
|
||||||
|
async def create_workspace(
|
||||||
|
request: dto.CreateWorkspaceInput,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> dto.CreateWorkspaceOutput:
|
||||||
|
return await deps.get_usecase().create_workspace(current_user.user_id, request)
|
||||||
|
|
||||||
|
|
||||||
|
@workspaces_router.patch('/{workspace_id}')
|
||||||
|
async def update_workspace(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
request: dto.UpdateWorkspaceInput,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> dto.WorkspaceMembershipOutput:
|
||||||
|
return await deps.get_usecase().update_workspace(workspace_id, current_user.user_id, request)
|
||||||
|
|
||||||
|
|
||||||
|
@workspaces_router.delete('/{workspace_id}')
|
||||||
|
async def delete_workspace(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> None:
|
||||||
|
await deps.get_usecase().delete_workspace(workspace_id, current_user.user_id)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
__all__ = (
|
__all__ = (
|
||||||
'User',
|
'User',
|
||||||
|
'Workspace',
|
||||||
|
'WorkspaceUser',
|
||||||
'TargetChannel',
|
'TargetChannel',
|
||||||
'ExternalChannel',
|
'ExternalChannel',
|
||||||
'Creative',
|
'Creative',
|
||||||
@@ -17,6 +19,8 @@ __all__ = (
|
|||||||
'PostViewsAvailability',
|
'PostViewsAvailability',
|
||||||
'LoginToken',
|
'LoginToken',
|
||||||
'UserNotFound',
|
'UserNotFound',
|
||||||
|
'WorkspaceNotFound',
|
||||||
|
'WorkspaceAccessDenied',
|
||||||
'LoginTokenNotFound',
|
'LoginTokenNotFound',
|
||||||
'LoginTokenExpired',
|
'LoginTokenExpired',
|
||||||
'LoginTokenAlreadyUsed',
|
'LoginTokenAlreadyUsed',
|
||||||
@@ -44,6 +48,8 @@ from .error import (
|
|||||||
PlacementNotFound,
|
PlacementNotFound,
|
||||||
TargetChannelNotFound,
|
TargetChannelNotFound,
|
||||||
UserNotFound,
|
UserNotFound,
|
||||||
|
WorkspaceAccessDenied,
|
||||||
|
WorkspaceNotFound,
|
||||||
)
|
)
|
||||||
from .external_channel import ExternalChannel
|
from .external_channel import ExternalChannel
|
||||||
from .login_token import LoginToken
|
from .login_token import LoginToken
|
||||||
@@ -54,3 +60,4 @@ from .subscription import Subscription, SubscriptionStatus
|
|||||||
from .target_channel import TargetChannel, TargetChannelStatus
|
from .target_channel import TargetChannel, TargetChannelStatus
|
||||||
from .telegram_state import TelegramState, TelegramStateEnum
|
from .telegram_state import TelegramState, TelegramStateEnum
|
||||||
from .user import User
|
from .user import User
|
||||||
|
from .workspace import Workspace, WorkspaceUser
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from .base import TimestampedModel
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .target_channel import TargetChannel
|
from .target_channel import TargetChannel
|
||||||
from .user import User
|
from .workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
class CreativeStatus(str, enum.Enum):
|
class CreativeStatus(str, enum.Enum):
|
||||||
@@ -23,15 +23,15 @@ class Creative(TimestampedModel):
|
|||||||
status = fields.CharEnumField(CreativeStatus, default=CreativeStatus.ACTIVE)
|
status = fields.CharEnumField(CreativeStatus, default=CreativeStatus.ACTIVE)
|
||||||
placements_count = fields.IntField(default=0)
|
placements_count = fields.IntField(default=0)
|
||||||
|
|
||||||
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
'models.User', related_name='creatives', on_delete=fields.CASCADE, index=True
|
'models.Workspace', related_name='creatives', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
|
target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField(
|
||||||
'models.TargetChannel', related_name='creatives', on_delete=fields.CASCADE, index=True
|
'models.TargetChannel', related_name='creatives', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
user_id: UUID
|
workspace_id: UUID
|
||||||
target_channel_id: UUID
|
target_channel_id: UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -64,3 +64,15 @@ def PlacementNotFound(placement_id: uuid.UUID | None = None) -> HTTPException:
|
|||||||
if placement_id is None:
|
if placement_id is None:
|
||||||
return HTTPException(status.HTTP_404_NOT_FOUND, 'Placement not found')
|
return HTTPException(status.HTTP_404_NOT_FOUND, 'Placement not found')
|
||||||
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 WorkspaceNotFound(workspace_id: uuid.UUID | None = None) -> HTTPException:
|
||||||
|
if workspace_id is None:
|
||||||
|
return HTTPException(status.HTTP_404_NOT_FOUND, 'Workspace not found')
|
||||||
|
return HTTPException(status.HTTP_404_NOT_FOUND, f'Workspace {workspace_id} not found')
|
||||||
|
|
||||||
|
|
||||||
|
def WorkspaceAccessDenied(workspace_id: uuid.UUID | None = None) -> HTTPException:
|
||||||
|
if workspace_id is None:
|
||||||
|
return HTTPException(status.HTTP_403_FORBIDDEN, 'Workspace access denied')
|
||||||
|
return HTTPException(status.HTTP_403_FORBIDDEN, f'Workspace {workspace_id} access denied')
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from .base import TimestampedModel
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .target_channel import TargetChannel
|
from .target_channel import TargetChannel
|
||||||
from .user import User
|
from .workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
class ExternalChannel(TimestampedModel):
|
class ExternalChannel(TimestampedModel):
|
||||||
@@ -18,14 +18,13 @@ class ExternalChannel(TimestampedModel):
|
|||||||
description = fields.TextField(null=True)
|
description = fields.TextField(null=True)
|
||||||
subscribers_count = fields.IntField(null=True)
|
subscribers_count = fields.IntField(null=True)
|
||||||
|
|
||||||
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
'models.User', related_name='external_channels', on_delete=fields.CASCADE, index=True
|
'models.Workspace', related_name='external_channels', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
target_channels: fields.ManyToManyRelation['TargetChannel']
|
target_channels: fields.ManyToManyRelation['TargetChannel']
|
||||||
user_id: UUID
|
workspace_id: UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'external_channel'
|
table = 'external_channel'
|
||||||
unique_together = (('user_id', 'telegram_id'),)
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ if TYPE_CHECKING:
|
|||||||
from .creative import Creative
|
from .creative import Creative
|
||||||
from .external_channel import ExternalChannel
|
from .external_channel import ExternalChannel
|
||||||
from .target_channel import TargetChannel
|
from .target_channel import TargetChannel
|
||||||
from .user import User
|
from .workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
class PlacementStatus(str, enum.Enum):
|
class PlacementStatus(str, enum.Enum):
|
||||||
@@ -59,15 +59,15 @@ class Placement(TimestampedModel):
|
|||||||
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField(
|
||||||
'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True
|
'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
'models.User', related_name='placements', on_delete=fields.CASCADE, index=True
|
'models.Workspace', related_name='placements', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
target_channel_id: UUID
|
target_channel_id: UUID
|
||||||
external_channel_id: UUID
|
external_channel_id: UUID
|
||||||
creative_id: UUID
|
creative_id: UUID
|
||||||
user_id: UUID
|
workspace_id: UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'placement'
|
table = 'placement'
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from .base import TimestampedModel
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .external_channel import ExternalChannel
|
from .external_channel import ExternalChannel
|
||||||
from .user import User
|
from .workspace import Workspace
|
||||||
|
|
||||||
|
|
||||||
class TargetChannelStatus(str, enum.Enum):
|
class TargetChannelStatus(str, enum.Enum):
|
||||||
@@ -24,8 +24,8 @@ class TargetChannel(TimestampedModel):
|
|||||||
username = fields.CharField(max_length=255, null=True, index=True)
|
username = fields.CharField(max_length=255, null=True, index=True)
|
||||||
status = fields.CharEnumField(TargetChannelStatus, default=TargetChannelStatus.ACTIVE)
|
status = fields.CharEnumField(TargetChannelStatus, default=TargetChannelStatus.ACTIVE)
|
||||||
|
|
||||||
user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField(
|
workspace: fields.ForeignKeyRelation['Workspace'] = fields.ForeignKeyField(
|
||||||
'models.User', related_name='target_channels', on_delete=fields.CASCADE, index=True
|
'models.Workspace', related_name='target_channels', on_delete=fields.CASCADE, index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
external_channels: fields.ManyToManyRelation['ExternalChannel'] = fields.ManyToManyField(
|
external_channels: fields.ManyToManyRelation['ExternalChannel'] = fields.ManyToManyField(
|
||||||
@@ -37,7 +37,7 @@ class TargetChannel(TimestampedModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
user_id: UUID
|
workspace_id: UUID
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table = 'target_channel'
|
table = 'target_channel'
|
||||||
|
|||||||
38
src/domain/workspace.py
Normal file
38
src/domain/workspace.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from tortoise import fields
|
||||||
|
|
||||||
|
from .base import TimestampedModel
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
|
||||||
|
class Workspace(TimestampedModel):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
name = fields.CharField(max_length=255)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'workspace'
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUser(TimestampedModel):
|
||||||
|
id = fields.UUIDField(pk=True)
|
||||||
|
|
||||||
|
workspace: fields.ForeignKeyRelation[Workspace] = fields.ForeignKeyField(
|
||||||
|
'models.Workspace', related_name='workspace_users', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
user: fields.ForeignKeyRelation[User] = fields.ForeignKeyField(
|
||||||
|
'models.User', related_name='workspace_users', on_delete=fields.CASCADE, index=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
user_id: uuid.UUID
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
table = 'workspace_user'
|
||||||
|
unique_together = (('workspace_id', 'user_id'),)
|
||||||
@@ -53,6 +53,11 @@ __all__ = (
|
|||||||
'ParserPostFoundEvent',
|
'ParserPostFoundEvent',
|
||||||
'ParserPostDeletedEvent',
|
'ParserPostDeletedEvent',
|
||||||
'ParserViewsUpdatedEvent',
|
'ParserViewsUpdatedEvent',
|
||||||
|
'WorkspaceMembershipOutput',
|
||||||
|
'GetWorkspacesOutput',
|
||||||
|
'CreateWorkspaceInput',
|
||||||
|
'CreateWorkspaceOutput',
|
||||||
|
'UpdateWorkspaceInput',
|
||||||
)
|
)
|
||||||
|
|
||||||
from .analytics import (
|
from .analytics import (
|
||||||
@@ -122,3 +127,10 @@ from .views import (
|
|||||||
PlacementViewsHistoryOutput,
|
PlacementViewsHistoryOutput,
|
||||||
UpdateViewsManuallyInput,
|
UpdateViewsManuallyInput,
|
||||||
)
|
)
|
||||||
|
from .workspace import (
|
||||||
|
CreateWorkspaceInput,
|
||||||
|
CreateWorkspaceOutput,
|
||||||
|
GetWorkspacesOutput,
|
||||||
|
UpdateWorkspaceInput,
|
||||||
|
WorkspaceMembershipOutput,
|
||||||
|
)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class ExternalChannelAnalyticsOutput(pydantic.BaseModel):
|
|||||||
|
|
||||||
class GetPlacementsAnalyticsInput(pydantic.BaseModel):
|
class GetPlacementsAnalyticsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
target_channel_id: uuid.UUID | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ class GetPlacementsAnalyticsOutput(pydantic.BaseModel):
|
|||||||
|
|
||||||
class GetCreativesAnalyticsInput(pydantic.BaseModel):
|
class GetCreativesAnalyticsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
target_channel_id: uuid.UUID | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -72,6 +74,7 @@ class GetCreativesAnalyticsOutput(pydantic.BaseModel):
|
|||||||
|
|
||||||
class GetExternalChannelsAnalyticsInput(pydantic.BaseModel):
|
class GetExternalChannelsAnalyticsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
target_channel_id: uuid.UUID | None = None
|
||||||
|
|
||||||
|
|
||||||
@@ -90,6 +93,7 @@ class SpendingDataPoint(pydantic.BaseModel):
|
|||||||
|
|
||||||
class GetSpendingAnalyticsInput(pydantic.BaseModel):
|
class GetSpendingAnalyticsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
target_channel_id: 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
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class CreativeOutput(pydantic.BaseModel):
|
|||||||
|
|
||||||
class GetCreativesInput(pydantic.BaseModel):
|
class GetCreativesInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
target_channel_id: uuid.UUID | None = None
|
||||||
include_archived: bool = False
|
include_archived: bool = False
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ class GetCreativesOutput(pydantic.BaseModel):
|
|||||||
class GetCreativeInput(pydantic.BaseModel):
|
class GetCreativeInput(pydantic.BaseModel):
|
||||||
creative_id: uuid.UUID
|
creative_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
class CreateCreativeInput(pydantic.BaseModel):
|
class CreateCreativeInput(pydantic.BaseModel):
|
||||||
@@ -47,3 +49,4 @@ class UpdateCreativeInput(pydantic.BaseModel):
|
|||||||
class DeleteCreativeInput(pydantic.BaseModel):
|
class DeleteCreativeInput(pydantic.BaseModel):
|
||||||
creative_id: uuid.UUID
|
creative_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ExternalChannelOutput(pydantic.BaseModel):
|
|||||||
class GetExternalChannelsInput(pydantic.BaseModel):
|
class GetExternalChannelsInput(pydantic.BaseModel):
|
||||||
target_channel_id: uuid.UUID
|
target_channel_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
class GetExternalChannelsOutput(pydantic.BaseModel):
|
class GetExternalChannelsOutput(pydantic.BaseModel):
|
||||||
@@ -33,6 +34,7 @@ class CreateExternalChannelInput(pydantic.BaseModel):
|
|||||||
class DeleteExternalChannelInput(pydantic.BaseModel):
|
class DeleteExternalChannelInput(pydantic.BaseModel):
|
||||||
channel_id: uuid.UUID
|
channel_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
class UpdateExternalChannelInput(pydantic.BaseModel):
|
class UpdateExternalChannelInput(pydantic.BaseModel):
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import pydantic
|
|
||||||
|
|
||||||
|
|
||||||
# Исходящие сообщения (к tgex-parser)
|
|
||||||
class ParserTrackTaskRequest(pydantic.BaseModel):
|
|
||||||
"""Сообщение для parser.track - добавить задачу на отслеживание канала"""
|
|
||||||
|
|
||||||
task_id: str # UUID placement
|
|
||||||
channel: str # username внешнего канала
|
|
||||||
target_text: str # invite_link для поиска в постах
|
|
||||||
|
|
||||||
|
|
||||||
class ParserGetViewsRequest(pydantic.BaseModel):
|
|
||||||
"""Сообщение для parser.get_views - синхронно получить просмотры поста"""
|
|
||||||
|
|
||||||
channel: str
|
|
||||||
message_id: int
|
|
||||||
|
|
||||||
|
|
||||||
# Входящие события (от tgex-parser)
|
|
||||||
class ParserPostFoundEvent(pydantic.BaseModel):
|
|
||||||
"""Событие parser.event.post_found - найден новый пост с target_text"""
|
|
||||||
|
|
||||||
task_id: str # UUID placement
|
|
||||||
channel: str
|
|
||||||
message_id: int
|
|
||||||
link: str # полная ссылка https://t.me/channel/message_id
|
|
||||||
text: str # текст поста
|
|
||||||
views: int
|
|
||||||
|
|
||||||
|
|
||||||
class ParserPostDeletedEvent(pydantic.BaseModel):
|
|
||||||
"""Событие parser.event.post_deleted - отслеживаемый пост удален"""
|
|
||||||
|
|
||||||
task_id: str
|
|
||||||
channel: str
|
|
||||||
message_id: int
|
|
||||||
link: str
|
|
||||||
views: int
|
|
||||||
|
|
||||||
|
|
||||||
class ParserViewsUpdatedEvent(pydantic.BaseModel):
|
|
||||||
"""Событие parser.event.views_updated - обновилось количество просмотров"""
|
|
||||||
|
|
||||||
task_id: str
|
|
||||||
channel: str
|
|
||||||
message_id: int
|
|
||||||
views: int
|
|
||||||
@@ -30,6 +30,7 @@ class PlacementOutput(pydantic.BaseModel):
|
|||||||
|
|
||||||
class GetPlacementsInput(pydantic.BaseModel):
|
class GetPlacementsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
target_channel_id: uuid.UUID | None = None
|
target_channel_id: uuid.UUID | None = None
|
||||||
external_channel_id: uuid.UUID | None = None
|
external_channel_id: uuid.UUID | None = None
|
||||||
creative_id: uuid.UUID | None = None
|
creative_id: uuid.UUID | None = None
|
||||||
@@ -43,6 +44,7 @@ class GetPlacementsOutput(pydantic.BaseModel):
|
|||||||
class GetPlacementInput(pydantic.BaseModel):
|
class GetPlacementInput(pydantic.BaseModel):
|
||||||
placement_id: uuid.UUID
|
placement_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
class CreatePlacementInput(pydantic.BaseModel):
|
class CreatePlacementInput(pydantic.BaseModel):
|
||||||
@@ -65,3 +67,4 @@ class UpdatePlacementInput(pydantic.BaseModel):
|
|||||||
class DeletePlacementInput(pydantic.BaseModel):
|
class DeletePlacementInput(pydantic.BaseModel):
|
||||||
placement_id: uuid.UUID
|
placement_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class ConnectTargetChanOutput(pydantic.BaseModel):
|
|||||||
|
|
||||||
class GetUserTargetChansInput(pydantic.BaseModel):
|
class GetUserTargetChansInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
class GetUserTargetChansOutput(pydantic.BaseModel):
|
class GetUserTargetChansOutput(pydantic.BaseModel):
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class GetViewsHistoryInput(pydantic.BaseModel):
|
|||||||
|
|
||||||
placement_id: uuid.UUID
|
placement_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_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 # Фильтр: по какую дату
|
||||||
|
|
||||||
@@ -34,4 +35,5 @@ class UpdateViewsManuallyInput(pydantic.BaseModel):
|
|||||||
|
|
||||||
placement_id: uuid.UUID
|
placement_id: uuid.UUID
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
|
workspace_id: uuid.UUID
|
||||||
views_count: int
|
views_count: int
|
||||||
|
|||||||
23
src/dto/workspace.py
Normal file
23
src/dto/workspace.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
import pydantic
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceMembershipOutput(pydantic.BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class CreateWorkspaceOutput(WorkspaceMembershipOutput): ...
|
||||||
|
|
||||||
|
|
||||||
|
class GetWorkspacesOutput(pydantic.BaseModel):
|
||||||
|
workspaces: list[WorkspaceMembershipOutput]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateWorkspaceInput(pydantic.BaseModel):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateWorkspaceInput(pydantic.BaseModel):
|
||||||
|
name: str | None = None
|
||||||
@@ -22,6 +22,7 @@ from .creative.get_creative import get_creative
|
|||||||
from .creative.get_creatives import get_creatives
|
from .creative.get_creatives import get_creatives
|
||||||
from .creative.handle_creative_callback import handle_creative_callback
|
from .creative.handle_creative_callback import handle_creative_callback
|
||||||
from .creative.handle_creative_text_input import handle_creative_text_input
|
from .creative.handle_creative_text_input import handle_creative_text_input
|
||||||
|
from .creative.handle_creative_workspace_callback import handle_creative_workspace_callback
|
||||||
from .creative.start_creative_creation import start_creative_creation
|
from .creative.start_creative_creation import start_creative_creation
|
||||||
from .creative.update_creative import update_creative
|
from .creative.update_creative import update_creative
|
||||||
from .external_channel.create_external_channel import create_external_channel
|
from .external_channel.create_external_channel import create_external_channel
|
||||||
@@ -41,7 +42,10 @@ from .target_channel.disconnect_target_chan_by_tg_id import disconnect_target_ch
|
|||||||
from .target_channel.get_user_target_chans import get_user_target_chans
|
from .target_channel.get_user_target_chans import get_user_target_chans
|
||||||
from .target_channel.update_target_chan_permissions import update_target_chan_permissions
|
from .target_channel.update_target_chan_permissions import update_target_chan_permissions
|
||||||
from .views.get_views_history import get_views_history
|
from .views.get_views_history import get_views_history
|
||||||
from .views.update_views_manually import update_views_manually
|
from .workspace.create_workspace import create_workspace
|
||||||
|
from .workspace.delete_workspace import delete_workspace
|
||||||
|
from .workspace.get_workspaces import get_workspaces
|
||||||
|
from .workspace.update_workspace import update_workspace
|
||||||
|
|
||||||
|
|
||||||
class Database(typing.Protocol):
|
class Database(typing.Protocol):
|
||||||
@@ -51,6 +55,24 @@ class Database(typing.Protocol):
|
|||||||
|
|
||||||
async def create_user(self, user: domain.User) -> None: ...
|
async def create_user(self, user: domain.User) -> None: ...
|
||||||
|
|
||||||
|
async def create_workspace(self, workspace: domain.Workspace) -> None: ...
|
||||||
|
|
||||||
|
async def update_workspace(self, workspace: domain.Workspace) -> None: ...
|
||||||
|
|
||||||
|
async def delete_workspace(self, workspace_id: UUID) -> None: ...
|
||||||
|
|
||||||
|
async def add_user_to_workspace(self, workspace_id: UUID, user_id: UUID) -> None: ...
|
||||||
|
|
||||||
|
async def get_user_workspaces(self, user_id: UUID) -> list[domain.WorkspaceUser]: ...
|
||||||
|
|
||||||
|
async def get_workspace(self, workspace_id: UUID) -> domain.Workspace | None: ...
|
||||||
|
|
||||||
|
async def get_workspace_for_user(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace | None: ...
|
||||||
|
|
||||||
|
async def get_default_workspace_for_user(self, user_id: UUID) -> domain.Workspace | None: ...
|
||||||
|
|
||||||
|
async def get_workspace_membership(self, workspace_id: UUID, user_id: UUID) -> domain.WorkspaceUser | None: ...
|
||||||
|
|
||||||
async def create_login_token(self, login_token: domain.LoginToken) -> None: ...
|
async def create_login_token(self, login_token: domain.LoginToken) -> None: ...
|
||||||
|
|
||||||
async def create_creative(self, creative: domain.Creative) -> None: ...
|
async def create_creative(self, creative: domain.Creative) -> None: ...
|
||||||
@@ -60,24 +82,28 @@ class Database(typing.Protocol):
|
|||||||
async def mark_token_as_used(self, token: str) -> None: ...
|
async def mark_token_as_used(self, token: str) -> None: ...
|
||||||
|
|
||||||
async def get_target_channel(
|
async def get_target_channel(
|
||||||
self, user_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
||||||
|
) -> domain.TargetChannel | None: ...
|
||||||
|
|
||||||
|
async def get_target_channel_for_user_by_telegram(
|
||||||
|
self, user_id: UUID, telegram_id: int
|
||||||
) -> domain.TargetChannel | None: ...
|
) -> domain.TargetChannel | None: ...
|
||||||
|
|
||||||
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None: ...
|
async def upsert_target_channel(self, channel: domain.TargetChannel) -> None: ...
|
||||||
|
|
||||||
async def get_user_target_channels(self, user_id: UUID) -> list[domain.TargetChannel]: ...
|
async def get_workspace_target_channels(self, workspace_id: UUID) -> list[domain.TargetChannel]: ...
|
||||||
|
|
||||||
async def update_target_channel(self, channel: domain.TargetChannel) -> None: ...
|
async def update_target_channel(self, channel: domain.TargetChannel) -> None: ...
|
||||||
|
|
||||||
async def get_external_channel(
|
async def get_external_channel(
|
||||||
self, user_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
self, workspace_id: UUID, channel_id: UUID | None = None, telegram_id: int | None = None
|
||||||
) -> domain.ExternalChannel | None: ...
|
) -> domain.ExternalChannel | None: ...
|
||||||
|
|
||||||
async def create_external_channel(self, channel: domain.ExternalChannel) -> None: ...
|
async def create_external_channel(self, channel: domain.ExternalChannel) -> None: ...
|
||||||
|
|
||||||
async def get_external_channels_for_target(self, target_channel_id: UUID) -> list[domain.ExternalChannel]: ...
|
async def get_external_channels_for_target(self, target_channel_id: UUID) -> list[domain.ExternalChannel]: ...
|
||||||
|
|
||||||
async def get_user_external_channels(self, user_id: UUID) -> list[domain.ExternalChannel]: ...
|
async def get_workspace_external_channels(self, workspace_id: UUID) -> list[domain.ExternalChannel]: ...
|
||||||
|
|
||||||
async def add_external_channel_to_targets(
|
async def add_external_channel_to_targets(
|
||||||
self, external_channel: domain.ExternalChannel, target_channel_ids: list[UUID]
|
self, external_channel: domain.ExternalChannel, target_channel_ids: list[UUID]
|
||||||
@@ -89,25 +115,25 @@ class Database(typing.Protocol):
|
|||||||
|
|
||||||
async def update_external_channel(self, channel: domain.ExternalChannel) -> None: ...
|
async def update_external_channel(self, channel: domain.ExternalChannel) -> None: ...
|
||||||
|
|
||||||
async def get_creative(self, user_id: UUID, creative_id: UUID) -> domain.Creative | None: ...
|
async def get_creative(self, workspace_id: UUID, creative_id: UUID) -> domain.Creative | None: ...
|
||||||
|
|
||||||
async def update_creative(self, creative: domain.Creative) -> None: ...
|
async def update_creative(self, creative: domain.Creative) -> None: ...
|
||||||
|
|
||||||
async def get_user_creatives(
|
async def get_workspace_creatives(
|
||||||
self, user_id: UUID, target_channel_id: UUID | None = None, include_archived: bool = False
|
self, workspace_id: UUID, target_channel_id: UUID | None = None, include_archived: bool = False
|
||||||
) -> list[domain.Creative]: ...
|
) -> list[domain.Creative]: ...
|
||||||
|
|
||||||
async def delete_creative(self, creative_id: UUID) -> None: ...
|
async def delete_creative(self, creative_id: UUID) -> None: ...
|
||||||
|
|
||||||
async def get_placement(self, user_id: UUID, placement_id: UUID) -> domain.Placement | None: ...
|
async def get_placement(self, workspace_id: UUID, placement_id: UUID) -> domain.Placement | None: ...
|
||||||
|
|
||||||
async def create_placement(self, placement: domain.Placement) -> None: ...
|
async def create_placement(self, placement: domain.Placement) -> None: ...
|
||||||
|
|
||||||
async def update_placement(self, placement: domain.Placement) -> None: ...
|
async def update_placement(self, placement: domain.Placement) -> None: ...
|
||||||
|
|
||||||
async def get_user_placements(
|
async def get_workspace_placements(
|
||||||
self,
|
self,
|
||||||
user_id: UUID,
|
workspace_id: UUID,
|
||||||
target_channel_id: UUID | None = None,
|
target_channel_id: UUID | None = None,
|
||||||
external_channel_id: UUID | None = None,
|
external_channel_id: UUID | None = None,
|
||||||
creative_id: UUID | None = None,
|
creative_id: UUID | None = None,
|
||||||
@@ -181,6 +207,13 @@ class Usecase:
|
|||||||
telegram_bot: TelegramBotWriter
|
telegram_bot: TelegramBotWriter
|
||||||
jwt_encoder: JWTEncoder
|
jwt_encoder: JWTEncoder
|
||||||
|
|
||||||
|
async def ensure_workspace_access(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace:
|
||||||
|
workspace = await self.database.get_workspace_for_user(workspace_id, user_id)
|
||||||
|
if not workspace:
|
||||||
|
raise domain.WorkspaceNotFound(workspace_id)
|
||||||
|
|
||||||
|
return workspace
|
||||||
|
|
||||||
validate_login_token = validate_login_token
|
validate_login_token = validate_login_token
|
||||||
telegram_login = telegram_login
|
telegram_login = telegram_login
|
||||||
telegram_start = telegram_start
|
telegram_start = telegram_start
|
||||||
@@ -201,6 +234,7 @@ class Usecase:
|
|||||||
delete_creative = delete_creative
|
delete_creative = delete_creative
|
||||||
start_creative_creation = start_creative_creation
|
start_creative_creation = start_creative_creation
|
||||||
handle_creative_callback = handle_creative_callback
|
handle_creative_callback = handle_creative_callback
|
||||||
|
handle_creative_workspace_callback = handle_creative_workspace_callback
|
||||||
handle_creative_text_input = handle_creative_text_input
|
handle_creative_text_input = handle_creative_text_input
|
||||||
get_placements = get_placements
|
get_placements = get_placements
|
||||||
get_placement = get_placement
|
get_placement = get_placement
|
||||||
@@ -210,8 +244,11 @@ class Usecase:
|
|||||||
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
|
||||||
update_views_manually = update_views_manually
|
|
||||||
get_placements_analytics = get_placements_analytics
|
get_placements_analytics = get_placements_analytics
|
||||||
get_creatives_analytics = get_creatives_analytics
|
get_creatives_analytics = get_creatives_analytics
|
||||||
get_external_channels_analytics = get_external_channels_analytics
|
get_external_channels_analytics = get_external_channels_analytics
|
||||||
get_spending_analytics = get_spending_analytics
|
get_spending_analytics = get_spending_analytics
|
||||||
|
get_workspaces = get_workspaces
|
||||||
|
create_workspace = create_workspace
|
||||||
|
update_workspace = update_workspace
|
||||||
|
delete_workspace = delete_workspace
|
||||||
|
|||||||
@@ -14,13 +14,19 @@ log = logging.getLogger(__name__)
|
|||||||
async def get_creatives_analytics(
|
async def get_creatives_analytics(
|
||||||
self: 'Usecase', input: dto.GetCreativesAnalyticsInput
|
self: 'Usecase', input: dto.GetCreativesAnalyticsInput
|
||||||
) -> dto.GetCreativesAnalyticsOutput:
|
) -> dto.GetCreativesAnalyticsOutput:
|
||||||
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
if input.target_channel_id:
|
if input.target_channel_id:
|
||||||
target_channel = await self.database.get_target_channel(input.user_id, input.target_channel_id)
|
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
||||||
if not target_channel:
|
if not target_channel:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||||
|
|
||||||
creatives = await self.database.get_user_creatives(input.user_id, input.target_channel_id, include_archived=False)
|
creatives = await self.database.get_workspace_creatives(
|
||||||
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
|
input.workspace_id, input.target_channel_id, include_archived=False
|
||||||
|
)
|
||||||
|
placements = await self.database.get_workspace_placements(
|
||||||
|
input.workspace_id, input.target_channel_id, include_archived=False
|
||||||
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class CreativeStats:
|
class CreativeStats:
|
||||||
|
|||||||
@@ -14,15 +14,19 @@ log = logging.getLogger(__name__)
|
|||||||
async def get_external_channels_analytics(
|
async def get_external_channels_analytics(
|
||||||
self: 'Usecase', input: dto.GetExternalChannelsAnalyticsInput
|
self: 'Usecase', input: dto.GetExternalChannelsAnalyticsInput
|
||||||
) -> dto.GetExternalChannelsAnalyticsOutput:
|
) -> dto.GetExternalChannelsAnalyticsOutput:
|
||||||
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
if input.target_channel_id:
|
if input.target_channel_id:
|
||||||
target_channel = await self.database.get_target_channel(input.user_id, input.target_channel_id)
|
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
||||||
if not target_channel:
|
if not target_channel:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||||
external_channels = await self.database.get_external_channels_for_target(input.target_channel_id)
|
external_channels = await self.database.get_external_channels_for_target(input.target_channel_id)
|
||||||
else:
|
else:
|
||||||
external_channels = await self.database.get_user_external_channels(input.user_id)
|
external_channels = await self.database.get_workspace_external_channels(input.workspace_id)
|
||||||
|
|
||||||
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
|
placements = await self.database.get_workspace_placements(
|
||||||
|
input.workspace_id, input.target_channel_id, include_archived=False
|
||||||
|
)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ChannelStats:
|
class ChannelStats:
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ def _calculate_cpm(cost: float | None, views: int | None) -> float | None:
|
|||||||
async def get_placements_analytics(
|
async def get_placements_analytics(
|
||||||
self: 'Usecase', input: dto.GetPlacementsAnalyticsInput
|
self: 'Usecase', input: dto.GetPlacementsAnalyticsInput
|
||||||
) -> dto.GetPlacementsAnalyticsOutput:
|
) -> dto.GetPlacementsAnalyticsOutput:
|
||||||
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
if input.target_channel_id:
|
if input.target_channel_id:
|
||||||
target_channel = await self.database.get_target_channel(input.user_id, input.target_channel_id)
|
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
||||||
if not target_channel:
|
if not target_channel:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||||
|
|
||||||
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
|
placements = await self.database.get_workspace_placements(
|
||||||
|
input.workspace_id, input.target_channel_id, include_archived=False
|
||||||
|
)
|
||||||
|
|
||||||
result = [
|
result = [
|
||||||
dto.PlacementAnalyticsOutput(
|
dto.PlacementAnalyticsOutput(
|
||||||
|
|||||||
@@ -33,12 +33,16 @@ def _format_period(dt: 'datetime.datetime', grouping: dto.DateGrouping) -> str:
|
|||||||
async def get_spending_analytics(
|
async def get_spending_analytics(
|
||||||
self: 'Usecase', input: dto.GetSpendingAnalyticsInput
|
self: 'Usecase', input: dto.GetSpendingAnalyticsInput
|
||||||
) -> dto.GetSpendingAnalyticsOutput:
|
) -> dto.GetSpendingAnalyticsOutput:
|
||||||
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
if input.target_channel_id:
|
if input.target_channel_id:
|
||||||
target_channel = await self.database.get_target_channel(input.user_id, input.target_channel_id)
|
target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id)
|
||||||
if not target_channel:
|
if not target_channel:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||||
|
|
||||||
placements = await self.database.get_user_placements(input.user_id, input.target_channel_id, include_archived=False)
|
placements = await self.database.get_workspace_placements(
|
||||||
|
input.workspace_id, input.target_channel_id, include_archived=False
|
||||||
|
)
|
||||||
|
|
||||||
filtered = [
|
filtered = [
|
||||||
p
|
p
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def create_creative(self: 'Usecase', input: dto.CreateCreativeInput, user_id: uuid.UUID) -> dto.CreativeOutput:
|
async def create_creative(
|
||||||
target_channel = await self.database.get_target_channel(user_id, channel_id=input.target_channel_id)
|
self: 'Usecase', input: dto.CreateCreativeInput, user_id: uuid.UUID, workspace_id: uuid.UUID
|
||||||
|
) -> dto.CreativeOutput:
|
||||||
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
target_channel = await self.database.get_target_channel(workspace_id, channel_id=input.target_channel_id)
|
||||||
if not target_channel:
|
if not target_channel:
|
||||||
log.warning('User %s attempted to create creative for unavailable target %s', user_id, input.target_channel_id)
|
log.warning('User %s attempted to create creative for unavailable target %s', user_id, input.target_channel_id)
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||||
@@ -21,9 +25,8 @@ async def create_creative(self: 'Usecase', input: dto.CreateCreativeInput, user_
|
|||||||
text=input.text,
|
text=input.text,
|
||||||
status=domain.CreativeStatus.ACTIVE,
|
status=domain.CreativeStatus.ACTIVE,
|
||||||
target_channel_id=target_channel.id,
|
target_channel_id=target_channel.id,
|
||||||
user_id=user_id,
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.database.create_creative(creative)
|
await self.database.create_creative(creative)
|
||||||
|
|
||||||
return dto.CreativeOutput(
|
return dto.CreativeOutput(
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> None:
|
async def delete_creative(self: 'Usecase', input: dto.DeleteCreativeInput) -> None:
|
||||||
creative = await self.database.get_creative(input.user_id, input.creative_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
creative = await self.database.get_creative(input.workspace_id, input.creative_id)
|
||||||
if not creative:
|
if not creative:
|
||||||
log.warning('User %s attempted to delete unavailable creative %s', input.user_id, input.creative_id)
|
log.warning('User %s attempted to delete unavailable creative %s', input.user_id, input.creative_id)
|
||||||
raise domain.CreativeNotFound(input.creative_id)
|
raise domain.CreativeNotFound(input.creative_id)
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.CreativeOutput:
|
async def get_creative(self: 'Usecase', input: dto.GetCreativeInput) -> dto.CreativeOutput:
|
||||||
creative = await self.database.get_creative(input.user_id, input.creative_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
creative = await self.database.get_creative(input.workspace_id, input.creative_id)
|
||||||
if not creative:
|
if not creative:
|
||||||
log.warning('User %s attempted to access unavailable creative %s', input.user_id, input.creative_id)
|
|
||||||
raise domain.CreativeNotFound(input.creative_id)
|
raise domain.CreativeNotFound(input.creative_id)
|
||||||
|
|
||||||
return dto.CreativeOutput(
|
return dto.CreativeOutput(
|
||||||
|
|||||||
@@ -7,7 +7,11 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.GetCreativesOutput:
|
async def get_creatives(self: 'Usecase', input: dto.GetCreativesInput) -> dto.GetCreativesOutput:
|
||||||
creatives = await self.database.get_user_creatives(input.user_id, input.target_channel_id, input.include_archived)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
creatives = await self.database.get_workspace_creatives(
|
||||||
|
input.workspace_id, input.target_channel_id, input.include_archived
|
||||||
|
)
|
||||||
|
|
||||||
return dto.GetCreativesOutput(
|
return dto.GetCreativesOutput(
|
||||||
creatives=[
|
creatives=[
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def update_creative(
|
async def update_creative(
|
||||||
self: 'Usecase', creative_id: uuid.UUID, input: dto.UpdateCreativeInput, user_id: uuid.UUID
|
self: 'Usecase',
|
||||||
|
creative_id: uuid.UUID,
|
||||||
|
input: dto.UpdateCreativeInput,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
) -> dto.CreativeOutput:
|
) -> dto.CreativeOutput:
|
||||||
creative = await self.database.get_creative(user_id, creative_id)
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
creative = await self.database.get_creative(workspace_id, creative_id)
|
||||||
if not creative:
|
if not creative:
|
||||||
log.warning('User %s attempted to update unavailable creative %s', user_id, creative_id)
|
log.warning('User %s attempted to update unavailable creative %s', user_id, creative_id)
|
||||||
raise domain.CreativeNotFound(creative_id)
|
raise domain.CreativeNotFound(creative_id)
|
||||||
|
|||||||
@@ -11,17 +11,18 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def create_external_channel(
|
async def create_external_channel(
|
||||||
self: 'Usecase', input: dto.CreateExternalChannelInput, user_id: uuid.UUID
|
self: 'Usecase', input: dto.CreateExternalChannelInput, user_id: uuid.UUID, workspace_id: uuid.UUID
|
||||||
) -> dto.ExternalChannelOutput:
|
) -> dto.ExternalChannelOutput:
|
||||||
existing_channel = await self.database.get_external_channel(user_id, telegram_id=input.telegram_id)
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
existing_channel = await self.database.get_external_channel(workspace_id, telegram_id=input.telegram_id)
|
||||||
if existing_channel:
|
if existing_channel:
|
||||||
raise domain.ExternalChannelAlreadyExists(input.telegram_id)
|
raise domain.ExternalChannelAlreadyExists(input.telegram_id)
|
||||||
|
|
||||||
for target_id in input.target_channel_ids:
|
for target_id in input.target_channel_ids:
|
||||||
target_channel = await self.database.get_target_channel(user_id, channel_id=target_id)
|
target_channel = await self.database.get_target_channel(workspace_id, channel_id=target_id)
|
||||||
|
|
||||||
if not target_channel:
|
if not target_channel:
|
||||||
log.warning('User %s attempted to add external channel to unavailable target %s', user_id, target_id)
|
|
||||||
raise domain.TargetChannelNotFound(target_id)
|
raise domain.TargetChannelNotFound(target_id)
|
||||||
|
|
||||||
async with self.database.transaction():
|
async with self.database.transaction():
|
||||||
@@ -31,7 +32,7 @@ async def create_external_channel(
|
|||||||
username=input.username,
|
username=input.username,
|
||||||
description=input.description,
|
description=input.description,
|
||||||
subscribers_count=input.subscribers_count,
|
subscribers_count=input.subscribers_count,
|
||||||
user_id=user_id,
|
workspace_id=workspace_id,
|
||||||
)
|
)
|
||||||
await self.database.create_external_channel(channel)
|
await self.database.create_external_channel(channel)
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def delete_external_channel(self: 'Usecase', input: dto.DeleteExternalChannelInput) -> None:
|
async def delete_external_channel(self: 'Usecase', input: dto.DeleteExternalChannelInput) -> None:
|
||||||
channel = await self.database.get_external_channel(input.user_id, channel_id=input.channel_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
channel = await self.database.get_external_channel(input.workspace_id, channel_id=input.channel_id)
|
||||||
if not channel:
|
if not channel:
|
||||||
log.warning('User %s attempted to delete unavailable external channel %s', input.user_id, input.channel_id)
|
|
||||||
raise domain.ExternalChannelNotFound(input.channel_id)
|
raise domain.ExternalChannelNotFound(input.channel_id)
|
||||||
|
|
||||||
await self.database.delete_external_channel(input.channel_id)
|
await self.database.delete_external_channel(input.channel_id)
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def get_external_channels(self: 'Usecase', input: dto.GetExternalChannelsInput) -> dto.GetExternalChannelsOutput:
|
async def get_external_channels(self: 'Usecase', input: dto.GetExternalChannelsInput) -> dto.GetExternalChannelsOutput:
|
||||||
target_channel = await self.database.get_target_channel(input.user_id, channel_id=input.target_channel_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
target_channel = await self.database.get_target_channel(input.workspace_id, channel_id=input.target_channel_id)
|
||||||
if not target_channel:
|
if not target_channel:
|
||||||
log.warning('Target channel %s not found or not accessible for user %s', input.target_channel_id, input.user_id)
|
log.warning('Target channel %s not found or not accessible for user %s', input.target_channel_id, input.user_id)
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def update_external_channel(
|
async def update_external_channel(
|
||||||
self: 'Usecase', channel_id: uuid.UUID, input: dto.UpdateExternalChannelInput, user_id: uuid.UUID
|
self: 'Usecase',
|
||||||
|
channel_id: uuid.UUID,
|
||||||
|
input: dto.UpdateExternalChannelInput,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
) -> dto.ExternalChannelOutput:
|
) -> dto.ExternalChannelOutput:
|
||||||
external_channel = await self.database.get_external_channel(user_id, channel_id=channel_id)
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
external_channel = await self.database.get_external_channel(workspace_id, channel_id=channel_id)
|
||||||
if not external_channel:
|
if not external_channel:
|
||||||
log.warning('External channel %s not found', channel_id)
|
log.warning('External channel %s not found', channel_id)
|
||||||
raise domain.ExternalChannelNotFound(channel_id)
|
raise domain.ExternalChannelNotFound(channel_id)
|
||||||
|
|||||||
@@ -11,22 +11,28 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def update_external_channel_links(
|
async def update_external_channel_links(
|
||||||
self: 'Usecase', channel_id: uuid.UUID, input: dto.UpdateExternalChannelLinksInput, user_id: uuid.UUID
|
self: 'Usecase',
|
||||||
|
channel_id: uuid.UUID,
|
||||||
|
input: dto.UpdateExternalChannelLinksInput,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
) -> dto.ExternalChannelOutput:
|
) -> dto.ExternalChannelOutput:
|
||||||
external_channel = await self.database.get_external_channel(user_id, channel_id=channel_id)
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
external_channel = await self.database.get_external_channel(workspace_id, channel_id=channel_id)
|
||||||
if not external_channel:
|
if not external_channel:
|
||||||
log.warning('External channel %s not found', channel_id)
|
log.warning('External channel %s not found', channel_id)
|
||||||
raise domain.ExternalChannelNotFound(channel_id)
|
raise domain.ExternalChannelNotFound(channel_id)
|
||||||
|
|
||||||
for target_id in input.add_target_channel_ids:
|
for target_id in input.add_target_channel_ids:
|
||||||
target_channel = await self.database.get_target_channel(user_id, channel_id=target_id)
|
target_channel = await self.database.get_target_channel(workspace_id, channel_id=target_id)
|
||||||
if not target_channel:
|
if not target_channel:
|
||||||
log.warning('User %s attempted to link external channel to target %s they do not own', user_id, target_id)
|
log.warning('User %s attempted to link external channel to target %s they do not own', user_id, target_id)
|
||||||
raise domain.TargetChannelNotFound(target_id)
|
raise domain.TargetChannelNotFound(target_id)
|
||||||
|
|
||||||
async with self.database.transaction():
|
async with self.database.transaction():
|
||||||
if input.add_target_channel_ids:
|
if input.add_target_channel_ids:
|
||||||
await self.database.add_external_channel_to_targets(channel_id, input.add_target_channel_ids)
|
await self.database.add_external_channel_to_targets(external_channel, input.add_target_channel_ids)
|
||||||
|
|
||||||
for target_id in input.remove_target_channel_ids:
|
for target_id in input.remove_target_channel_ids:
|
||||||
await self.database.remove_external_channel_from_target(channel_id, target_id)
|
await self.database.remove_external_channel_from_target(channel_id, target_id)
|
||||||
|
|||||||
@@ -10,16 +10,20 @@ if TYPE_CHECKING:
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def create_placement(self: 'Usecase', input: dto.CreatePlacementInput, user_id: uuid.UUID) -> dto.PlacementOutput:
|
async def create_placement(
|
||||||
target_channel = await self.database.get_target_channel(user_id, channel_id=input.target_channel_id)
|
self: 'Usecase', input: dto.CreatePlacementInput, user_id: uuid.UUID, workspace_id: uuid.UUID
|
||||||
|
) -> dto.PlacementOutput:
|
||||||
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
target_channel = await self.database.get_target_channel(workspace_id, channel_id=input.target_channel_id)
|
||||||
if not target_channel:
|
if not target_channel:
|
||||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||||
|
|
||||||
external_channel = await self.database.get_external_channel(user_id, channel_id=input.external_channel_id)
|
external_channel = await self.database.get_external_channel(workspace_id, channel_id=input.external_channel_id)
|
||||||
if not external_channel:
|
if not external_channel:
|
||||||
raise domain.ExternalChannelNotFound(input.external_channel_id)
|
raise domain.ExternalChannelNotFound(input.external_channel_id)
|
||||||
|
|
||||||
creative = await self.database.get_creative(user_id, input.creative_id)
|
creative = await self.database.get_creative(workspace_id, input.creative_id)
|
||||||
if not creative:
|
if not creative:
|
||||||
raise domain.CreativeNotFound(input.creative_id)
|
raise domain.CreativeNotFound(input.creative_id)
|
||||||
|
|
||||||
@@ -30,7 +34,7 @@ async def create_placement(self: 'Usecase', input: dto.CreatePlacementInput, use
|
|||||||
target_channel_id=input.target_channel_id,
|
target_channel_id=input.target_channel_id,
|
||||||
external_channel_id=input.external_channel_id,
|
external_channel_id=input.external_channel_id,
|
||||||
creative_id=input.creative_id,
|
creative_id=input.creative_id,
|
||||||
user_id=user_id,
|
workspace_id=workspace_id,
|
||||||
placement_date=input.placement_date,
|
placement_date=input.placement_date,
|
||||||
cost=input.cost,
|
cost=input.cost,
|
||||||
comment=input.comment,
|
comment=input.comment,
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def delete_placement(self: 'Usecase', input: dto.DeletePlacementInput) -> None:
|
async def delete_placement(self: 'Usecase', input: dto.DeletePlacementInput) -> None:
|
||||||
placement = await self.database.get_placement(input.user_id, input.placement_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
|
||||||
if not placement:
|
if not placement:
|
||||||
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
||||||
raise domain.PlacementNotFound(input.placement_id)
|
raise domain.PlacementNotFound(input.placement_id)
|
||||||
@@ -20,7 +22,7 @@ async def delete_placement(self: 'Usecase', input: dto.DeletePlacementInput) ->
|
|||||||
return
|
return
|
||||||
|
|
||||||
async with self.database.transaction():
|
async with self.database.transaction():
|
||||||
creative = await self.database.get_creative(input.user_id, placement.creative_id)
|
creative = await self.database.get_creative(input.workspace_id, placement.creative_id)
|
||||||
if creative and creative.placements_count > 0:
|
if creative and creative.placements_count > 0:
|
||||||
creative.placements_count -= 1
|
creative.placements_count -= 1
|
||||||
await self.database.update_creative(creative)
|
await self.database.update_creative(creative)
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.PlacementOutput:
|
async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.PlacementOutput:
|
||||||
placement = await self.database.get_placement(input.user_id, input.placement_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
|
||||||
if not placement:
|
if not placement:
|
||||||
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
||||||
raise domain.PlacementNotFound(input.placement_id)
|
raise domain.PlacementNotFound(input.placement_id)
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.GetPlacementsOutput:
|
async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto.GetPlacementsOutput:
|
||||||
placements = await self.database.get_user_placements(
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
input.user_id,
|
|
||||||
|
placements = await self.database.get_workspace_placements(
|
||||||
|
input.workspace_id,
|
||||||
target_channel_id=input.target_channel_id,
|
target_channel_id=input.target_channel_id,
|
||||||
external_channel_id=input.external_channel_id,
|
external_channel_id=input.external_channel_id,
|
||||||
creative_id=input.creative_id,
|
creative_id=input.creative_id,
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def update_placement(
|
async def update_placement(
|
||||||
self: 'Usecase', placement_id: uuid.UUID, input: dto.UpdatePlacementInput, user_id: uuid.UUID
|
self: 'Usecase',
|
||||||
|
placement_id: uuid.UUID,
|
||||||
|
input: dto.UpdatePlacementInput,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
) -> dto.PlacementOutput:
|
) -> dto.PlacementOutput:
|
||||||
placement = await self.database.get_placement(user_id, placement_id)
|
await self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
placement = await self.database.get_placement(workspace_id, placement_id)
|
||||||
if not placement:
|
if not placement:
|
||||||
log.warning('Placement %s not found for user %s', placement_id, user_id)
|
log.warning('Placement %s not found for user %s', placement_id, user_id)
|
||||||
raise domain.PlacementNotFound(placement_id)
|
raise domain.PlacementNotFound(placement_id)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ async def disconnect_target_chan_by_tg_id(self: 'Usecase', input: dto.Disconnect
|
|||||||
log.warning(f'User with telegram_id {input.user_telegram_id} not found when disconnecting channel')
|
log.warning(f'User with telegram_id {input.user_telegram_id} not found when disconnecting channel')
|
||||||
return
|
return
|
||||||
|
|
||||||
channel = await self.database.get_target_channel(user.id, telegram_id=input.telegram_id)
|
channel = await self.database.get_target_channel_for_user_by_telegram(user.id, input.telegram_id)
|
||||||
if not channel:
|
if not channel:
|
||||||
log.warning(f'Target channel {input.telegram_id} not found')
|
log.warning(f'Target channel {input.telegram_id} not found')
|
||||||
raise domain.TargetChannelNotFound()
|
raise domain.TargetChannelNotFound()
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
async def get_user_target_chans(self: 'Usecase', input: dto.GetUserTargetChansInput) -> dto.GetUserTargetChansOutput:
|
async def get_user_target_chans(self: 'Usecase', input: dto.GetUserTargetChansInput) -> dto.GetUserTargetChansOutput:
|
||||||
channels = await self.database.get_user_target_channels(input.user_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
channels = await self.database.get_workspace_target_channels(input.workspace_id)
|
||||||
|
|
||||||
return dto.GetUserTargetChansOutput(
|
return dto.GetUserTargetChansOutput(
|
||||||
target_channels=[
|
target_channels=[
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ async def update_target_chan_permissions(self: 'Usecase', input: dto.UpdateTarge
|
|||||||
log.warning(f'User with telegram_id {input.user_telegram_id} not found when updating channel permissions')
|
log.warning(f'User with telegram_id {input.user_telegram_id} not found when updating channel permissions')
|
||||||
return
|
return
|
||||||
|
|
||||||
channel = await self.database.get_target_channel(user.id, telegram_id=input.telegram_id)
|
channel = await self.database.get_target_channel_for_user_by_telegram(user.id, input.telegram_id)
|
||||||
if not channel:
|
if not channel:
|
||||||
log.warning(f'Target channel {input.telegram_id} not found when permissions changed')
|
log.warning(f'Target channel {input.telegram_id} not found when permissions changed')
|
||||||
raise domain.TargetChannelNotFound()
|
raise domain.TargetChannelNotFound()
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> dto.GetViewsHistoryOutput:
|
async def get_views_history(self: 'Usecase', input: dto.GetViewsHistoryInput) -> dto.GetViewsHistoryOutput:
|
||||||
placement = await self.database.get_placement(input.user_id, input.placement_id)
|
await self.ensure_workspace_access(input.workspace_id, input.user_id)
|
||||||
|
|
||||||
|
placement = await self.database.get_placement(input.workspace_id, input.placement_id)
|
||||||
if not placement:
|
if not placement:
|
||||||
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
||||||
raise domain.PlacementNotFound(input.placement_id)
|
raise domain.PlacementNotFound(input.placement_id)
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import logging
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from tortoise import timezone
|
|
||||||
|
|
||||||
from src import domain, dto
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .. import Usecase
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def update_views_manually(self: 'Usecase', input: dto.UpdateViewsManuallyInput) -> dto.PlacementOutput:
|
|
||||||
placement = await self.database.get_placement(input.user_id, input.placement_id)
|
|
||||||
if not placement:
|
|
||||||
log.warning('Placement %s not found for user %s', input.placement_id, input.user_id)
|
|
||||||
raise domain.PlacementNotFound(input.placement_id)
|
|
||||||
|
|
||||||
fetched_at = timezone.now()
|
|
||||||
|
|
||||||
async with self.database.transaction():
|
|
||||||
placement.views_count = input.views_count
|
|
||||||
placement.views_availability = domain.PostViewsAvailability.MANUAL
|
|
||||||
placement.last_views_fetch_at = fetched_at
|
|
||||||
await self.database.update_placement(placement)
|
|
||||||
|
|
||||||
history = domain.PlacementViewsHistory(
|
|
||||||
placement_id=placement.id,
|
|
||||||
views_count=input.views_count,
|
|
||||||
fetched_at=fetched_at,
|
|
||||||
)
|
|
||||||
await self.database.create_placement_views_history(history)
|
|
||||||
|
|
||||||
log.info('Views manually updated for placement %s: %d', placement.id, input.views_count)
|
|
||||||
|
|
||||||
return dto.PlacementOutput(
|
|
||||||
id=placement.id,
|
|
||||||
target_channel_id=placement.target_channel_id,
|
|
||||||
target_channel_title=placement.target_channel.title,
|
|
||||||
external_channel_id=placement.external_channel_id,
|
|
||||||
external_channel_title=placement.external_channel.title,
|
|
||||||
creative_id=placement.creative_id,
|
|
||||||
creative_name=placement.creative.name,
|
|
||||||
placement_date=placement.placement_date,
|
|
||||||
cost=placement.cost,
|
|
||||||
comment=placement.comment,
|
|
||||||
ad_post_url=placement.ad_post_url,
|
|
||||||
invite_link_type=placement.invite_link_type,
|
|
||||||
invite_link=placement.invite_link,
|
|
||||||
status=placement.status,
|
|
||||||
subscriptions_count=placement.subscriptions_count,
|
|
||||||
views_count=placement.views_count,
|
|
||||||
views_availability=placement.views_availability,
|
|
||||||
last_views_fetch_at=placement.last_views_fetch_at,
|
|
||||||
created_at=placement.created_at,
|
|
||||||
)
|
|
||||||
28
src/usecase/workspace/create_workspace.py
Normal file
28
src/usecase/workspace/create_workspace.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain, dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
|
||||||
|
async def create_workspace(
|
||||||
|
self: 'Usecase', user_id: uuid.UUID, input: dto.CreateWorkspaceInput
|
||||||
|
) -> dto.CreateWorkspaceOutput:
|
||||||
|
user = await self.database.get_user(user_id=user_id)
|
||||||
|
if not user:
|
||||||
|
raise domain.UserNotFound(user_id)
|
||||||
|
|
||||||
|
workspace = domain.Workspace(
|
||||||
|
name=input.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
async with self.database.transaction():
|
||||||
|
await self.database.create_workspace(workspace)
|
||||||
|
await self.database.add_user_to_workspace(workspace.id, user_id)
|
||||||
|
|
||||||
|
return dto.CreateWorkspaceOutput(
|
||||||
|
id=workspace.id,
|
||||||
|
name=workspace.name,
|
||||||
|
)
|
||||||
11
src/usecase/workspace/delete_workspace.py
Normal file
11
src/usecase/workspace/delete_workspace.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_workspace(self: 'Usecase', workspace_id: uuid.UUID, user_id: uuid.UUID) -> None:
|
||||||
|
self.ensure_workspace_access(workspace_id, user_id)
|
||||||
|
|
||||||
|
await self.database.delete_workspace(workspace_id)
|
||||||
21
src/usecase/workspace/get_workspaces.py
Normal file
21
src/usecase/workspace/get_workspaces.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
|
||||||
|
async def get_workspaces(self: 'Usecase', user_id: uuid.UUID) -> dto.GetWorkspacesOutput:
|
||||||
|
memberships = await self.database.get_user_workspaces(user_id)
|
||||||
|
|
||||||
|
return dto.GetWorkspacesOutput(
|
||||||
|
workspaces=[
|
||||||
|
dto.WorkspaceMembershipOutput(
|
||||||
|
id=membership.workspace_id,
|
||||||
|
name=membership.workspace.name,
|
||||||
|
)
|
||||||
|
for membership in memberships
|
||||||
|
]
|
||||||
|
)
|
||||||
26
src/usecase/workspace/update_workspace.py
Normal file
26
src/usecase/workspace/update_workspace.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain, dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
|
||||||
|
async def update_workspace(
|
||||||
|
self: 'Usecase', workspace_id: uuid.UUID, user_id: uuid.UUID, input: dto.UpdateWorkspaceInput
|
||||||
|
) -> dto.WorkspaceMembershipOutput:
|
||||||
|
membership = await self.database.get_workspace_membership(workspace_id, user_id)
|
||||||
|
if not membership:
|
||||||
|
raise domain.WorkspaceNotFound(workspace_id)
|
||||||
|
|
||||||
|
workspace = membership.workspace
|
||||||
|
|
||||||
|
if input.name is not None:
|
||||||
|
workspace.name = input.name
|
||||||
|
await self.database.update_workspace(workspace)
|
||||||
|
|
||||||
|
return dto.WorkspaceMembershipOutput(
|
||||||
|
id=workspace.id,
|
||||||
|
name=workspace.name,
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user