статусы
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
# ruff: noqa
|
||||||
|
# mypy: ignore-errors
|
||||||
|
from tortoise import BaseDBAsyncClient
|
||||||
|
|
||||||
|
RUN_IN_TRANSACTION = True
|
||||||
|
|
||||||
|
|
||||||
|
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement_post" ADD "status" VARCHAR(64) NOT NULL DEFAULT 'Без статуса';
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||||
|
return """
|
||||||
|
ALTER TABLE "placement_post" DROP COLUMN "status";
|
||||||
|
"""
|
||||||
@@ -95,6 +95,26 @@ async def build_placement_creative(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@placements_user_router.patch('/{placement_id}/posts/{placement_post_id}')
|
||||||
|
async def update_placement_post(
|
||||||
|
placement_id: uuid.UUID,
|
||||||
|
placement_post_id: uuid.UUID,
|
||||||
|
request: dto.UpdatePlacementPostInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
current_user: Annotated[JWTPayload, Depends(deps.get_current_user)],
|
||||||
|
) -> dto.PlacementWithPostsOutput:
|
||||||
|
"""Update placement post status"""
|
||||||
|
return await deps.get_usecase().update_placement_post(
|
||||||
|
placement_id=placement_id,
|
||||||
|
placement_post_id=placement_post_id,
|
||||||
|
input=request,
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
project_id=project_id,
|
||||||
|
user_id=current_user.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@placements_user_router.delete('/{placement_id}')
|
@placements_user_router.delete('/{placement_id}')
|
||||||
async def delete_placement(
|
async def delete_placement(
|
||||||
placement_id: uuid.UUID,
|
placement_id: uuid.UUID,
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ __all__ = (
|
|||||||
'Placement',
|
'Placement',
|
||||||
'PlacementStatus',
|
'PlacementStatus',
|
||||||
'PlacementType',
|
'PlacementType',
|
||||||
|
'parse_format_duration',
|
||||||
'PlacementPost',
|
'PlacementPost',
|
||||||
|
'PlacementPostStatus',
|
||||||
'Creative',
|
'Creative',
|
||||||
'CreativeMedia',
|
'CreativeMedia',
|
||||||
'replace_invite_link_with_tag',
|
'replace_invite_link_with_tag',
|
||||||
@@ -119,8 +121,9 @@ from .placement import (
|
|||||||
Placement,
|
Placement,
|
||||||
PlacementStatus,
|
PlacementStatus,
|
||||||
PlacementType,
|
PlacementType,
|
||||||
|
parse_format_duration,
|
||||||
)
|
)
|
||||||
from .placement_post import PlacementPost
|
from .placement_post import PlacementPost, PlacementPostStatus
|
||||||
from .post import Post
|
from .post import Post
|
||||||
from .post_views_history import PostViewsHistory
|
from .post_views_history import PostViewsHistory
|
||||||
from .project import Project, ProjectStatus
|
from .project import Project, ProjectStatus
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import enum
|
import enum
|
||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ if TYPE_CHECKING:
|
|||||||
from .creative import Creative
|
from .creative import Creative
|
||||||
from .project import Project
|
from .project import Project
|
||||||
|
|
||||||
__all__ = ['Placement', 'PlacementStatus', 'PlacementType', 'InviteLinkType', 'CostType']
|
__all__ = ['Placement', 'PlacementStatus', 'PlacementType', 'InviteLinkType', 'CostType', 'parse_format_duration']
|
||||||
|
|
||||||
|
|
||||||
class CostType(enum.StrEnum):
|
class CostType(enum.StrEnum):
|
||||||
@@ -42,6 +43,46 @@ class InviteLinkType(enum.StrEnum):
|
|||||||
APPROVAL = 'approval' # с одобрением ботом
|
APPROVAL = 'approval' # с одобрением ботом
|
||||||
|
|
||||||
|
|
||||||
|
def parse_format_duration(format_str: str | None) -> int | None:
|
||||||
|
"""
|
||||||
|
Парсит формат размещения и возвращает длительность в секундах.
|
||||||
|
|
||||||
|
Примеры форматов:
|
||||||
|
- "1 / 24" -> 86400 сек (24 часа)
|
||||||
|
- "1/48" -> 172800 сек (48 часов)
|
||||||
|
- "1 / 72" -> 259200 сек (72 часа)
|
||||||
|
- "1 / (7 дней)" -> 604800 сек (7 дней)
|
||||||
|
- "1 / (30 дней)" -> 2592000 сек (30 дней)
|
||||||
|
- "1 / (без удаления)" -> None (не удалять)
|
||||||
|
- "2 / 24" -> 86400 сек (24 часа)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int | None: Длительность в секундах или None для "без удаления"
|
||||||
|
"""
|
||||||
|
if not format_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
format_str = format_str.strip().lower()
|
||||||
|
|
||||||
|
# Проверяем "без удаления"
|
||||||
|
if 'без удаления' in format_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Ищем число часов (например "24", "48", "72")
|
||||||
|
hours_match = re.search(r'/\s*(\d+)\s*$', format_str)
|
||||||
|
if hours_match:
|
||||||
|
hours = int(hours_match.group(1))
|
||||||
|
return hours * 3600 # конвертируем в секунды
|
||||||
|
|
||||||
|
# Ищем дни (например "(7 дней)", "(30 дней)")
|
||||||
|
days_match = re.search(r'\((\d+)\s*дн', format_str)
|
||||||
|
if days_match:
|
||||||
|
days = int(days_match.group(1))
|
||||||
|
return days * 24 * 3600 # конвертируем в секунды
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Placement(TimestampedModel):
|
class Placement(TimestampedModel):
|
||||||
status = fields.CharEnumField(PlacementStatus, default=PlacementStatus.NO_STATUS)
|
status = fields.CharEnumField(PlacementStatus, default=PlacementStatus.NO_STATUS)
|
||||||
placement_at = fields.DatetimeField(null=True)
|
placement_at = fields.DatetimeField(null=True)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import enum
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
@@ -9,7 +10,26 @@ if TYPE_CHECKING:
|
|||||||
from .placement import Placement
|
from .placement import Placement
|
||||||
from .post import Post
|
from .post import Post
|
||||||
|
|
||||||
__all__ = ['PlacementPost']
|
__all__ = ['PlacementPost', 'PlacementPostStatus']
|
||||||
|
|
||||||
|
|
||||||
|
class PlacementPostStatus(enum.StrEnum):
|
||||||
|
# Ручные статусы
|
||||||
|
NO_STATUS = 'Без статуса'
|
||||||
|
SEND_POST = 'Отправить пост'
|
||||||
|
POST_APPROVAL = 'Согласование поста'
|
||||||
|
WAITING_SCHEDULE = 'Ожидание отложки'
|
||||||
|
SCHEDULED = 'Запланирован'
|
||||||
|
|
||||||
|
# Автоматические статусы
|
||||||
|
POST_PUBLISHED = 'Пост вышел'
|
||||||
|
COMPLETED_DELETED = 'Размещение отработало - пост удалён'
|
||||||
|
COMPLETED_NOT_DELETED = 'Размещение отработало - пост не удалён'
|
||||||
|
|
||||||
|
# Проверить (требуют внимания)
|
||||||
|
CHECK_DELETED_EARLY = 'Проверить - пост удалён раньше срока'
|
||||||
|
CHECK_NOT_PUBLISHED = 'Проверить - пост не вышел'
|
||||||
|
CHECK_COMPLETED = 'Размещение отработало' # ручной после проверки
|
||||||
|
|
||||||
|
|
||||||
class PlacementPost(TimestampedModel):
|
class PlacementPost(TimestampedModel):
|
||||||
@@ -19,6 +39,7 @@ class PlacementPost(TimestampedModel):
|
|||||||
post: fields.ForeignKeyRelation['Post'] | None = fields.ForeignKeyField(
|
post: fields.ForeignKeyRelation['Post'] | None = fields.ForeignKeyField(
|
||||||
'models.Post', related_name='placement_posts', on_delete=fields.SET_NULL, null=True, index=True
|
'models.Post', related_name='placement_posts', on_delete=fields.SET_NULL, null=True, index=True
|
||||||
)
|
)
|
||||||
|
status = fields.CharEnumField(PlacementPostStatus, default=PlacementPostStatus.NO_STATUS, max_length=64)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
post_id: UUID | None
|
post_id: UUID | None
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ __all__ = (
|
|||||||
'GetPlacementInput',
|
'GetPlacementInput',
|
||||||
'UpdatePlacementInput',
|
'UpdatePlacementInput',
|
||||||
'DeletePlacementInput',
|
'DeletePlacementInput',
|
||||||
|
'UpdatePlacementPostInput',
|
||||||
'PlacementPostOutput',
|
'PlacementPostOutput',
|
||||||
'PostOutput',
|
'PostOutput',
|
||||||
'PlacementWithPostsOutput',
|
'PlacementWithPostsOutput',
|
||||||
@@ -173,6 +174,7 @@ from .purchase import (
|
|||||||
PlacementWithPostsOutput,
|
PlacementWithPostsOutput,
|
||||||
PostOutput,
|
PostOutput,
|
||||||
UpdatePlacementInput,
|
UpdatePlacementInput,
|
||||||
|
UpdatePlacementPostInput,
|
||||||
)
|
)
|
||||||
from .user import UserOutput
|
from .user import UserOutput
|
||||||
from .validate_login_token import ValidateLoginTokenInput, ValidateLoginTokenOutput
|
from .validate_login_token import ValidateLoginTokenInput, ValidateLoginTokenOutput
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import uuid
|
|||||||
import pydantic
|
import pydantic
|
||||||
|
|
||||||
from src.domain.placement import CostType, InviteLinkType, PlacementStatus, PlacementType
|
from src.domain.placement import CostType, InviteLinkType, PlacementStatus, PlacementType
|
||||||
|
from src.domain.placement_post import PlacementPostStatus
|
||||||
|
|
||||||
from .channel import ChannelOutput
|
from .channel import ChannelOutput
|
||||||
|
|
||||||
@@ -47,6 +48,8 @@ class PostOutput(pydantic.BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PlacementPostOutput(pydantic.BaseModel):
|
class PlacementPostOutput(pydantic.BaseModel):
|
||||||
|
id: uuid.UUID
|
||||||
|
status: PlacementPostStatus
|
||||||
subscriptions_count: int
|
subscriptions_count: int
|
||||||
views_count: int | None
|
views_count: int | None
|
||||||
created_at: datetime.datetime
|
created_at: datetime.datetime
|
||||||
@@ -109,3 +112,7 @@ class DeletePlacementInput(pydantic.BaseModel):
|
|||||||
workspace_id: uuid.UUID
|
workspace_id: uuid.UUID
|
||||||
project_id: uuid.UUID
|
project_id: uuid.UUID
|
||||||
placement_id: uuid.UUID
|
placement_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
|
class UpdatePlacementPostInput(pydantic.BaseModel):
|
||||||
|
status: PlacementPostStatus | None = None
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from .creative.get_creative import get_creative
|
|||||||
from .creative.get_creatives import get_creatives
|
from .creative.get_creatives import get_creatives
|
||||||
from .creative.update_creative import update_creative
|
from .creative.update_creative import update_creative
|
||||||
from .placement.fetch_placement_post_cycle import fetch_placement_post_cycle
|
from .placement.fetch_placement_post_cycle import fetch_placement_post_cycle
|
||||||
|
from .placement.update_post_status_cycle import update_post_status_cycle
|
||||||
from .project.archive_project import archive_project, unarchive_project
|
from .project.archive_project import archive_project, unarchive_project
|
||||||
from .project.delete_project import delete_project
|
from .project.delete_project import delete_project
|
||||||
from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id
|
from .project.disconnect_project_by_tg_id import disconnect_project_by_tg_id
|
||||||
@@ -46,6 +47,7 @@ from .purchase.delete_placement import delete_placement
|
|||||||
from .purchase.get_placement import get_placement_user
|
from .purchase.get_placement import get_placement_user
|
||||||
from .purchase.get_placements import get_placements
|
from .purchase.get_placements import get_placements
|
||||||
from .purchase.update_placement import update_placement
|
from .purchase.update_placement import update_placement
|
||||||
|
from .purchase.update_placement_post import update_placement_post
|
||||||
from .subscription.handle_subscription import handle_subscription
|
from .subscription.handle_subscription import handle_subscription
|
||||||
from .subscription.handle_unsubscription import handle_unsubscription
|
from .subscription.handle_unsubscription import handle_unsubscription
|
||||||
from .views.get_views_history import get_views_history
|
from .views.get_views_history import get_views_history
|
||||||
@@ -228,6 +230,7 @@ class Usecase:
|
|||||||
get_placement_user = get_placement_user
|
get_placement_user = get_placement_user
|
||||||
build_placement_creative = build_placement_creative
|
build_placement_creative = build_placement_creative
|
||||||
update_placement = update_placement
|
update_placement = update_placement
|
||||||
|
update_placement_post = update_placement_post
|
||||||
delete_placement = delete_placement
|
delete_placement = delete_placement
|
||||||
# Creative use cases
|
# Creative use cases
|
||||||
get_creatives = get_creatives
|
get_creatives = get_creatives
|
||||||
@@ -237,6 +240,7 @@ class Usecase:
|
|||||||
delete_creative = delete_creative
|
delete_creative = delete_creative
|
||||||
# PlacementPost (system-managed) use cases
|
# PlacementPost (system-managed) use cases
|
||||||
fetch_placement_post_cycle = fetch_placement_post_cycle
|
fetch_placement_post_cycle = fetch_placement_post_cycle
|
||||||
|
update_post_status_cycle = update_post_status_cycle
|
||||||
handle_subscription = handle_subscription
|
handle_subscription = handle_subscription
|
||||||
handle_unsubscription = handle_unsubscription
|
handle_unsubscription = handle_unsubscription
|
||||||
get_views_history = get_views_history
|
get_views_history = get_views_history
|
||||||
|
|||||||
141
src/usecase/placement/update_post_status_cycle.py
Normal file
141
src/usecase/placement/update_post_status_cycle.py
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from tortoise import timezone
|
||||||
|
|
||||||
|
from src import domain
|
||||||
|
from src.domain.placement_post import PlacementPostStatus
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Статусы, которые не должны обновляться автоматически (финальные или ручные)
|
||||||
|
FINAL_STATUSES = {
|
||||||
|
PlacementPostStatus.COMPLETED_DELETED,
|
||||||
|
PlacementPostStatus.COMPLETED_NOT_DELETED,
|
||||||
|
PlacementPostStatus.CHECK_COMPLETED,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Статусы, при которых нужно проверять условия для автоматического обновления
|
||||||
|
AUTO_UPDATE_STATUSES = {
|
||||||
|
PlacementPostStatus.NO_STATUS,
|
||||||
|
PlacementPostStatus.SEND_POST,
|
||||||
|
PlacementPostStatus.POST_APPROVAL,
|
||||||
|
PlacementPostStatus.WAITING_SCHEDULE,
|
||||||
|
PlacementPostStatus.SCHEDULED,
|
||||||
|
PlacementPostStatus.POST_PUBLISHED,
|
||||||
|
PlacementPostStatus.CHECK_DELETED_EARLY,
|
||||||
|
PlacementPostStatus.CHECK_NOT_PUBLISHED,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_post_status_cycle(self: 'Usecase', interval_seconds: int) -> None:
|
||||||
|
"""Автоматически обновляет статусы PlacementPost на основе состояния постов"""
|
||||||
|
log.debug('Starting update_post_status_cycle')
|
||||||
|
|
||||||
|
# Получаем все PlacementPost со статусами, которые могут быть обновлены
|
||||||
|
placement_posts = (
|
||||||
|
await domain.PlacementPost.filter(
|
||||||
|
status__in=list(AUTO_UPDATE_STATUSES),
|
||||||
|
)
|
||||||
|
.prefetch_related('placement', 'post', 'post__channel')
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not placement_posts:
|
||||||
|
log.debug('No placement_posts to update')
|
||||||
|
return
|
||||||
|
|
||||||
|
updated_count = 0
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
|
for placement_post in placement_posts:
|
||||||
|
placement = placement_post.placement
|
||||||
|
post = placement_post.post
|
||||||
|
|
||||||
|
if not placement:
|
||||||
|
log.warning('PlacementPost %s missing placement', placement_post.id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_status = _determine_status(placement, placement_post, post, now)
|
||||||
|
|
||||||
|
if new_status and new_status != placement_post.status:
|
||||||
|
old_status = placement_post.status
|
||||||
|
placement_post.status = new_status
|
||||||
|
await placement_post.save()
|
||||||
|
updated_count += 1
|
||||||
|
log.info(
|
||||||
|
'Updated PlacementPost %s status: %s -> %s',
|
||||||
|
placement_post.id,
|
||||||
|
old_status,
|
||||||
|
new_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug('Update post status cycle completed. Updated %s placement_posts', updated_count)
|
||||||
|
|
||||||
|
|
||||||
|
def _determine_status(
|
||||||
|
placement: domain.Placement,
|
||||||
|
placement_post: domain.PlacementPost,
|
||||||
|
post: domain.Post | None,
|
||||||
|
now: datetime.datetime,
|
||||||
|
) -> PlacementPostStatus | None:
|
||||||
|
"""Определяет новый статус PlacementPost на основе текущего состояния"""
|
||||||
|
|
||||||
|
# Если поста нет и прошло время размещения - "Пост не вышел"
|
||||||
|
if post is None:
|
||||||
|
if placement.placement_at and placement.placement_at < now:
|
||||||
|
return PlacementPostStatus.CHECK_NOT_PUBLISHED
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Если пост есть, но не опубликован - оставляем как есть
|
||||||
|
if not post.published_at:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Пост опубликован - проверяем условия для перехода статусов
|
||||||
|
published_at = post.published_at
|
||||||
|
deleted_at = post.deleted_from_channel_at
|
||||||
|
|
||||||
|
# Вычисляем время в топе
|
||||||
|
if deleted_at:
|
||||||
|
time_on_top = int((deleted_at - published_at).total_seconds())
|
||||||
|
else:
|
||||||
|
time_on_top = int((now - published_at).total_seconds())
|
||||||
|
|
||||||
|
# Получаем требуемую длительность из формата
|
||||||
|
required_duration = domain.parse_format_duration(placement.format)
|
||||||
|
|
||||||
|
# Если формат "без удаления" - размещение отработало, если прошло 24 часа
|
||||||
|
if required_duration is None:
|
||||||
|
# По умолчанию считаем что "без удаления" = 24 часа минимум
|
||||||
|
required_duration = 24 * 3600
|
||||||
|
|
||||||
|
# Проверяем условия для автоматических статусов
|
||||||
|
|
||||||
|
# Пост удалён раньше срока
|
||||||
|
if deleted_at and time_on_top < required_duration:
|
||||||
|
return PlacementPostStatus.CHECK_DELETED_EARLY
|
||||||
|
|
||||||
|
# Размещение отработало - пост удалён
|
||||||
|
if deleted_at and time_on_top >= required_duration:
|
||||||
|
return PlacementPostStatus.COMPLETED_DELETED
|
||||||
|
|
||||||
|
# Размещение отработало - пост не удалён
|
||||||
|
if not deleted_at and time_on_top >= required_duration:
|
||||||
|
return PlacementPostStatus.COMPLETED_NOT_DELETED
|
||||||
|
|
||||||
|
# Пост вышел, но ещё не отработал
|
||||||
|
if placement_post.status in {
|
||||||
|
PlacementPostStatus.NO_STATUS,
|
||||||
|
PlacementPostStatus.SEND_POST,
|
||||||
|
PlacementPostStatus.POST_APPROVAL,
|
||||||
|
PlacementPostStatus.WAITING_SCHEDULE,
|
||||||
|
PlacementPostStatus.SCHEDULED,
|
||||||
|
}:
|
||||||
|
return PlacementPostStatus.POST_PUBLISHED
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -42,6 +42,8 @@ def _build_placement_post_output(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return dto.PlacementPostOutput(
|
return dto.PlacementPostOutput(
|
||||||
|
id=placement_post.id,
|
||||||
|
status=placement_post.status,
|
||||||
subscriptions_count=subscriptions_count,
|
subscriptions_count=subscriptions_count,
|
||||||
views_count=views_count,
|
views_count=views_count,
|
||||||
created_at=placement_post.created_at,
|
created_at=placement_post.created_at,
|
||||||
|
|||||||
72
src/usecase/purchase/update_placement_post.py
Normal file
72
src/usecase/purchase/update_placement_post.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import logging
|
||||||
|
import uuid
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from src import domain, dto
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .. import Usecase
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Статусы Placement, при которых статус PlacementPost не может быть изменён
|
||||||
|
LOCKED_PLACEMENT_STATUSES = {
|
||||||
|
domain.PlacementStatus.NO_STATUS,
|
||||||
|
domain.PlacementStatus.WRITE,
|
||||||
|
domain.PlacementStatus.CANCELED,
|
||||||
|
domain.PlacementStatus.PRICE_NOT_OK,
|
||||||
|
domain.PlacementStatus.NOT_RELEVANT,
|
||||||
|
domain.PlacementStatus.NO_RESPONSE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_placement_post(
|
||||||
|
self: 'Usecase',
|
||||||
|
placement_id: uuid.UUID,
|
||||||
|
placement_post_id: uuid.UUID,
|
||||||
|
input: dto.UpdatePlacementPostInput,
|
||||||
|
workspace_id: uuid.UUID,
|
||||||
|
project_id: uuid.UUID,
|
||||||
|
user_id: uuid.UUID,
|
||||||
|
) -> dto.PlacementWithPostsOutput:
|
||||||
|
context = await self.ensure_workspace_permission(workspace_id, user_id, domain.PermissionKey.PLACEMENTS_WRITE)
|
||||||
|
|
||||||
|
project = await self.database.get_project(workspace_id, project_id=project_id)
|
||||||
|
if not project:
|
||||||
|
raise domain.ProjectNotFound(project_id)
|
||||||
|
|
||||||
|
context.ensure_project_permission(domain.PermissionKey.PLACEMENTS_WRITE, project.id)
|
||||||
|
|
||||||
|
placement = await self.database.get_placement(workspace_id, placement_id)
|
||||||
|
if not placement or placement.project_id != project.id:
|
||||||
|
raise domain.PlacementNotFound(placement_id)
|
||||||
|
|
||||||
|
placement_post = await self.database.get_placement_post(workspace_id, placement_post_id)
|
||||||
|
if not placement_post or placement_post.placement_id != placement.id:
|
||||||
|
raise domain.PlacementPostNotFound(placement_post_id)
|
||||||
|
|
||||||
|
fields_set = input.model_fields_set
|
||||||
|
|
||||||
|
if 'status' in fields_set and input.status is not None:
|
||||||
|
# Проверяем, можно ли менять статус поста при текущем статусе Placement
|
||||||
|
if placement.status in LOCKED_PLACEMENT_STATUSES:
|
||||||
|
log.warning(
|
||||||
|
'Cannot change PlacementPost status when Placement status is %s',
|
||||||
|
placement.status,
|
||||||
|
)
|
||||||
|
# Статус поста должен оставаться "Без статуса"
|
||||||
|
if input.status != domain.PlacementPostStatus.NO_STATUS:
|
||||||
|
raise ValueError(
|
||||||
|
f'Статус поста недоступен для изменения при статусе взаимодействия "{placement.status}"'
|
||||||
|
)
|
||||||
|
placement_post.status = input.status
|
||||||
|
|
||||||
|
await placement_post.save()
|
||||||
|
|
||||||
|
placement_input = dto.GetPlacementInput(
|
||||||
|
user_id=user_id,
|
||||||
|
workspace_id=workspace_id,
|
||||||
|
project_id=project_id,
|
||||||
|
placement_id=placement.id,
|
||||||
|
)
|
||||||
|
return await self.get_placement_user(input=placement_input)
|
||||||
Reference in New Issue
Block a user