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.get_feed_duration_seconds(placement) # Если формат "без удаления" - размещение отработало, если прошло 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