diff --git a/src/adapter/postgres.py b/src/adapter/postgres.py index 3737dd8..1f39050 100644 --- a/src/adapter/postgres.py +++ b/src/adapter/postgres.py @@ -461,6 +461,37 @@ class Postgres(DatabaseBase): .all() ) + @staticmethod + async def get_placement_posts_by_placement_ids( + workspace_id: uuid.UUID, + placement_ids: list[uuid.UUID], + include_archived: bool = False, + ) -> list[domain.PlacementPost]: + if not placement_ids: + return [] + + query = domain.PlacementPost.filter( + placement__project__workspace_id=workspace_id, + placement_id__in=placement_ids, + ) + + if not include_archived: + query = query.filter(status=domain.PlacementPostStatus.ACTIVE) + + return ( + await query.prefetch_related( + 'placement', + 'placement__project', + 'placement__project__channel', + 'placement__channel', + 'placement__creative', + 'post', + 'post__channel', + ) + .order_by('-created_at') + .all() + ) + @staticmethod async def create_placement_post(placement_post: domain.PlacementPost) -> None: await placement_post.save() diff --git a/src/controller/http_v1/__init__.py b/src/controller/http_v1/__init__.py index 8be0075..bfa1ab8 100644 --- a/src/controller/http_v1/__init__.py +++ b/src/controller/http_v1/__init__.py @@ -5,7 +5,6 @@ from src.controller.http_v1.auth import auth_router from src.controller.http_v1.channels import channels_router from src.controller.http_v1.creatives import creatives_router from src.controller.http_v1.internal import internal_router -from src.controller.http_v1.placements import placement_posts_router from src.controller.http_v1.projects import projects_router from src.controller.http_v1.purchases import placements_user_router from src.controller.http_v1.views import views_router @@ -23,7 +22,6 @@ api_v1_router.include_router(channels_router) api_v1_router.include_router(projects_router) api_v1_router.include_router(placements_user_router) # User-managed placements api_v1_router.include_router(creatives_router) -api_v1_router.include_router(placement_posts_router) # System-managed placement_posts api_v1_router.include_router(views_router) api_v1_router.include_router(analytics_router) api_v1_router.include_router(workspaces_router) diff --git a/src/controller/http_v1/channels.py b/src/controller/http_v1/channels.py index 5e0f5ac..36a3f2b 100644 --- a/src/controller/http_v1/channels.py +++ b/src/controller/http_v1/channels.py @@ -1,3 +1,4 @@ +import uuid from typing import Annotated from fastapi import Depends @@ -12,8 +13,7 @@ channels_router = APIRouter(prefix='/channels', tags=['channels']) @channels_router.get('') async def get_channels( - _: Annotated[JWTPayload, Depends(deps.get_current_user)], - username: str | None = None, + _: Annotated[JWTPayload, Depends(deps.get_current_user)], username: str | None = None ) -> Page[dto.ChannelOutput]: input = dto.GetChannelsInput(username=username) result = await deps.get_usecase().get_channels(input=input) @@ -22,10 +22,7 @@ async def get_channels( @channels_router.get('/{channel_id}') async def get_channel( - channel_id: str, - _: Annotated[JWTPayload, Depends(deps.get_current_user)], + channel_id: uuid.UUID, _: Annotated[JWTPayload, Depends(deps.get_current_user)] ) -> dto.ChannelOutput: - import uuid - - input_data = dto.GetChannelInput(channel_id=uuid.UUID(channel_id)) + input_data = dto.GetChannelInput(channel_id=channel_id) return await deps.get_usecase().get_channel(input=input_data) diff --git a/src/controller/http_v1/internal.py b/src/controller/http_v1/internal.py index 31dd855..024d3f3 100644 --- a/src/controller/http_v1/internal.py +++ b/src/controller/http_v1/internal.py @@ -38,17 +38,6 @@ async def get_jwt_by_telegram_id( ) -@internal_router.get('/projects/{workspace_id}/{project_id}') -async def get_project(workspace_id: str, project_id: str) -> dto.ProjectOutput: - import uuid - - input_data = dto.GetProjectInput( - workspace_id=uuid.UUID(workspace_id), - project_id=uuid.UUID(project_id), - ) - return await deps.get_usecase().get_project(input_data) - - class AttachChannelToWorkspaceRequest(BaseModel): channel_id: str workspace_id: str diff --git a/src/controller/http_v1/placements.py b/src/controller/http_v1/placements.py deleted file mode 100644 index 0c3b854..0000000 --- a/src/controller/http_v1/placements.py +++ /dev/null @@ -1,55 +0,0 @@ -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 - -# PlacementPosts router - System-managed actual published posts -placement_posts_router = APIRouter(prefix='/workspaces/{workspace_id}/placement_posts', tags=['placement_posts']) - - -@placement_posts_router.get('') -async def list_placement_posts( - workspace_id: uuid.UUID, - current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], - project_id: uuid.UUID | None = None, - placement_channel_id: uuid.UUID | None = None, - creative_id: uuid.UUID | None = None, - placement_id: uuid.UUID | None = None, - include_archived: bool = False, -) -> dto.GetPlacementPostsOutput: - """List all placement_posts (system-managed actual posts) in workspace""" - input = dto.GetPlacementPostsInput( - user_id=current_user.user_id, - workspace_id=workspace_id, - project_id=project_id, - placement_channel_id=placement_channel_id, - creative_id=creative_id, - placement_id=placement_id, - include_archived=include_archived, - ) - - result = await deps.get_usecase().get_placement_posts(input=input) - return result - - -@placement_posts_router.get('/{placement_post_id}') -async def get_placement_post( - placement_post_id: uuid.UUID, - workspace_id: uuid.UUID, - current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], -) -> dto.PlacementPostOutput: - """Get single placement_post by ID""" - input = dto.GetPlacementPostInput( - placement_post_id=placement_post_id, - user_id=current_user.user_id, - workspace_id=workspace_id, - ) - - return await deps.get_usecase().get_placement_post(input=input) - - -# PlacementPost creation/updates are handled by worker; API exposes only read endpoints. diff --git a/src/controller/http_v1/projects.py b/src/controller/http_v1/projects.py index 0997084..7212b02 100644 --- a/src/controller/http_v1/projects.py +++ b/src/controller/http_v1/projects.py @@ -7,6 +7,7 @@ from fastapi_pagination import Page, paginate from src import deps, dto from src.adapter.jwt import JWTPayload +from src.domain import PermissionKey projects_router = APIRouter(prefix='/workspaces/{workspace_id}/projects', tags=['projects']) @@ -27,6 +28,24 @@ async def get_projects( return paginate(result) # type: ignore[no-any-return] +@projects_router.get('/{project_id}') +async def get_project( + workspace_id: uuid.UUID, + project_id: uuid.UUID, + current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], +) -> dto.ProjectOutput: + context = await deps.get_usecase().ensure_workspace_permission( + workspace_id, current_user.user_id, PermissionKey.PROJECTS_READ + ) + context.ensure_project_permission(PermissionKey.PROJECTS_READ, project_id) + + input = dto.GetProjectInput( + workspace_id=workspace_id, + project_id=project_id, + ) + return await deps.get_usecase().get_project(input=input) + + @projects_router.patch('/{project_id}/invite-link-type') async def update_project_invite_link_type( workspace_id: uuid.UUID, diff --git a/src/controller/http_v1/purchases.py b/src/controller/http_v1/purchases.py index a9454cf..0bb554f 100644 --- a/src/controller/http_v1/purchases.py +++ b/src/controller/http_v1/purchases.py @@ -52,7 +52,7 @@ async def get_placement( workspace_id: uuid.UUID, project_id: uuid.UUID, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], -) -> dto.PlacementOutput: +) -> dto.PlacementWithPostsOutput: """Get single placement by ID""" input = dto.GetPlacementInput( user_id=current_user.user_id, diff --git a/src/dto/__init__.py b/src/dto/__init__.py index e9f0215..7519b8c 100644 --- a/src/dto/__init__.py +++ b/src/dto/__init__.py @@ -26,9 +26,7 @@ __all__ = ( 'GetPlacementsOutput', 'GetPlacementInput', 'PlacementPostOutput', - 'GetPlacementPostsInput', - 'GetPlacementPostsOutput', - 'GetPlacementPostInput', + 'PlacementWithPostsOutput', 'CreativeButton', 'CreativeOutput', 'GetCreativesInput', @@ -131,12 +129,6 @@ from .creative import ( GetCreativesOutput, UpdateCreativeInput, ) -from .placement import ( - GetPlacementPostInput, - GetPlacementPostsInput, - GetPlacementPostsOutput, - PlacementPostOutput, -) from .project import ( ArchiveProjectInput, ChannelBotPermissions, @@ -159,6 +151,8 @@ from .purchase import ( GetPlacementsOutput, PlacementDetails, PlacementOutput, + PlacementPostOutput, + PlacementWithPostsOutput, ) from .user import UserOutput from .validate_login_token import ValidateLoginTokenInput, ValidateLoginTokenOutput diff --git a/src/dto/placement.py b/src/dto/placement.py deleted file mode 100644 index 37f8d6f..0000000 --- a/src/dto/placement.py +++ /dev/null @@ -1,48 +0,0 @@ -import datetime -import uuid - -import pydantic - -from src import domain -from src.dto.purchase import PlacementOutput - - -class PlacementPostOutput(pydantic.BaseModel): - id: uuid.UUID - project_id: uuid.UUID - project_channel_title: str | None - placement_channel_id: uuid.UUID # Канал, где размещен пост - placement_channel_title: str | None - creative_id: uuid.UUID - creative_name: str - placement_date: datetime.datetime # wanted_placement_date - cost: float | None - comment: str | None - ad_post_url: str | None - invite_link_type: domain.InviteLinkType - invite_link: str - status: domain.PlacementPostStatus - subscriptions_count: int - placement_id: uuid.UUID # Ссылка на Placement - placement: PlacementOutput - created_at: datetime.datetime - - -class GetPlacementPostsInput(pydantic.BaseModel): - user_id: uuid.UUID - workspace_id: uuid.UUID - project_id: uuid.UUID | None = None - placement_channel_id: uuid.UUID | None = None - creative_id: uuid.UUID | None = None - placement_id: uuid.UUID | None = None - include_archived: bool = False - - -class GetPlacementPostsOutput(pydantic.BaseModel): - placement_posts: list[PlacementPostOutput] - - -class GetPlacementPostInput(pydantic.BaseModel): - placement_post_id: uuid.UUID - user_id: uuid.UUID - workspace_id: uuid.UUID diff --git a/src/dto/purchase.py b/src/dto/purchase.py index 4c192ad..3c84fc1 100644 --- a/src/dto/purchase.py +++ b/src/dto/purchase.py @@ -4,6 +4,7 @@ import uuid import pydantic from src.domain.placement import CostType, InviteLinkType, PlacementStatus, PlacementType +from src.domain.placement_post import PlacementPostStatus from .channel import ChannelOutput @@ -33,6 +34,31 @@ class PlacementOutput(pydantic.BaseModel): details: PlacementDetails | None = None +class PlacementPostOutput(pydantic.BaseModel): + id: uuid.UUID + project_id: uuid.UUID + project_channel_title: str | None + placement_channel_id: uuid.UUID # Канал, где размещен пост + placement_channel_title: str | None + creative_id: uuid.UUID + creative_name: str + placement_date: datetime.datetime # wanted_placement_date + cost: float | None + comment: str | None + ad_post_url: str | None + invite_link_type: InviteLinkType + invite_link: str + status: PlacementPostStatus + subscriptions_count: int + placement_id: uuid.UUID # Ссылка на Placement + placement: PlacementOutput + created_at: datetime.datetime + + +class PlacementWithPostsOutput(PlacementOutput): + placement_posts: list[PlacementPostOutput] = pydantic.Field(default_factory=list) + + class CreatePlacementChannelInput(pydantic.BaseModel): """Input для создания одного placement (channel + детали)""" @@ -57,7 +83,7 @@ class GetPlacementsInput(pydantic.BaseModel): class GetPlacementsOutput(pydantic.BaseModel): - placements: list[PlacementOutput] + placements: list[PlacementWithPostsOutput] class GetPlacementInput(pydantic.BaseModel): diff --git a/src/usecase/__init__.py b/src/usecase/__init__.py index f97e035..7d97105 100644 --- a/src/usecase/__init__.py +++ b/src/usecase/__init__.py @@ -28,8 +28,6 @@ from .creative.get_creative import get_creative from .creative.get_creatives import get_creatives from .creative.update_creative import update_creative from .placement.fetch_placement_post_cycle import fetch_placement_post_cycle -from .placement.get_placement_post import get_placement_post -from .placement.get_placement_posts import get_placement_posts 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 @@ -172,8 +170,6 @@ class Usecase: update_creative = update_creative delete_creative = delete_creative # PlacementPost (system-managed) use cases - get_placement_posts = get_placement_posts - get_placement_post = get_placement_post fetch_placement_post_cycle = fetch_placement_post_cycle handle_subscription = handle_subscription handle_unsubscription = handle_unsubscription diff --git a/src/usecase/placement/get_placement_post.py b/src/usecase/placement/get_placement_post.py deleted file mode 100644 index c0691ba..0000000 --- a/src/usecase/placement/get_placement_post.py +++ /dev/null @@ -1,57 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from src import domain, dto -from src.usecase.purchase.create_placements import _build_placement_output - -if TYPE_CHECKING: - from .. import Usecase - -log = logging.getLogger(__name__) - - -async def get_placement_post(self: 'Usecase', input: dto.GetPlacementPostInput) -> dto.PlacementPostOutput: - """Получить одну публикацию по ID (system-managed post)""" - context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ - ) - - placement_post = await self.database.get_placement_post(input.workspace_id, input.placement_post_id) - if not placement_post: - log.warning('PlacementPost %s not found for user %s', input.placement_post_id, input.user_id) - raise domain.PlacementPostNotFound(input.placement_post_id) - - placement = placement_post.placement - if not placement: - log.error('PlacementPost %s missing placement data', placement_post.id) - raise domain.PlacementNotFound(placement_post.placement_id) - - context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, placement.project_id) - - if not placement.channel or not placement.project or not placement.creative: - log.error('PlacementPost %s placement missing related data', placement_post.id) - raise domain.PlacementNotFound(placement.id) - - ad_post_url = placement_post.post.url if placement_post.post else None - subscriptions_count = await self.database.count_subscriptions_by_placement_post(placement_post.id) - - return dto.PlacementPostOutput( - id=placement_post.id, - project_id=placement.project_id, - project_channel_title=placement.project.channel.title if placement.project.channel else None, - placement_channel_id=placement.channel_id, - placement_channel_title=placement.channel.title, - creative_id=placement.creative_id, - creative_name=placement.creative.name, - placement_date=placement.placement_at or placement_post.created_at, - cost=placement.cost_value, - comment=placement.comment, - ad_post_url=ad_post_url, - invite_link_type=placement.invite_link_type, - invite_link=placement.invite_link, - status=placement_post.status, - subscriptions_count=subscriptions_count, - placement_id=placement.id, - placement=_build_placement_output(placement), - created_at=placement_post.created_at, - ) diff --git a/src/usecase/placement/get_placement_posts.py b/src/usecase/placement/get_placement_posts.py deleted file mode 100644 index 375be21..0000000 --- a/src/usecase/placement/get_placement_posts.py +++ /dev/null @@ -1,74 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from src import domain, dto -from src.usecase.purchase.create_placements import _build_placement_output - -if TYPE_CHECKING: - from .. import Usecase - -log = logging.getLogger(__name__) - - -async def get_placement_posts(self: 'Usecase', input: dto.GetPlacementPostsInput) -> dto.GetPlacementPostsOutput: - """Получить список публикаций (system-managed posts)""" - context = await self.ensure_workspace_permission( - input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ - ) - - allowed_project_ids = context.allowed_project_ids(domain.PermissionKey.PLACEMENTS_READ) - - if input.project_id is not None: - context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_READ, input.project_id) - allowed_project_ids = None - - placement_posts = await self.database.get_workspace_placement_posts( - input.workspace_id, - project_id=input.project_id, - placement_channel_id=input.placement_channel_id, - creative_id=input.creative_id, - placement_id=input.placement_id, - include_archived=input.include_archived, - allowed_project_ids=allowed_project_ids, - ) - - placement_post_ids = [p.id for p in placement_posts] - subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids) - - placement_post_outputs = [] - for placement_post in placement_posts: - placement = placement_post.placement - if not placement: - log.warning('PlacementPost %s missing placement data', placement_post.id) - continue - - if not placement.channel or not placement.project or not placement.creative: - log.warning('PlacementPost %s placement missing related data', placement_post.id) - continue - - ad_post_url = placement_post.post.url if placement_post.post else None - - placement_post_outputs.append( - dto.PlacementPostOutput( - id=placement_post.id, - project_id=placement.project_id, - project_channel_title=placement.project.channel.title if placement.project.channel else None, - placement_channel_id=placement.channel_id, - placement_channel_title=placement.channel.title, - creative_id=placement.creative_id, - creative_name=placement.creative.name, - placement_date=placement.placement_at or placement_post.created_at, - cost=placement.cost_value, - comment=placement.comment, - ad_post_url=ad_post_url, - invite_link_type=placement.invite_link_type, - invite_link=placement.invite_link, - status=placement_post.status, - subscriptions_count=subscriptions_counts.get(placement_post.id, 0), - placement_id=placement.id, - placement=_build_placement_output(placement), - created_at=placement_post.created_at, - ) - ) - - return dto.GetPlacementPostsOutput(placement_posts=placement_post_outputs) diff --git a/src/usecase/purchase/create_placements.py b/src/usecase/purchase/create_placements.py index 7703316..6c4e089 100644 --- a/src/usecase/purchase/create_placements.py +++ b/src/usecase/purchase/create_placements.py @@ -141,4 +141,14 @@ async def create_placements( creative.id, ) - return dto.GetPlacementsOutput(placements=[_build_placement_output(p) for p in placements]) + placement_outputs = [] + for placement in placements: + placement_output = _build_placement_output(placement) + placement_outputs.append( + dto.PlacementWithPostsOutput( + **placement_output.model_dump(), + placement_posts=[], + ) + ) + + return dto.GetPlacementsOutput(placements=placement_outputs) diff --git a/src/usecase/purchase/get_placement.py b/src/usecase/purchase/get_placement.py index 2216615..adb6894 100644 --- a/src/usecase/purchase/get_placement.py +++ b/src/usecase/purchase/get_placement.py @@ -2,16 +2,52 @@ import logging from typing import TYPE_CHECKING from src import domain, dto - -from .create_placements import _build_placement_output - -if TYPE_CHECKING: - from .. import Usecase +from src.usecase.purchase.create_placements import _build_placement_output log = logging.getLogger(__name__) -async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> dto.PlacementOutput: +def _build_placement_post_output( + placement_post: domain.PlacementPost, subscriptions_count: int +) -> dto.PlacementPostOutput | None: + placement = placement_post.placement + if not placement: + log.warning('PlacementPost %s missing placement data', placement_post.id) + return None + + if not placement.channel or not placement.project or not placement.creative: + log.warning('PlacementPost %s placement missing related data', placement_post.id) + return None + + ad_post_url = placement_post.post.url if placement_post.post else None + + return dto.PlacementPostOutput( + id=placement_post.id, + project_id=placement.project_id, + project_channel_title=placement.project.channel.title if placement.project.channel else None, + placement_channel_id=placement.channel_id, + placement_channel_title=placement.channel.title, + creative_id=placement.creative_id, + creative_name=placement.creative.name, + placement_date=placement.placement_at or placement_post.created_at, + cost=placement.cost_value, + comment=placement.comment, + ad_post_url=ad_post_url, + invite_link_type=placement.invite_link_type, + invite_link=placement.invite_link, + status=placement_post.status, + subscriptions_count=subscriptions_count, + placement_id=placement.id, + placement=_build_placement_output(placement), + created_at=placement_post.created_at, + ) + + +if TYPE_CHECKING: + from .. import Usecase + + +async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> dto.PlacementWithPostsOutput: """Get single placement by ID (user-managed)""" context = await self.ensure_workspace_permission( input.workspace_id, input.user_id, domain.PermissionKey.PLACEMENTS_READ @@ -32,4 +68,24 @@ async def get_placement_user(self: 'Usecase', input: dto.GetPlacementInput) -> d channel = await self.database.get_channel(channel_id=placement.channel_id) placement.channel = channel - return _build_placement_output(placement) + placement_posts = await self.database.get_workspace_placement_posts( + input.workspace_id, + placement_id=placement.id, + include_archived=True, + ) + placement_post_ids = [post.id for post in placement_posts] + subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids) + + placement_post_outputs = [] + for placement_post in placement_posts: + placement_post_output = _build_placement_post_output( + placement_post, subscriptions_counts.get(placement_post.id, 0) + ) + if placement_post_output is not None: + placement_post_outputs.append(placement_post_output) + + placement_output = _build_placement_output(placement) + return dto.PlacementWithPostsOutput( + **placement_output.model_dump(), + placement_posts=placement_post_outputs, + ) diff --git a/src/usecase/purchase/get_placements.py b/src/usecase/purchase/get_placements.py index ee5863d..bfa0cf1 100644 --- a/src/usecase/purchase/get_placements.py +++ b/src/usecase/purchase/get_placements.py @@ -1,9 +1,11 @@ import logging +import uuid from typing import TYPE_CHECKING from src import domain, dto from .create_placements import _build_placement_output +from .get_placement import _build_placement_post_output if TYPE_CHECKING: from .. import Usecase @@ -26,4 +28,32 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto. placements = await self.database.get_project_placements(input.workspace_id, project.id) log.debug('Fetched %s placements for project %s', len(placements), project.id) - return dto.GetPlacementsOutput(placements=[_build_placement_output(p) for p in placements]) + placement_ids = [placement.id for placement in placements] + placement_posts = await self.database.get_placement_posts_by_placement_ids( + input.workspace_id, + placement_ids, + include_archived=True, + ) + placement_post_ids = [post.id for post in placement_posts] + subscriptions_counts = await self.database.count_subscriptions_by_placement_post_batch(placement_post_ids) + + placement_posts_by_placement_id: dict[uuid.UUID, list[dto.PlacementPostOutput]] = {} + for placement_post in placement_posts: + placement_post_output = _build_placement_post_output( + placement_post, subscriptions_counts.get(placement_post.id, 0) + ) + if placement_post_output is None: + continue + placement_posts_by_placement_id.setdefault(placement_post.placement_id, []).append(placement_post_output) + + placement_outputs = [] + for placement in placements: + placement_output = _build_placement_output(placement) + placement_outputs.append( + dto.PlacementWithPostsOutput( + **placement_output.model_dump(), + placement_posts=placement_posts_by_placement_id.get(placement.id, []), + ) + ) + + return dto.GetPlacementsOutput(placements=placement_outputs) diff --git a/tg_bot/backend/backend_client.go b/tg_bot/backend/backend_client.go index 966b526..5282a4f 100644 --- a/tg_bot/backend/backend_client.go +++ b/tg_bot/backend/backend_client.go @@ -245,10 +245,11 @@ func (c *Client) GetProjects( func (c *Client) GetProject( ctx context.Context, + jwt string, workspaceID string, projectID string, ) (*Project, error) { - path := fmt.Sprintf("api/v1/internal/projects/%s/%s", workspaceID, projectID) + path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s", workspaceID, projectID) var project Project err := c.do( @@ -257,6 +258,7 @@ func (c *Client) GetProject( path, nil, &project, + withBearer(jwt), ) return &project, err @@ -558,13 +560,14 @@ type PlacementDetails struct { } type PlacementOutput struct { - ID string `json:"id"` - Status string `json:"status"` - Comment *string `json:"comment,omitempty"` - InviteLink string `json:"invite_link"` - InviteLinkType string `json:"invite_link_type"` - Channel Channel `json:"channel"` - Details *PlacementDetails `json:"details,omitempty"` + ID string `json:"id"` + Status string `json:"status"` + Comment *string `json:"comment,omitempty"` + InviteLink string `json:"invite_link"` + InviteLinkType string `json:"invite_link_type"` + Channel Channel `json:"channel"` + Details *PlacementDetails `json:"details,omitempty"` + PlacementPosts []PlacementPostOutput `json:"placement_posts,omitempty"` } type PlacementPostOutput struct { @@ -594,10 +597,6 @@ type GetPlacementsOutput struct { Placements []PlacementOutput `json:"placements"` } -type GetPlacementPostsOutput struct { - PlacementPosts []PlacementPostOutput `json:"placement_posts"` -} - func (c *Client) CreatePlacements( ctx context.Context, jwt string, @@ -664,39 +663,6 @@ func (c *Client) GetPlacement( return &placement, err } -func (c *Client) GetPlacementPosts( - ctx context.Context, - jwt string, - workspaceID string, - projectID *string, - placementID *string, -) (*GetPlacementPostsOutput, error) { - q := url.Values{} - if projectID != nil && *projectID != "" { - q.Set("project_id", *projectID) - } - if placementID != nil && *placementID != "" { - q.Set("placement_id", *placementID) - } - - path := fmt.Sprintf("api/v1/workspaces/%s/placement_posts", workspaceID) - if len(q) > 0 { - path += "?" + q.Encode() - } - - var resp GetPlacementPostsOutput - err := c.do( - ctx, - http.MethodGet, - path, - nil, - &resp, - withBearer(jwt), - ) - - return &resp, err -} - // ============================================================================ // Workspace Members // ============================================================================ diff --git a/tg_bot/screens/login.go b/tg_bot/screens/login.go index 1638f46..d2b4c54 100644 --- a/tg_bot/screens/login.go +++ b/tg_bot/screens/login.go @@ -37,7 +37,7 @@ func (s *Login) Enter(b *bot.Bot, mode bot.RenderMode) { ParseMode: echotron.HTML, }) if err != nil { - log.Err(err) + log.Err(err).Msg("SendMessage error") return } diff --git a/tg_bot/screens/my_projects.go b/tg_bot/screens/my_projects.go index 558f521..dfc1793 100644 --- a/tg_bot/screens/my_projects.go +++ b/tg_bot/screens/my_projects.go @@ -215,6 +215,7 @@ func (s *MyProjects) HandleCallback(b *bot.Bot, u *echotron.Update) { // Получаем проект напрямую по ID project, err := b.Backend.GetProject( context.Background(), + b.Session.JWT, s.WorkspaceID, projectID, ) diff --git a/tg_bot/screens/placement_details.go b/tg_bot/screens/placement_details.go index 7186ea6..14dac83 100644 --- a/tg_bot/screens/placement_details.go +++ b/tg_bot/screens/placement_details.go @@ -44,26 +44,9 @@ func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode, jwt st return } - placementPosts, err := b.Backend.GetPlacementPosts( - context.Background(), - jwt, - s.WorkspaceID, - &s.ProjectID, - &s.PlacementID, - ) - if err != nil { - log.Error().Err(err).Msg("Failed to get placement posts") - } - var placementPost *backend.PlacementPostOutput - if placementPosts != nil { - for i := range placementPosts.PlacementPosts { - post := &placementPosts.PlacementPosts[i] - if post.PlacementID == s.PlacementID { - placementPost = post - break - } - } + if len(placement.PlacementPosts) > 0 { + placementPost = &placement.PlacementPosts[0] } text := "Детали размещения\n\n"