diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d9ca4bb..be167947 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [0.9.32] - 2025-10-05 + +### ✨ Features +- **Редактирование мигрированных шаутов**: Добавлена мутация `create_draft_from_shout` для создания черновика из существующего опубликованного шаута + - Создаёт черновик со всеми данными из шаута (title, body, lead, topics, authors, media, etc.) + - Проверяет авторство перед созданием черновика + - Переиспользует существующий черновик если он уже создан для этого шаута + - Копирует все связи: авторов и темы (включая main topic) + +### 🔧 Fixed +- **NotificationEntity enum**: Исправлена ошибка `NotificationEntity.FOLLOWER` → `NotificationEntity.AUTHOR` + - В enum не было значения `FOLLOWER`, используется `AUTHOR` для уведомлений о подписчиках + +### Technical Details +- `core/schema/mutation.graphql`: добавлена мутация `create_draft_from_shout(shout_id: Int!): CommonResult!` +- `core/resolvers/draft.py`: добавлен resolver `create_draft_from_shout` с валидацией авторства +- `core/resolvers/notifier.py`: исправлено использование `NotificationEntity.AUTHOR` вместо несуществующего `FOLLOWER` + ## [0.9.31] - 2025-10-04 ### ✅ Fixed: Notifications TODOs diff --git a/pyproject.toml b/pyproject.toml index 3fb842b8..01997655 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "discours-core" -version = "0.9.28" +version = "0.9.32" description = "Core backend for Discours.io platform" authors = [ {name = "Tony Rewin", email = "tonyrewin@yandex.ru"} diff --git a/resolvers/draft.py b/resolvers/draft.py index 524906d4..da808ee0 100644 --- a/resolvers/draft.py +++ b/resolvers/draft.py @@ -274,6 +274,108 @@ async def create_draft(_: None, info: GraphQLResolveInfo, draft_input: dict[str, return {"error": f"Failed to create draft: {e!s}"} +@mutation.field("create_draft_from_shout") +@login_required +async def create_draft_from_shout(_: None, info: GraphQLResolveInfo, shout_id: int) -> dict[str, Any]: + """ + Создаёт черновик из существующего опубликованного шаута для редактирования. + + Args: + info: GraphQL context + shout_id (int): ID публикации (shout) + + Returns: + dict: Contains either: + - draft: The created draft object with shout reference + - error: Error message if creation failed + + Example: + >>> async def test_create_from_shout(): + ... context = {'user_id': '123', 'author': {'id': 1}} + ... info = type('Info', (), {'context': context})() + ... result = await create_draft_from_shout(None, info, 42) + ... assert result.get('error') is None + ... assert result['draft'].shout == 42 + ... return result + """ + author_dict = info.context.get("author") or {} + author_id = author_dict.get("id") + + if not author_id or not isinstance(author_id, int): + return {"error": "Author ID is required"} + + try: + with local_session() as session: + # Загружаем шаут с авторами и темами + shout = ( + session.query(Shout) + .options(joinedload(Shout.authors), joinedload(Shout.topics)) + .where(Shout.id == shout_id) + .first() + ) + + if not shout: + return {"error": f"Shout with id={shout_id} not found"} + + # Проверяем, что пользователь является автором шаута + author_ids = [a.id for a in shout.authors] + if author_id not in author_ids: + return {"error": "You are not authorized to edit this shout"} + + # Проверяем, нет ли уже черновика для этого шаута + existing_draft = session.query(Draft).where(Draft.shout == shout_id).first() + if existing_draft: + logger.info(f"Draft already exists for shout {shout_id}: draft_id={existing_draft.id}") + return {"draft": create_draft_dict(existing_draft)} + + # Создаём новый черновик из шаута + now = int(time.time()) + draft = Draft( + created_at=now, + created_by=author_id, + community=shout.community, + layout=shout.layout or "article", + title=shout.title or "", + subtitle=shout.subtitle, + body=shout.body or "", + lead=shout.lead, + slug=shout.slug, + cover=shout.cover, + cover_caption=shout.cover_caption, + seo=shout.seo, + media=shout.media, + lang=shout.lang or "ru", + shout=shout_id, # Связываем с существующим шаутом + ) + + session.add(draft) + session.flush() + + # Копируем авторов из шаута + for author in shout.authors: + da = DraftAuthor(draft=draft.id, author=author.id) + session.add(da) + + # Копируем темы из шаута + shout_topics = session.query(ShoutTopic).where(ShoutTopic.shout == shout_id).all() + for st in shout_topics: + dt = DraftTopic(draft=draft.id, topic=st.topic, main=st.main) + session.add(dt) + + session.commit() + + logger.info(f"Created draft {draft.id} from shout {shout_id}") + + # Формируем результат + draft_dict = create_draft_dict(draft) + + return {"draft": draft_dict} + + except Exception as e: + logger.error(f"Failed to create draft from shout {shout_id}: {e}", exc_info=True) + return {"error": f"Failed to create draft from shout: {e!s}"} + + def generate_teaser(body: str, limit: int = 300) -> str: body_text = extract_text(body) return ". ".join(body_text[:limit].split(". ")[:-1]) diff --git a/resolvers/notifier.py b/resolvers/notifier.py index 63dc9c7d..ad67e01e 100644 --- a/resolvers/notifier.py +++ b/resolvers/notifier.py @@ -286,7 +286,7 @@ async def notifications_seen_thread(_: None, info: GraphQLResolveInfo, thread: s if thread == "followers": # Mark follower notifications as seen query_conditions = [ - Notification.entity == NotificationEntity.FOLLOWER.value, + Notification.entity == NotificationEntity.AUTHOR.value, ] if after_datetime: query_conditions.append(Notification.created_at > after_datetime) diff --git a/schema/mutation.graphql b/schema/mutation.graphql index 31ee060e..8ce76cc6 100644 --- a/schema/mutation.graphql +++ b/schema/mutation.graphql @@ -24,6 +24,7 @@ type Mutation { # draft create_draft(draft_input: DraftInput!): CommonResult! + create_draft_from_shout(shout_id: Int!): CommonResult! update_draft(draft_id: Int!, draft_input: DraftInput!): CommonResult! delete_draft(draft_id: Int!): CommonResult! # publication