2025-02-09 14:18:01 +00:00
|
|
|
|
import time
|
2025-03-20 08:01:39 +00:00
|
|
|
|
from operator import or_
|
2025-02-11 09:00:35 +00:00
|
|
|
|
|
2025-02-09 19:26:50 +00:00
|
|
|
|
from sqlalchemy.sql import and_
|
2025-02-09 14:18:01 +00:00
|
|
|
|
|
2025-02-09 19:26:50 +00:00
|
|
|
|
from cache.cache import (
|
2025-02-11 09:00:35 +00:00
|
|
|
|
cache_author,
|
|
|
|
|
cache_by_id,
|
|
|
|
|
cache_topic,
|
|
|
|
|
invalidate_shout_related_cache,
|
|
|
|
|
invalidate_shouts_cache,
|
2025-02-09 19:26:50 +00:00
|
|
|
|
)
|
2025-02-09 14:18:01 +00:00
|
|
|
|
from orm.author import Author
|
|
|
|
|
from orm.draft import Draft
|
2025-02-09 19:26:50 +00:00
|
|
|
|
from orm.shout import Shout, ShoutAuthor, ShoutTopic
|
2025-02-11 09:00:35 +00:00
|
|
|
|
from orm.topic import Topic
|
2025-02-09 14:18:01 +00:00
|
|
|
|
from services.auth import login_required
|
|
|
|
|
from services.db import local_session
|
2025-02-09 19:26:50 +00:00
|
|
|
|
from services.notify import notify_shout
|
2025-02-11 09:00:35 +00:00
|
|
|
|
from services.schema import mutation, query
|
2025-02-09 19:26:50 +00:00
|
|
|
|
from services.search import search_service
|
2025-02-11 09:00:35 +00:00
|
|
|
|
from utils.logger import root_logger as logger
|
2025-02-09 14:18:01 +00:00
|
|
|
|
|
2025-02-10 15:04:08 +00:00
|
|
|
|
|
|
|
|
|
def create_shout_from_draft(session, draft, author_id):
|
|
|
|
|
# Создаем новую публикацию
|
|
|
|
|
shout = Shout(
|
|
|
|
|
body=draft.body,
|
|
|
|
|
slug=draft.slug,
|
|
|
|
|
cover=draft.cover,
|
|
|
|
|
cover_caption=draft.cover_caption,
|
|
|
|
|
lead=draft.lead,
|
|
|
|
|
description=draft.description,
|
|
|
|
|
title=draft.title,
|
|
|
|
|
subtitle=draft.subtitle,
|
|
|
|
|
layout=draft.layout,
|
|
|
|
|
media=draft.media,
|
|
|
|
|
lang=draft.lang,
|
|
|
|
|
seo=draft.seo,
|
|
|
|
|
created_by=author_id,
|
|
|
|
|
community=draft.community,
|
|
|
|
|
draft=draft.id,
|
|
|
|
|
deleted_at=None,
|
|
|
|
|
)
|
|
|
|
|
return shout
|
|
|
|
|
|
|
|
|
|
|
2025-02-09 14:18:01 +00:00
|
|
|
|
@query.field("load_drafts")
|
|
|
|
|
@login_required
|
|
|
|
|
async def load_drafts(_, info):
|
|
|
|
|
user_id = info.context.get("user_id")
|
|
|
|
|
author_dict = info.context.get("author", {})
|
|
|
|
|
author_id = author_dict.get("id")
|
|
|
|
|
|
|
|
|
|
if not user_id or not author_id:
|
|
|
|
|
return {"error": "User ID and author ID are required"}
|
|
|
|
|
|
|
|
|
|
with local_session() as session:
|
2025-03-20 08:01:39 +00:00
|
|
|
|
drafts = (
|
|
|
|
|
session.query(Draft)
|
|
|
|
|
.filter(or_(Draft.authors.any(Author.id == author_id), Draft.created_by == author_id))
|
|
|
|
|
.all()
|
|
|
|
|
)
|
2025-02-09 14:18:01 +00:00
|
|
|
|
return {"drafts": drafts}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@mutation.field("create_draft")
|
|
|
|
|
@login_required
|
2025-02-11 09:00:35 +00:00
|
|
|
|
async def create_draft(_, info, draft_input):
|
2025-02-12 18:59:05 +00:00
|
|
|
|
"""Create a new draft.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
info: GraphQL context
|
|
|
|
|
draft_input (dict): Draft data including optional fields:
|
2025-02-27 13:16:41 +00:00
|
|
|
|
- title (str, required) - заголовок черновика
|
2025-02-12 19:34:57 +00:00
|
|
|
|
- body (str, required) - текст черновика
|
2025-02-12 18:59:05 +00:00
|
|
|
|
- slug (str)
|
|
|
|
|
- etc.
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
dict: Contains either:
|
|
|
|
|
- draft: The created draft object
|
|
|
|
|
- error: Error message if creation failed
|
|
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
>>> async def test_create():
|
|
|
|
|
... context = {'user_id': '123', 'author': {'id': 1}}
|
|
|
|
|
... info = type('Info', (), {'context': context})()
|
|
|
|
|
... result = await create_draft(None, info, {'title': 'Test'})
|
|
|
|
|
... assert result.get('error') is None
|
|
|
|
|
... assert result['draft'].title == 'Test'
|
|
|
|
|
... return result
|
|
|
|
|
"""
|
2025-02-09 14:18:01 +00:00
|
|
|
|
user_id = info.context.get("user_id")
|
|
|
|
|
author_dict = info.context.get("author", {})
|
|
|
|
|
author_id = author_dict.get("id")
|
|
|
|
|
|
|
|
|
|
if not user_id or not author_id:
|
2025-02-12 18:59:05 +00:00
|
|
|
|
return {"error": "Author ID is required"}
|
2025-02-09 14:18:01 +00:00
|
|
|
|
|
2025-02-12 19:34:57 +00:00
|
|
|
|
# Проверяем обязательные поля
|
|
|
|
|
if "body" not in draft_input or not draft_input["body"]:
|
2025-02-18 21:21:51 +00:00
|
|
|
|
draft_input["body"] = "" # Пустая строка вместо NULL
|
2025-03-20 08:01:39 +00:00
|
|
|
|
|
2025-02-27 13:16:41 +00:00
|
|
|
|
if "title" not in draft_input or not draft_input["title"]:
|
|
|
|
|
draft_input["title"] = "" # Пустая строка вместо NULL
|
2025-02-18 21:21:51 +00:00
|
|
|
|
|
2025-02-12 18:59:05 +00:00
|
|
|
|
try:
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
# Remove id from input if present since it's auto-generated
|
|
|
|
|
if "id" in draft_input:
|
|
|
|
|
del draft_input["id"]
|
|
|
|
|
|
2025-02-12 19:34:57 +00:00
|
|
|
|
# Добавляем текущее время создания
|
|
|
|
|
draft_input["created_at"] = int(time.time())
|
2025-02-18 21:21:51 +00:00
|
|
|
|
|
2025-02-12 18:59:05 +00:00
|
|
|
|
draft = Draft(created_by=author_id, **draft_input)
|
|
|
|
|
session.add(draft)
|
|
|
|
|
session.commit()
|
|
|
|
|
return {"draft": draft}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Failed to create draft: {e}", exc_info=True)
|
|
|
|
|
return {"error": f"Failed to create draft: {str(e)}"}
|
2025-02-09 14:18:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@mutation.field("update_draft")
|
|
|
|
|
@login_required
|
2025-03-20 08:01:39 +00:00
|
|
|
|
async def update_draft(_, info, draft_id: int, draft_input):
|
|
|
|
|
"""Обновляет черновик публикации.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
draft_id: ID черновика для обновления
|
|
|
|
|
draft_input: Данные для обновления черновика
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
dict: Обновленный черновик или сообщение об ошибке
|
|
|
|
|
"""
|
2025-02-09 14:18:01 +00:00
|
|
|
|
user_id = info.context.get("user_id")
|
|
|
|
|
author_dict = info.context.get("author", {})
|
|
|
|
|
author_id = author_dict.get("id")
|
2025-03-20 08:01:39 +00:00
|
|
|
|
|
2025-02-09 14:18:01 +00:00
|
|
|
|
if not user_id or not author_id:
|
2025-02-11 09:00:35 +00:00
|
|
|
|
return {"error": "Author ID are required"}
|
2025-02-09 14:18:01 +00:00
|
|
|
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
draft = session.query(Draft).filter(Draft.id == draft_id).first()
|
|
|
|
|
if not draft:
|
|
|
|
|
return {"error": "Draft not found"}
|
|
|
|
|
|
2025-03-20 08:01:39 +00:00
|
|
|
|
Draft.update(draft, draft_input)
|
2025-03-31 11:39:02 +00:00
|
|
|
|
# Set updated_at and updated_by from the authenticated user
|
|
|
|
|
current_time = int(time.time())
|
|
|
|
|
draft.updated_at = current_time
|
|
|
|
|
draft.updated_by = author_id
|
|
|
|
|
|
2025-02-09 14:18:01 +00:00
|
|
|
|
session.commit()
|
|
|
|
|
return {"draft": draft}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@mutation.field("delete_draft")
|
|
|
|
|
@login_required
|
|
|
|
|
async def delete_draft(_, info, draft_id: int):
|
|
|
|
|
author_dict = info.context.get("author", {})
|
|
|
|
|
author_id = author_dict.get("id")
|
|
|
|
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
draft = session.query(Draft).filter(Draft.id == draft_id).first()
|
|
|
|
|
if not draft:
|
|
|
|
|
return {"error": "Draft not found"}
|
2025-02-10 15:04:08 +00:00
|
|
|
|
if author_id != draft.created_by and draft.authors.filter(Author.id == author_id).count() == 0:
|
|
|
|
|
return {"error": "You are not allowed to delete this draft"}
|
2025-02-09 14:18:01 +00:00
|
|
|
|
session.delete(draft)
|
|
|
|
|
session.commit()
|
|
|
|
|
return {"draft": draft}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@mutation.field("publish_draft")
|
|
|
|
|
@login_required
|
|
|
|
|
async def publish_draft(_, info, draft_id: int):
|
|
|
|
|
user_id = info.context.get("user_id")
|
|
|
|
|
author_dict = info.context.get("author", {})
|
|
|
|
|
author_id = author_dict.get("id")
|
|
|
|
|
if not user_id or not author_id:
|
|
|
|
|
return {"error": "User ID and author ID are required"}
|
|
|
|
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
draft = session.query(Draft).filter(Draft.id == draft_id).first()
|
|
|
|
|
if not draft:
|
|
|
|
|
return {"error": "Draft not found"}
|
2025-02-10 15:04:08 +00:00
|
|
|
|
shout = create_shout_from_draft(session, draft, author_id)
|
|
|
|
|
session.add(shout)
|
|
|
|
|
session.commit()
|
2025-02-11 09:00:35 +00:00
|
|
|
|
return {"shout": shout, "draft": draft}
|
2025-02-09 14:18:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@mutation.field("unpublish_draft")
|
|
|
|
|
@login_required
|
|
|
|
|
async def unpublish_draft(_, info, draft_id: int):
|
|
|
|
|
user_id = info.context.get("user_id")
|
|
|
|
|
author_dict = info.context.get("author", {})
|
|
|
|
|
author_id = author_dict.get("id")
|
|
|
|
|
if not user_id or not author_id:
|
|
|
|
|
return {"error": "User ID and author ID are required"}
|
|
|
|
|
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
draft = session.query(Draft).filter(Draft.id == draft_id).first()
|
2025-02-10 15:04:08 +00:00
|
|
|
|
if not draft:
|
|
|
|
|
return {"error": "Draft not found"}
|
|
|
|
|
shout = session.query(Shout).filter(Shout.draft == draft.id).first()
|
|
|
|
|
if shout:
|
|
|
|
|
shout.published_at = None
|
|
|
|
|
session.commit()
|
2025-02-11 09:00:35 +00:00
|
|
|
|
return {"shout": shout, "draft": draft}
|
2025-02-10 15:04:08 +00:00
|
|
|
|
return {"error": "Failed to unpublish draft"}
|
2025-02-09 14:18:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@mutation.field("publish_shout")
|
|
|
|
|
@login_required
|
2025-02-11 09:00:35 +00:00
|
|
|
|
async def publish_shout(_, info, shout_id: int):
|
2025-02-09 14:18:01 +00:00
|
|
|
|
"""Publish draft as a shout or update existing shout.
|
2025-02-11 09:00:35 +00:00
|
|
|
|
|
2025-02-09 14:18:01 +00:00
|
|
|
|
Args:
|
2025-02-09 19:26:50 +00:00
|
|
|
|
shout_id: ID существующей публикации или 0 для новой
|
|
|
|
|
draft: Объект черновика (опционально)
|
2025-02-09 14:18:01 +00:00
|
|
|
|
"""
|
|
|
|
|
user_id = info.context.get("user_id")
|
|
|
|
|
author_dict = info.context.get("author", {})
|
|
|
|
|
author_id = author_dict.get("id")
|
2025-02-10 15:04:08 +00:00
|
|
|
|
now = int(time.time())
|
2025-02-09 14:18:01 +00:00
|
|
|
|
if not user_id or not author_id:
|
|
|
|
|
return {"error": "User ID and author ID are required"}
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
with local_session() as session:
|
2025-02-10 15:04:08 +00:00
|
|
|
|
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
|
|
|
|
if not shout:
|
|
|
|
|
return {"error": "Shout not found"}
|
2025-02-11 21:39:25 +00:00
|
|
|
|
was_published = shout.published_at is not None
|
|
|
|
|
draft = session.query(Draft).where(Draft.id == shout.draft).first()
|
2025-02-09 14:18:01 +00:00
|
|
|
|
if not draft:
|
2025-02-10 15:04:08 +00:00
|
|
|
|
return {"error": "Draft not found"}
|
|
|
|
|
# Находим черновик если не передан
|
|
|
|
|
|
2025-02-09 14:18:01 +00:00
|
|
|
|
if not shout:
|
2025-02-10 15:04:08 +00:00
|
|
|
|
shout = create_shout_from_draft(session, draft, author_id)
|
2025-02-09 14:18:01 +00:00
|
|
|
|
else:
|
2025-02-09 19:26:50 +00:00
|
|
|
|
# Обновляем существующую публикацию
|
2025-02-09 14:18:01 +00:00
|
|
|
|
shout.draft = draft.id
|
|
|
|
|
shout.created_by = author_id
|
|
|
|
|
shout.title = draft.title
|
|
|
|
|
shout.subtitle = draft.subtitle
|
|
|
|
|
shout.body = draft.body
|
|
|
|
|
shout.cover = draft.cover
|
|
|
|
|
shout.cover_caption = draft.cover_caption
|
|
|
|
|
shout.lead = draft.lead
|
|
|
|
|
shout.description = draft.description
|
|
|
|
|
shout.layout = draft.layout
|
|
|
|
|
shout.media = draft.media
|
|
|
|
|
shout.lang = draft.lang
|
|
|
|
|
shout.seo = draft.seo
|
|
|
|
|
|
2025-02-10 15:04:08 +00:00
|
|
|
|
draft.updated_at = now
|
|
|
|
|
shout.updated_at = now
|
2025-02-12 16:21:21 +00:00
|
|
|
|
|
2025-02-11 21:39:25 +00:00
|
|
|
|
# Устанавливаем published_at только если была ранее снята с публикации
|
2025-02-10 15:04:08 +00:00
|
|
|
|
if not was_published:
|
|
|
|
|
shout.published_at = now
|
2025-02-11 09:00:35 +00:00
|
|
|
|
|
2025-02-09 19:26:50 +00:00
|
|
|
|
# Обрабатываем связи с авторами
|
2025-02-11 09:00:35 +00:00
|
|
|
|
if (
|
|
|
|
|
not session.query(ShoutAuthor)
|
|
|
|
|
.filter(and_(ShoutAuthor.shout == shout.id, ShoutAuthor.author == author_id))
|
|
|
|
|
.first()
|
|
|
|
|
):
|
2025-02-09 19:26:50 +00:00
|
|
|
|
sa = ShoutAuthor(shout=shout.id, author=author_id)
|
|
|
|
|
session.add(sa)
|
|
|
|
|
|
|
|
|
|
# Обрабатываем темы
|
|
|
|
|
if draft.topics:
|
|
|
|
|
for topic in draft.topics:
|
|
|
|
|
st = ShoutTopic(
|
2025-02-11 09:00:35 +00:00
|
|
|
|
topic=topic.id, shout=shout.id, main=topic.main if hasattr(topic, "main") else False
|
2025-02-09 19:26:50 +00:00
|
|
|
|
)
|
|
|
|
|
session.add(st)
|
|
|
|
|
|
2025-02-09 14:18:01 +00:00
|
|
|
|
session.add(shout)
|
|
|
|
|
session.add(draft)
|
2025-02-09 19:26:50 +00:00
|
|
|
|
session.flush()
|
|
|
|
|
|
|
|
|
|
# Инвалидируем кэш только если это новая публикация или была снята с публикации
|
|
|
|
|
if not was_published:
|
2025-02-11 09:00:35 +00:00
|
|
|
|
cache_keys = ["feed", f"author_{author_id}", "random_top", "unrated"]
|
|
|
|
|
|
2025-02-09 19:26:50 +00:00
|
|
|
|
# Добавляем ключи для тем
|
|
|
|
|
for topic in shout.topics:
|
|
|
|
|
cache_keys.append(f"topic_{topic.id}")
|
|
|
|
|
cache_keys.append(f"topic_shouts_{topic.id}")
|
|
|
|
|
await cache_by_id(Topic, topic.id, cache_topic)
|
|
|
|
|
|
|
|
|
|
# Инвалидируем кэш
|
|
|
|
|
await invalidate_shouts_cache(cache_keys)
|
|
|
|
|
await invalidate_shout_related_cache(shout, author_id)
|
|
|
|
|
|
|
|
|
|
# Обновляем кэш авторов
|
|
|
|
|
for author in shout.authors:
|
|
|
|
|
await cache_by_id(Author, author.id, cache_author)
|
|
|
|
|
|
|
|
|
|
# Отправляем уведомление о публикации
|
|
|
|
|
await notify_shout(shout.dict(), "published")
|
|
|
|
|
|
|
|
|
|
# Обновляем поисковый индекс
|
|
|
|
|
search_service.index(shout)
|
|
|
|
|
else:
|
|
|
|
|
# Для уже опубликованных материалов просто отправляем уведомление об обновлении
|
|
|
|
|
await notify_shout(shout.dict(), "update")
|
|
|
|
|
|
2025-02-09 14:18:01 +00:00
|
|
|
|
session.commit()
|
2025-02-09 19:26:50 +00:00
|
|
|
|
return {"shout": shout}
|
2025-02-09 14:18:01 +00:00
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-02-09 19:26:50 +00:00
|
|
|
|
logger.error(f"Failed to publish shout: {e}", exc_info=True)
|
2025-02-11 09:00:35 +00:00
|
|
|
|
if "session" in locals():
|
2025-02-09 19:26:50 +00:00
|
|
|
|
session.rollback()
|
|
|
|
|
return {"error": f"Failed to publish shout: {str(e)}"}
|
2025-02-09 14:18:01 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@mutation.field("unpublish_shout")
|
|
|
|
|
@login_required
|
|
|
|
|
async def unpublish_shout(_, info, shout_id: int):
|
|
|
|
|
"""Unpublish a shout.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
shout_id: The ID of the shout to unpublish
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
dict: The unpublished shout or an error message
|
|
|
|
|
"""
|
|
|
|
|
author_dict = info.context.get("author", {})
|
|
|
|
|
author_id = author_dict.get("id")
|
|
|
|
|
if not author_id:
|
|
|
|
|
return {"error": "Author ID is required"}
|
|
|
|
|
|
|
|
|
|
shout = None
|
|
|
|
|
with local_session() as session:
|
|
|
|
|
try:
|
|
|
|
|
shout = session.query(Shout).filter(Shout.id == shout_id).first()
|
|
|
|
|
shout.published_at = None
|
|
|
|
|
session.commit()
|
|
|
|
|
invalidate_shout_related_cache(shout)
|
|
|
|
|
invalidate_shouts_cache()
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
session.rollback()
|
|
|
|
|
return {"error": "Failed to unpublish shout"}
|
|
|
|
|
|
|
|
|
|
return {"shout": shout}
|