feat: delete & archive projects
This commit is contained in:
@@ -246,7 +246,7 @@ class Postgres(DatabaseBase, Database):
|
|||||||
async def get_project(
|
async def get_project(
|
||||||
self, workspace_id: uuid.UUID, project_id: uuid.UUID | None = None, channel_id: uuid.UUID | None = None
|
self, workspace_id: uuid.UUID, project_id: uuid.UUID | None = None, channel_id: uuid.UUID | None = None
|
||||||
) -> domain.Project | None:
|
) -> domain.Project | None:
|
||||||
query = domain.Project.filter(workspace_id=workspace_id).prefetch_related('channel')
|
query = domain.Project.filter(workspace_id=workspace_id, deleted_at__isnull=True).prefetch_related('channel')
|
||||||
if project_id:
|
if project_id:
|
||||||
query = query.filter(id=project_id)
|
query = query.filter(id=project_id)
|
||||||
if channel_id:
|
if channel_id:
|
||||||
@@ -274,17 +274,39 @@ class Postgres(DatabaseBase, Database):
|
|||||||
await project.save()
|
await project.save()
|
||||||
|
|
||||||
async def get_workspace_projects(
|
async def get_workspace_projects(
|
||||||
self, workspace_id: uuid.UUID, allowed_project_ids: set[uuid.UUID] | None = None
|
self,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
allowed_project_ids: set[uuid.UUID] | None = None,
|
||||||
|
include_archived: bool = False,
|
||||||
) -> list[domain.Project]:
|
) -> list[domain.Project]:
|
||||||
query = domain.Project.filter(workspace_id=workspace_id).prefetch_related('channel')
|
query = domain.Project.filter(workspace_id=workspace_id, deleted_at__isnull=True).prefetch_related('channel')
|
||||||
|
|
||||||
if allowed_project_ids is not None:
|
if allowed_project_ids is not None:
|
||||||
if not allowed_project_ids:
|
if not allowed_project_ids:
|
||||||
return []
|
return []
|
||||||
query = query.filter(id__in=list(allowed_project_ids))
|
query = query.filter(id__in=list(allowed_project_ids))
|
||||||
|
|
||||||
|
if not include_archived:
|
||||||
|
query = query.filter(status=domain.ProjectStatus.ACTIVE)
|
||||||
|
|
||||||
return await query.order_by('created_at').all()
|
return await query.order_by('created_at').all()
|
||||||
|
|
||||||
|
async def archive_project(self, workspace_id: uuid.UUID, project_id: uuid.UUID) -> None:
|
||||||
|
project = await domain.Project.get_or_none(id=project_id, workspace_id=workspace_id)
|
||||||
|
if not project:
|
||||||
|
raise domain.ProjectNotFound()
|
||||||
|
project.status = domain.ProjectStatus.ARCHIVED
|
||||||
|
await project.save()
|
||||||
|
|
||||||
|
async def delete_project(self, workspace_id: uuid.UUID, project_id: uuid.UUID) -> None:
|
||||||
|
project = await domain.Project.get_or_none(id=project_id, workspace_id=workspace_id)
|
||||||
|
if not project:
|
||||||
|
raise domain.ProjectNotFound()
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
project.deleted_at = datetime.datetime.now()
|
||||||
|
await project.save()
|
||||||
|
|
||||||
async def get_active_purchase(self, project_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Purchase | None:
|
async def get_active_purchase(self, project_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Purchase | None:
|
||||||
return await domain.Purchase.get_or_none(
|
return await domain.Purchase.get_or_none(
|
||||||
project_id=project_id, creative_id=creative_id, status=domain.PurchaseStatus.ACTIVE
|
project_id=project_id, creative_id=creative_id, status=domain.PurchaseStatus.ACTIVE
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends, Query
|
||||||
from fastapi.routing import APIRouter
|
from fastapi.routing import APIRouter
|
||||||
from fastapi_pagination import Page, paginate
|
from fastapi_pagination import Page, paginate
|
||||||
|
|
||||||
@@ -13,11 +13,14 @@ projects_router = APIRouter(prefix='/workspaces/{workspace_id}/projects', tags=[
|
|||||||
|
|
||||||
@projects_router.get('')
|
@projects_router.get('')
|
||||||
async def get_projects(
|
async def get_projects(
|
||||||
workspace_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)]
|
workspace_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
include_archived: bool = Query(default=False),
|
||||||
) -> Page[dto.ProjectOutput]:
|
) -> Page[dto.ProjectOutput]:
|
||||||
input = dto.GetWorkspaceProjectsInput(
|
input = dto.GetWorkspaceProjectsInput(
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
workspace_id=workspace_id,
|
workspace_id=workspace_id,
|
||||||
|
include_archived=include_archived,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await deps.get_usecase().get_workspace_projects(input=input)
|
result = await deps.get_usecase().get_workspace_projects(input=input)
|
||||||
@@ -37,3 +40,30 @@ async def update_project_invite_link_type(
|
|||||||
purchase_invite_type_default=input.purchase_invite_type_default,
|
purchase_invite_type_default=input.purchase_invite_type_default,
|
||||||
user_id=current_user.user_id,
|
user_id=current_user.user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@projects_router.post('/{project_id}/archive')
|
||||||
|
async def archive_project(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> dto.ProjectOutput:
|
||||||
|
input = dto.ArchiveProjectInput(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
project_id=project_id,
|
||||||
|
user_id=current_user.user_id,
|
||||||
|
)
|
||||||
|
return await deps.get_usecase().archive_project(input=input)
|
||||||
|
|
||||||
|
|
||||||
|
@projects_router.delete('/{project_id}')
|
||||||
|
async def delete_project(
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> None:
|
||||||
|
await deps.get_usecase().delete_project(
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
project_id=project_id,
|
||||||
|
user_id=current_user.user_id,
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ __all__ = (
|
|||||||
'GetWorkspaceProjectsInput',
|
'GetWorkspaceProjectsInput',
|
||||||
'GetWorkspaceProjectsOutput',
|
'GetWorkspaceProjectsOutput',
|
||||||
'GetProjectInput',
|
'GetProjectInput',
|
||||||
|
'ArchiveProjectInput',
|
||||||
'DisconnectProjectByTgIdInput',
|
'DisconnectProjectByTgIdInput',
|
||||||
'ConnectProjectInput',
|
'ConnectProjectInput',
|
||||||
'ProjectOutput',
|
'ProjectOutput',
|
||||||
@@ -121,6 +122,7 @@ from .creative import (
|
|||||||
)
|
)
|
||||||
from .placement import GetPlacementInput, GetPlacementsInput, GetPlacementsOutput, PlacementOutput
|
from .placement import GetPlacementInput, GetPlacementsInput, GetPlacementsOutput, PlacementOutput
|
||||||
from .project import (
|
from .project import (
|
||||||
|
ArchiveProjectInput,
|
||||||
ChannelBotPermissions,
|
ChannelBotPermissions,
|
||||||
ConnectProjectInput,
|
ConnectProjectInput,
|
||||||
DisconnectProjectByTgIdInput,
|
DisconnectProjectByTgIdInput,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class UpdateProjectInviteLinkTypeInput(pydantic.BaseModel):
|
|||||||
class GetWorkspaceProjectsInput(pydantic.BaseModel):
|
class GetWorkspaceProjectsInput(pydantic.BaseModel):
|
||||||
user_id: uuid.UUID
|
user_id: uuid.UUID
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
|
include_archived: bool = False
|
||||||
|
|
||||||
|
|
||||||
class GetWorkspaceProjectsOutput(pydantic.BaseModel):
|
class GetWorkspaceProjectsOutput(pydantic.BaseModel):
|
||||||
@@ -63,3 +64,9 @@ class UpdateProjectPermissionsInput(pydantic.BaseModel):
|
|||||||
class GetProjectInput(pydantic.BaseModel):
|
class GetProjectInput(pydantic.BaseModel):
|
||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
project_id: uuid.UUID
|
project_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
|
class ArchiveProjectInput(pydantic.BaseModel):
|
||||||
|
workspace_id: uuid.UUID
|
||||||
|
project_id: uuid.UUID
|
||||||
|
user_id: uuid.UUID
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ from .creative.update_creative import update_creative
|
|||||||
from .placement.fetch_placement_post_cycle import fetch_placement_post_cycle
|
from .placement.fetch_placement_post_cycle import fetch_placement_post_cycle
|
||||||
from .placement.get_placement import get_placement
|
from .placement.get_placement import get_placement
|
||||||
from .placement.get_placements import get_placements
|
from .placement.get_placements import get_placements
|
||||||
|
from .project.archive_project import archive_project
|
||||||
|
from .project.delete_project import delete_project
|
||||||
from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id
|
from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id
|
||||||
from .project.get_project import get_project
|
from .project.get_project import get_project
|
||||||
from .project.get_workspace_projects import get_workspace_projects
|
from .project.get_workspace_projects import get_workspace_projects
|
||||||
@@ -158,9 +160,13 @@ class Database(typing.Protocol):
|
|||||||
async def update_project(self, project: domain.Project) -> None: ...
|
async def update_project(self, project: domain.Project) -> None: ...
|
||||||
|
|
||||||
async def get_workspace_projects(
|
async def get_workspace_projects(
|
||||||
self, workspace_id: UUID, allowed_project_ids: set[UUID] | None = None
|
self, workspace_id: UUID, allowed_project_ids: set[UUID] | None = None, include_archived: bool = False
|
||||||
) -> list[domain.Project]: ...
|
) -> list[domain.Project]: ...
|
||||||
|
|
||||||
|
async def archive_project(self, workspace_id: UUID, project_id: UUID) -> None: ...
|
||||||
|
|
||||||
|
async def delete_project(self, workspace_id: UUID, project_id: UUID) -> None: ...
|
||||||
|
|
||||||
async def get_active_purchase(self, project_id: UUID, creative_id: UUID) -> domain.Purchase | None: ...
|
async def get_active_purchase(self, project_id: UUID, creative_id: UUID) -> domain.Purchase | None: ...
|
||||||
|
|
||||||
async def get_purchase(self, workspace_id: UUID, purchase_id: UUID) -> domain.Purchase | None: ...
|
async def get_purchase(self, workspace_id: UUID, purchase_id: UUID) -> domain.Purchase | None: ...
|
||||||
@@ -415,6 +421,8 @@ class Usecase:
|
|||||||
tg_add_project = tg_add_project
|
tg_add_project = tg_add_project
|
||||||
get_workspace_projects = get_workspace_projects
|
get_workspace_projects = get_workspace_projects
|
||||||
get_project = get_project
|
get_project = get_project
|
||||||
|
archive_project = archive_project
|
||||||
|
delete_project = delete_project
|
||||||
disconnect_project_by_tg_id = disconnect_project_by_tg_id
|
disconnect_project_by_tg_id = disconnect_project_by_tg_id
|
||||||
update_project_permissions = update_project_permissions
|
update_project_permissions = update_project_permissions
|
||||||
update_project_invite_link_type = update_project_invite_link_type
|
update_project_invite_link_type = update_project_invite_link_type
|
||||||
|
|||||||
32
src/usecase/project/archive_project.py
Normal file
32
src/usecase/project/archive_project.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain, dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
|
||||||
|
async def archive_project(self: 'Usecase', input: dto.ArchiveProjectInput) -> dto.ProjectOutput:
|
||||||
|
await self.ensure_workspace_permission(
|
||||||
|
input.workspace_id, input.user_id, domain.PermissionKey.PROJECTS_WRITE, for_project_id=input.project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
async with self.database.transaction():
|
||||||
|
await self.database.archive_project(input.workspace_id, input.project_id)
|
||||||
|
project = await self.database.get_project(input.workspace_id, project_id=input.project_id)
|
||||||
|
|
||||||
|
if not project:
|
||||||
|
raise domain.ProjectNotFound()
|
||||||
|
|
||||||
|
await project.fetch_related('channel')
|
||||||
|
|
||||||
|
return dto.ProjectOutput(
|
||||||
|
id=project.id,
|
||||||
|
telegram_id=project.channel.telegram_id,
|
||||||
|
title=project.channel.title,
|
||||||
|
username=project.channel.username,
|
||||||
|
status=project.status,
|
||||||
|
purchase_invite_type_default=project.purchase_invite_type_default,
|
||||||
|
)
|
||||||
|
|
||||||
17
src/usecase/project/delete_project.py
Normal file
17
src/usecase/project/delete_project.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_project(self: 'Usecase', workspace_id: uuid.UUID, project_id: uuid.UUID, user_id: uuid.UUID) -> None:
|
||||||
|
await self.ensure_workspace_permission(
|
||||||
|
workspace_id, user_id, domain.PermissionKey.PROJECTS_WRITE, for_project_id=project_id
|
||||||
|
)
|
||||||
|
|
||||||
|
async with self.database.transaction():
|
||||||
|
await self.database.delete_project(workspace_id, project_id)
|
||||||
|
|
||||||
@@ -13,7 +13,9 @@ async def get_workspace_projects(self: 'Usecase', input: dto.GetWorkspaceProject
|
|||||||
|
|
||||||
allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.PROJECTS_READ)
|
allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.PROJECTS_READ)
|
||||||
|
|
||||||
projects = await self.database.get_workspace_projects(input.workspace_id, allowed_project_ids=allowed_project_ids)
|
projects = await self.database.get_workspace_projects(
|
||||||
|
input.workspace_id, allowed_project_ids=allowed_project_ids, include_archived=input.include_archived
|
||||||
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
dto.ProjectOutput(
|
dto.ProjectOutput(
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (s *MainMenu) Handle(b *bot.Bot, u *echotron.Update) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tgexAbout = `
|
const tgexAbout = `
|
||||||
TGEX — Telegram-бот для покупки и продажи рекламы без ЛС и ручных таблиц.
|
Smartpost — платформа для покупки и продажи рекламы без ЛС и ручных таблиц.
|
||||||
|
|
||||||
• Внутренний счёт и безопасные сделки
|
• Внутренний счёт и безопасные сделки
|
||||||
• Фиксация цен, дат и форматов
|
• Фиксация цен, дат и форматов
|
||||||
|
|||||||
Reference in New Issue
Block a user