This commit is contained in:
@@ -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`
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -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
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