This commit is contained in:
@@ -6,11 +6,13 @@
|
||||
- **Исправлена критическая ошибка с уведомлениями**: Устранена ошибка `null value in column "kind" of relation "notification" violates not-null constraint`
|
||||
- **Исправлен возвращаемый формат publish_draft**: Теперь возвращается `{"draft": draft_dict}` вместо `{"shout": shout}` для соответствия GraphQL схеме
|
||||
- **Фронтенд получает корректные данные**: При публикации черновика фронтенд теперь получает ожидаемое поле `draft` вместо `null`
|
||||
- **Исправлена ошибка GraphQL**: Устранена ошибка "Cannot return null for non-nullable field Draft.topics" при публикации черновиков
|
||||
|
||||
### 🏗️ Changed
|
||||
- **Обновлена функция save_notification**: Добавлено обязательное поле `kind` для создания уведомлений
|
||||
- **Исправлена типизация**: Поле `kind` теперь корректно преобразуется из `action` в `NotificationAction` enum
|
||||
- **Убрано неиспользуемое значение PUBLISHED**: Из enum `NotificationAction` убрано значение, которое не использовалось
|
||||
- **Рефакторинг кода**: Создана вспомогательная функция `create_draft_dict()` для избежания дублирования в `publish_draft` и `unpublish_draft`
|
||||
|
||||
### 📦 Added
|
||||
- **Добавлен fallback для нестандартных действий**: Если `action` не соответствует enum, используется `NotificationAction.CREATE`
|
||||
|
||||
@@ -17,9 +17,52 @@ from services.search import search_service
|
||||
from storage.db import local_session
|
||||
from storage.schema import mutation, query
|
||||
from utils.extract_text import extract_text
|
||||
from utils.validators import validate_html_content
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
def create_draft_dict(draft: Draft) -> dict[str, Any]:
|
||||
"""
|
||||
Создает словарь с данными черновика, избегая проблем с null значениями в связях.
|
||||
|
||||
Args:
|
||||
draft: Объект черновика
|
||||
|
||||
Returns:
|
||||
dict: Словарь с данными черновика
|
||||
"""
|
||||
return {
|
||||
"id": draft.id,
|
||||
"created_at": draft.created_at,
|
||||
"created_by": draft.created_by,
|
||||
"community": draft.community,
|
||||
"layout": draft.layout,
|
||||
"slug": draft.slug,
|
||||
"title": draft.title,
|
||||
"subtitle": draft.subtitle,
|
||||
"lead": draft.lead,
|
||||
"body": draft.body,
|
||||
"media": draft.media,
|
||||
"cover": draft.cover,
|
||||
"cover_caption": draft.cover_caption,
|
||||
"lang": draft.lang,
|
||||
"seo": draft.seo,
|
||||
"updated_at": draft.updated_at,
|
||||
"deleted_at": draft.deleted_at,
|
||||
"updated_by": draft.updated_by,
|
||||
"deleted_by": draft.deleted_by,
|
||||
"shout": draft.shout,
|
||||
# Явно загружаем связи, чтобы избежать null значений
|
||||
"authors": [
|
||||
{"id": a.id, "name": a.name, "slug": a.slug, "pic": getattr(a, "pic", None)} for a in (draft.authors or [])
|
||||
],
|
||||
"topics": [
|
||||
{"id": t.id, "name": t.name, "slug": t.slug, "is_main": getattr(t, "is_main", False)}
|
||||
for t in (draft.topics or [])
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def create_shout_from_draft(session: Session | None, draft: Draft, author_id: int) -> Shout:
|
||||
"""
|
||||
Создаёт новый объект публикации (Shout) на основе черновика.
|
||||
@@ -358,39 +401,6 @@ async def delete_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dict
|
||||
return {"draft": draft}
|
||||
|
||||
|
||||
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
|
||||
True
|
||||
>>> error
|
||||
''
|
||||
>>> is_valid, error = validate_html_content("Invalid < HTML")
|
||||
>>> is_valid
|
||||
False
|
||||
>>> 'Invalid HTML' in error
|
||||
True
|
||||
"""
|
||||
if not html_content or not html_content.strip():
|
||||
return False, "Content is empty"
|
||||
|
||||
try:
|
||||
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: {e!s}"
|
||||
|
||||
|
||||
@mutation.field("publish_draft")
|
||||
@login_required
|
||||
async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dict[str, Any]:
|
||||
@@ -503,7 +513,9 @@ async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dic
|
||||
logger.debug(f"Shout data: {shout.dict()}")
|
||||
|
||||
# Возвращаем обновленный черновик с информацией о shout
|
||||
draft_dict = draft.dict()
|
||||
draft_dict = create_draft_dict(draft)
|
||||
|
||||
# Добавляем информацию о shout
|
||||
draft_dict["shout"] = {"id": shout.id, "slug": shout.slug, "published_at": shout.published_at}
|
||||
|
||||
return {"draft": draft_dict}
|
||||
@@ -570,7 +582,8 @@ async def unpublish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> d
|
||||
await invalidate_shout_related_cache(shout, author_id)
|
||||
|
||||
# Формируем результат
|
||||
draft_dict = draft.dict()
|
||||
draft_dict = create_draft_dict(draft)
|
||||
|
||||
# Добавляем информацию о публикации
|
||||
draft_dict["shout"] = {"id": shout.id, "slug": shout.slug, "published_at": None}
|
||||
|
||||
|
||||
@@ -138,6 +138,15 @@ async def test_publish_draft_returns_draft():
|
||||
assert draft_data["id"] == 1
|
||||
assert "shout" in draft_data
|
||||
|
||||
# Проверяем, что authors и topics не null
|
||||
assert "authors" in draft_data
|
||||
assert draft_data["authors"] is not None
|
||||
assert isinstance(draft_data["authors"], list)
|
||||
|
||||
assert "topics" in draft_data
|
||||
assert draft_data["topics"] is not None
|
||||
assert isinstance(draft_data["topics"], list)
|
||||
|
||||
shout_data = draft_data["shout"]
|
||||
assert shout_data["id"] == 100
|
||||
assert shout_data["slug"] == "test-shout"
|
||||
|
||||
34
utils/validators.py
Normal file
34
utils/validators.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from utils.extract_text import extract_text
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
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
|
||||
True
|
||||
>>> error
|
||||
''
|
||||
>>> is_valid, error = validate_html_content("Invalid < HTML")
|
||||
>>> is_valid
|
||||
False
|
||||
>>> 'Invalid HTML' in error
|
||||
True
|
||||
"""
|
||||
if not html_content or not html_content.strip():
|
||||
return False, ""
|
||||
|
||||
try:
|
||||
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: {e!s}"
|
||||
Reference in New Issue
Block a user