upgrade schema, resolvers, panel added

This commit is contained in:
2025-05-16 09:23:48 +03:00
parent 8a60bec73a
commit 2d382be794
80 changed files with 8641 additions and 1100 deletions

View File

@@ -1,26 +1,22 @@
import time
import trafilatura
from sqlalchemy.orm import joinedload
from cache.cache import (
cache_author,
cache_by_id,
cache_topic,
invalidate_shout_related_cache,
invalidate_shouts_cache,
)
from orm.author import Author
from auth.orm import Author
from orm.draft import Draft, DraftAuthor, DraftTopic
from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.topic import Topic
from services.auth import login_required
from services.db import local_session
from services.notify import notify_shout
from services.schema import mutation, query
from services.search import search_service
from utils.html_wrapper import wrap_html_fragment
from utils.extract_text import extract_text
from utils.logger import root_logger as logger
def create_shout_from_draft(session, draft, author_id):
"""
Создаёт новый объект публикации (Shout) на основе черновика.
@@ -62,11 +58,11 @@ def create_shout_from_draft(session, draft, author_id):
draft=draft.id,
deleted_at=None,
)
# Инициализируем пустые массивы для связей
shout.topics = []
shout.authors = []
return shout
@@ -75,10 +71,10 @@ def create_shout_from_draft(session, draft, author_id):
async def load_drafts(_, info):
"""
Загружает все черновики, доступные текущему пользователю.
Предварительно загружает связанные объекты (topics, authors, publication),
чтобы избежать ошибок с отсоединенными объектами при сериализации.
Returns:
dict: Список черновиков или сообщение об ошибке
"""
@@ -97,12 +93,12 @@ async def load_drafts(_, info):
.options(
joinedload(Draft.topics),
joinedload(Draft.authors),
joinedload(Draft.publication) # Загружаем связанную публикацию
joinedload(Draft.publication), # Загружаем связанную публикацию
)
.filter(Draft.authors.any(Author.id == author_id))
)
drafts = drafts_query.all()
# Преобразуем объекты в словари, пока они в контексте сессии
drafts_data = []
for draft in drafts:
@@ -110,19 +106,19 @@ async def load_drafts(_, info):
# Всегда возвращаем массив для topics, даже если он пустой
draft_dict["topics"] = [topic.dict() for topic in (draft.topics or [])]
draft_dict["authors"] = [author.dict() for author in (draft.authors or [])]
# Добавляем информацию о публикации, если она есть
if draft.publication:
draft_dict["publication"] = {
"id": draft.publication.id,
"slug": draft.publication.slug,
"published_at": draft.publication.published_at
"published_at": draft.publication.published_at,
}
else:
draft_dict["publication"] = None
draft_dict["publication"] = None
drafts_data.append(draft_dict)
return {"drafts": drafts_data}
except Exception as e:
logger.error(f"Failed to load drafts: {e}", exc_info=True)
@@ -180,27 +176,27 @@ async def create_draft(_, info, draft_input):
# Remove id from input if present since it's auto-generated
if "id" in draft_input:
del draft_input["id"]
# Добавляем текущее время создания и ID автора
draft_input["created_at"] = int(time.time())
draft_input["created_by"] = author_id
draft = Draft(**draft_input)
session.add(draft)
session.flush()
# Добавляем создателя как автора
da = DraftAuthor(shout=draft.id, author=author_id)
session.add(da)
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)}"}
def generate_teaser(body, limit=300):
body_html = wrap_html_fragment(body)
body_text = trafilatura.extract(body_html, include_comments=False, include_tables=False)
body_text = extract_text(body)
body_teaser = ". ".join(body_text[:limit].split(". ")[:-1])
return body_teaser
@@ -246,9 +242,20 @@ async def update_draft(_, info, draft_id: int, draft_input):
# Фильтруем входные данные, оставляя только разрешенные поля
allowed_fields = {
"layout", "author_ids", "topic_ids", "main_topic_id",
"media", "lead", "subtitle", "lang", "seo", "body",
"title", "slug", "cover", "cover_caption"
"layout",
"author_ids",
"topic_ids",
"main_topic_id",
"media",
"lead",
"subtitle",
"lang",
"seo",
"body",
"title",
"slug",
"cover",
"cover_caption",
}
filtered_input = {k: v for k, v in draft_input.items() if k in allowed_fields}
@@ -277,9 +284,9 @@ async def update_draft(_, info, draft_id: int, draft_input):
# Добавляем новые связи
for tid in topic_ids:
dt = DraftTopic(
shout=draft_id,
shout=draft_id,
topic=tid,
main=(tid == main_topic_id) if main_topic_id else False
main=(tid == main_topic_id) if main_topic_id else False,
)
session.add(dt)
@@ -287,13 +294,10 @@ async def update_draft(_, info, draft_id: int, draft_input):
if "seo" not in filtered_input and not draft.seo:
body_src = filtered_input.get("body", draft.body)
lead_src = filtered_input.get("lead", draft.lead)
body_html = wrap_html_fragment(body_src)
lead_html = wrap_html_fragment(lead_src)
try:
body_text = trafilatura.extract(body_html, include_comments=False, include_tables=False) if body_src else None
lead_text = trafilatura.extract(lead_html, include_comments=False, include_tables=False) if lead_src else None
body_text = extract_text(body_src) if body_src else None
lead_text = extract_text(lead_src) if lead_src else None
body_teaser = generate_teaser(body_text, 300) if body_text else ""
filtered_input["seo"] = lead_text if lead_text else body_teaser
except Exception as e:
@@ -308,14 +312,14 @@ async def update_draft(_, info, draft_id: int, draft_input):
draft.updated_by = author_id
session.commit()
# Преобразуем объект в словарь для ответа
draft_dict = draft.dict()
draft_dict["topics"] = [topic.dict() for topic in draft.topics]
draft_dict["authors"] = [author.dict() for author in draft.authors]
# Добавляем объект автора в updated_by
draft_dict["updated_by"] = author_dict
return {"draft": draft_dict}
except Exception as e:
@@ -343,13 +347,13 @@ async def delete_draft(_, info, draft_id: int):
def validate_html_content(html_content: str) -> tuple[bool, str]:
"""
Проверяет валидность HTML контента через trafilatura.
Args:
html_content: HTML строка для проверки
Returns:
tuple[bool, str]: (валидность, сообщение об ошибке)
Example:
>>> is_valid, error = validate_html_content("<p>Valid HTML</p>")
>>> is_valid
@@ -364,13 +368,10 @@ def validate_html_content(html_content: str) -> tuple[bool, str]:
"""
if not html_content or not html_content.strip():
return False, "Content is empty"
try:
html_content = wrap_html_fragment(html_content)
extracted = trafilatura.extract(html_content)
if not extracted:
return False, "Invalid HTML structure or empty content"
return True, ""
extracted = extract_text(html_content)
return bool(extracted), extracted or ""
except Exception as e:
logger.error(f"HTML validation error: {e}", exc_info=True)
return False, f"Invalid HTML content: {str(e)}"
@@ -381,10 +382,10 @@ def validate_html_content(html_content: str) -> tuple[bool, str]:
async def publish_draft(_, info, draft_id: int):
"""
Публикует черновик, создавая новый Shout или обновляя существующий.
Args:
draft_id (int): ID черновика для публикации
Returns:
dict: Результат публикации с shout или сообщением об ошибке
"""
@@ -400,11 +401,7 @@ async def publish_draft(_, info, draft_id: int):
# Загружаем черновик со всеми связями
draft = (
session.query(Draft)
.options(
joinedload(Draft.topics),
joinedload(Draft.authors),
joinedload(Draft.publication)
)
.options(joinedload(Draft.topics), joinedload(Draft.authors), joinedload(Draft.publication))
.filter(Draft.id == draft_id)
.first()
)
@@ -421,7 +418,17 @@ async def publish_draft(_, info, draft_id: int):
if draft.publication:
shout = draft.publication
# Обновляем существующую публикацию
for field in ["body", "title", "subtitle", "lead", "cover", "cover_caption", "media", "lang", "seo"]:
for field in [
"body",
"title",
"subtitle",
"lead",
"cover",
"cover_caption",
"media",
"lang",
"seo",
]:
if hasattr(draft, field):
setattr(shout, field, getattr(draft, field))
shout.updated_at = int(time.time())
@@ -440,16 +447,14 @@ async def publish_draft(_, info, draft_id: int):
session.query(ShoutTopic).filter(ShoutTopic.shout == shout.id).delete()
# Добавляем авторов
for author in (draft.authors or []):
for author in draft.authors or []:
sa = ShoutAuthor(shout=shout.id, author=author.id)
session.add(sa)
# Добавляем темы
for topic in (draft.topics or []):
for topic in draft.topics or []:
st = ShoutTopic(
topic=topic.id,
shout=shout.id,
main=topic.main if hasattr(topic, "main") else False
topic=topic.id, shout=shout.id, main=topic.main if hasattr(topic, "main") else False
)
session.add(st)