draft-validator-fix
Some checks failed
Deploy on push / deploy (push) Failing after 2m30s

This commit is contained in:
2025-08-23 12:36:04 +03:00
parent ee53d5b491
commit 19a964585e
4 changed files with 93 additions and 35 deletions

View File

@@ -6,11 +6,13 @@
- **Исправлена критическая ошибка с уведомлениями**: Устранена ошибка `null value in column "kind" of relation "notification" violates not-null constraint` - **Исправлена критическая ошибка с уведомлениями**: Устранена ошибка `null value in column "kind" of relation "notification" violates not-null constraint`
- **Исправлен возвращаемый формат publish_draft**: Теперь возвращается `{"draft": draft_dict}` вместо `{"shout": shout}` для соответствия GraphQL схеме - **Исправлен возвращаемый формат publish_draft**: Теперь возвращается `{"draft": draft_dict}` вместо `{"shout": shout}` для соответствия GraphQL схеме
- **Фронтенд получает корректные данные**: При публикации черновика фронтенд теперь получает ожидаемое поле `draft` вместо `null` - **Фронтенд получает корректные данные**: При публикации черновика фронтенд теперь получает ожидаемое поле `draft` вместо `null`
- **Исправлена ошибка GraphQL**: Устранена ошибка "Cannot return null for non-nullable field Draft.topics" при публикации черновиков
### 🏗️ Changed ### 🏗️ Changed
- **Обновлена функция save_notification**: Добавлено обязательное поле `kind` для создания уведомлений - **Обновлена функция save_notification**: Добавлено обязательное поле `kind` для создания уведомлений
- **Исправлена типизация**: Поле `kind` теперь корректно преобразуется из `action` в `NotificationAction` enum - **Исправлена типизация**: Поле `kind` теперь корректно преобразуется из `action` в `NotificationAction` enum
- **Убрано неиспользуемое значение PUBLISHED**: Из enum `NotificationAction` убрано значение, которое не использовалось - **Убрано неиспользуемое значение PUBLISHED**: Из enum `NotificationAction` убрано значение, которое не использовалось
- **Рефакторинг кода**: Создана вспомогательная функция `create_draft_dict()` для избежания дублирования в `publish_draft` и `unpublish_draft`
### 📦 Added ### 📦 Added
- **Добавлен fallback для нестандартных действий**: Если `action` не соответствует enum, используется `NotificationAction.CREATE` - **Добавлен fallback для нестандартных действий**: Если `action` не соответствует enum, используется `NotificationAction.CREATE`

View File

@@ -17,9 +17,52 @@ from services.search import search_service
from storage.db import local_session from storage.db import local_session
from storage.schema import mutation, query from storage.schema import mutation, query
from utils.extract_text import extract_text from utils.extract_text import extract_text
from utils.validators import validate_html_content
from utils.logger import root_logger as logger 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: def create_shout_from_draft(session: Session | None, draft: Draft, author_id: int) -> Shout:
""" """
Создаёт новый объект публикации (Shout) на основе черновика. Создаёт новый объект публикации (Shout) на основе черновика.
@@ -358,39 +401,6 @@ async def delete_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dict
return {"draft": draft} 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") @mutation.field("publish_draft")
@login_required @login_required
async def publish_draft(_: None, info: GraphQLResolveInfo, draft_id: int) -> dict[str, Any]: 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()}") logger.debug(f"Shout data: {shout.dict()}")
# Возвращаем обновленный черновик с информацией о shout # Возвращаем обновленный черновик с информацией о 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} draft_dict["shout"] = {"id": shout.id, "slug": shout.slug, "published_at": shout.published_at}
return {"draft": draft_dict} 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) 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} draft_dict["shout"] = {"id": shout.id, "slug": shout.slug, "published_at": None}

View File

@@ -138,6 +138,15 @@ async def test_publish_draft_returns_draft():
assert draft_data["id"] == 1 assert draft_data["id"] == 1
assert "shout" in draft_data 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"] shout_data = draft_data["shout"]
assert shout_data["id"] == 100 assert shout_data["id"] == 100
assert shout_data["slug"] == "test-shout" assert shout_data["slug"] == "test-shout"

34
utils/validators.py Normal file
View 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}"