### 🚨 Исправлено - **Удалено поле username из модели Author**: Поле `username` больше не является частью модели `Author` - Убрано свойство `@property def username` из `orm/author.py` - Обновлены все сервисы для использования `email` или `slug` вместо `username` - Исправлены резолверы для исключения `username` при обработке данных автора - Поле `username` теперь используется только в JWT токенах для совместимости ### 🧪 Исправлено - **E2E тесты админ-панели**: Полностью переработаны E2E тесты для работы с реальным API - Тесты теперь делают реальные HTTP запросы к GraphQL API - Бэкенд для тестов использует выделенную тестовую БД (`test_e2e.db`) - Создан фикстура `backend_server` для запуска тестового сервера - Добавлен фикстура `create_test_users_in_backend_db` для регистрации пользователей через API - Убраны несуществующие GraphQL запросы (`get_community_stats`) - Тесты корректно работают с системой ролей и правами администратора ### �� Техническое - **Рефакторинг аутентификации**: Упрощена логика работы с пользователями - Убраны зависимости от несуществующих полей в ORM моделях - Обновлены сервисы аутентификации для корректной работы без `username` - Исправлены все места использования `username` в коде - **Улучшена тестовая инфраструктура**: - Тесты теперь используют реальный HTTP API вместо прямых DB проверок - Правильная изоляция тестовых данных через отдельную БД - Корректная работа с системой ролей и правами
This commit is contained in:
@@ -409,7 +409,7 @@ async def get_author(
|
||||
# Создаем объект автора для использования метода dict
|
||||
temp_author = Author()
|
||||
for key, value in cached_author.items():
|
||||
if hasattr(temp_author, key):
|
||||
if hasattr(temp_author, key) and key != "username": # username - это свойство, нельзя устанавливать
|
||||
setattr(temp_author, key, value)
|
||||
# Получаем отфильтрованную версию
|
||||
author_dict = temp_author.dict(is_admin)
|
||||
@@ -608,7 +608,7 @@ async def get_author_follows_authors(
|
||||
# Создаем объект автора для использования метода dict
|
||||
temp_author = Author()
|
||||
for key, value in author_data.items():
|
||||
if hasattr(temp_author, key):
|
||||
if hasattr(temp_author, key) and key != "username": # username - это свойство, нельзя устанавливать
|
||||
setattr(temp_author, key, value)
|
||||
# Добавляем отфильтрованную версию
|
||||
# temp_author - это объект Author, который мы хотим сериализовать
|
||||
@@ -688,7 +688,7 @@ async def get_author_followers(_: None, info: GraphQLResolveInfo, **kwargs: Any)
|
||||
# Создаем объект автора для использования метода dict
|
||||
temp_author = Author()
|
||||
for key, value in follower_data.items():
|
||||
if hasattr(temp_author, key):
|
||||
if hasattr(temp_author, key) and key != "username": # username - это свойство, нельзя устанавливать
|
||||
setattr(temp_author, key, value)
|
||||
# Добавляем отфильтрованную версию
|
||||
# temp_author - это объект Author, который мы хотим сериализовать
|
||||
|
||||
@@ -39,8 +39,8 @@ def load_shouts_bookmarked(_: None, info, options) -> list[Shout]:
|
||||
AuthorBookmark.author == author_id,
|
||||
)
|
||||
)
|
||||
q, limit, offset = apply_options(q, options, author_id)
|
||||
return get_shouts_with_links(info, q, limit, offset)
|
||||
q, limit, offset, sort_meta = apply_options(q, options, author_id)
|
||||
return get_shouts_with_links(info, q, limit, offset, sort_meta)
|
||||
|
||||
|
||||
@mutation.field("toggle_bookmark_shout")
|
||||
|
||||
@@ -33,8 +33,8 @@ async def load_shouts_coauthored(_: None, info: GraphQLResolveInfo, options: dic
|
||||
return []
|
||||
q = query_with_stat(info)
|
||||
q = q.where(Shout.authors.any(id=author_id))
|
||||
q, limit, offset = apply_options(q, options)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
q, limit, offset, sort_meta = apply_options(q, options)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset, sort_meta=sort_meta)
|
||||
|
||||
|
||||
@query.field("load_shouts_discussed")
|
||||
@@ -52,8 +52,8 @@ async def load_shouts_discussed(_: None, info: GraphQLResolveInfo, options: dict
|
||||
return []
|
||||
q = query_with_stat(info)
|
||||
options["filters"]["commented"] = True
|
||||
q, limit, offset = apply_options(q, options, author_id)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
q, limit, offset, sort_meta = apply_options(q, options, author_id)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset, sort_meta=sort_meta)
|
||||
|
||||
|
||||
def shouts_by_follower(info: GraphQLResolveInfo, follower_id: int, options: dict[str, Any]) -> list[Shout]:
|
||||
@@ -87,8 +87,8 @@ def shouts_by_follower(info: GraphQLResolveInfo, follower_id: int, options: dict
|
||||
.scalar_subquery()
|
||||
)
|
||||
q = q.where(Shout.id.in_(followed_subquery))
|
||||
q, limit, offset = apply_options(q, options)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
q, limit, offset, sort_meta = apply_options(q, options)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset, sort_meta=sort_meta)
|
||||
|
||||
|
||||
@query.field("load_shouts_followed_by")
|
||||
@@ -144,8 +144,8 @@ async def load_shouts_authored_by(_: None, info: GraphQLResolveInfo, slug: str,
|
||||
else select(Shout).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
)
|
||||
q = q.where(Shout.authors.any(id=author_id))
|
||||
q, limit, offset = apply_options(q, options, author_id)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
q, limit, offset, sort_meta = apply_options(q, options, author_id)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset, sort_meta=sort_meta)
|
||||
except Exception as error:
|
||||
logger.debug(error)
|
||||
return []
|
||||
@@ -172,8 +172,8 @@ async def load_shouts_with_topic(_: None, info: GraphQLResolveInfo, slug: str, o
|
||||
else select(Shout).where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
)
|
||||
q = q.where(Shout.topics.any(id=topic_id))
|
||||
q, limit, offset = apply_options(q, options)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset)
|
||||
q, limit, offset, sort_meta = apply_options(q, options)
|
||||
return get_shouts_with_links(info, q, limit, offset=offset, sort_meta=sort_meta)
|
||||
except Exception as error:
|
||||
logger.debug(error)
|
||||
return []
|
||||
|
||||
@@ -134,7 +134,9 @@ async def follow(
|
||||
# Создаем объект автора для использования метода dict
|
||||
temp_author = Author()
|
||||
for key, value in author_data.items():
|
||||
if hasattr(temp_author, key):
|
||||
if (
|
||||
hasattr(temp_author, key) and key != "username"
|
||||
): # username - это свойство, нельзя устанавливать
|
||||
setattr(temp_author, key, value)
|
||||
# Добавляем отфильтрованную версию
|
||||
follows_filtered.append(temp_author.dict())
|
||||
|
||||
@@ -17,7 +17,9 @@ from storage.schema import query
|
||||
from utils.logger import root_logger as logger
|
||||
|
||||
|
||||
def apply_options(q: Select, options: dict[str, Any], reactions_created_by: int = 0) -> tuple[Select, int, int]:
|
||||
def apply_options(
|
||||
q: Select, options: dict[str, Any], reactions_created_by: int = 0
|
||||
) -> tuple[Select, int, int, dict[str, Any]]:
|
||||
"""
|
||||
Применяет опции фильтрации и сортировки
|
||||
[опционально] выбирая те публикации, на которые есть реакции/комментарии от указанного автора
|
||||
@@ -25,7 +27,7 @@ def apply_options(q: Select, options: dict[str, Any], reactions_created_by: int
|
||||
:param q: Исходный запрос.
|
||||
:param options: Опции фильтрации и сортировки.
|
||||
:param reactions_created_by: Идентификатор автора.
|
||||
:return: Запрос с примененными опциями.
|
||||
:return: Запрос с примененными опциями + метаданные сортировки.
|
||||
"""
|
||||
filters = options.get("filters")
|
||||
if isinstance(filters, dict):
|
||||
@@ -35,10 +37,18 @@ def apply_options(q: Select, options: dict[str, Any], reactions_created_by: int
|
||||
q = q.where(Reaction.created_by == reactions_created_by)
|
||||
if "commented" in filters:
|
||||
q = q.where(Reaction.body.is_not(None))
|
||||
|
||||
# 🔎 Определяем, нужна ли Python-сортировка
|
||||
sort_meta = {
|
||||
"needs_python_sort": options.get("order_by") == "views_count",
|
||||
"order_by": options.get("order_by"),
|
||||
"order_by_desc": options.get("order_by_desc", True),
|
||||
}
|
||||
|
||||
q = apply_sorting(q, options)
|
||||
limit = options.get("limit", 10)
|
||||
offset = options.get("offset", 0)
|
||||
return q, limit, offset
|
||||
return q, limit, offset, sort_meta
|
||||
|
||||
|
||||
def has_field(info: GraphQLResolveInfo, fieldname: str) -> bool:
|
||||
@@ -185,13 +195,17 @@ def query_with_stat(info: GraphQLResolveInfo) -> Select:
|
||||
func.coalesce(stats_subquery.c.rating, 0),
|
||||
"last_commented_at",
|
||||
func.coalesce(stats_subquery.c.last_commented_at, 0),
|
||||
"views_count",
|
||||
0, # views_count будет заполнен в get_shouts_with_links из ViewedStorage
|
||||
).label("stat")
|
||||
)
|
||||
|
||||
return q
|
||||
|
||||
|
||||
def get_shouts_with_links(info: GraphQLResolveInfo, q: Select, limit: int = 20, offset: int = 0) -> list[Shout]:
|
||||
def get_shouts_with_links(
|
||||
info: GraphQLResolveInfo, q: Select, limit: int = 20, offset: int = 0, sort_meta: dict[str, Any] | None = None
|
||||
) -> list[Shout]:
|
||||
"""
|
||||
получение публикаций с применением пагинации
|
||||
"""
|
||||
@@ -305,7 +319,7 @@ def get_shouts_with_links(info: GraphQLResolveInfo, q: Select, limit: int = 20,
|
||||
elif isinstance(row.stat, dict):
|
||||
stat = row.stat
|
||||
viewed = ViewedStorage.get_shout(shout_id=shout_id) or 0
|
||||
shout_dict["stat"] = {**stat, "viewed": viewed}
|
||||
shout_dict["stat"] = {**stat, "views_count": viewed}
|
||||
|
||||
# Обработка main_topic и topics
|
||||
topics = None
|
||||
@@ -371,6 +385,15 @@ def get_shouts_with_links(info: GraphQLResolveInfo, q: Select, limit: int = 20,
|
||||
logger.error(f"Fatal error in get_shouts_with_links: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
# 🔎 Сортировка по views_count в Python после получения данных
|
||||
if sort_meta and sort_meta.get("needs_python_sort"):
|
||||
reverse_order = sort_meta.get("order_by_desc", True)
|
||||
shouts.sort(
|
||||
key=lambda shout: shout.get("stat", {}).get("views_count", 0) if isinstance(shout, dict) else 0,
|
||||
reverse=reverse_order,
|
||||
)
|
||||
logger.info(f"🔎 Applied Python sorting by views_count (desc={reverse_order})")
|
||||
|
||||
logger.info(f"Returning {len(shouts)} shouts from get_shouts_with_links")
|
||||
return shouts
|
||||
|
||||
@@ -453,6 +476,8 @@ async def get_shout(_: None, info: GraphQLResolveInfo, slug: str = "", shout_id:
|
||||
def apply_sorting(q: Select, options: dict[str, Any]) -> Select:
|
||||
"""
|
||||
Применение сортировки с сохранением порядка
|
||||
|
||||
views_count сортируется в Python в get_shouts_with_links, т.к. данные из Redis
|
||||
"""
|
||||
order_str = options.get("order_by")
|
||||
if order_str in ["rating", "comments_count", "last_commented_at"]:
|
||||
@@ -460,6 +485,9 @@ def apply_sorting(q: Select, options: dict[str, Any]) -> Select:
|
||||
q = q.distinct(text(order_str), Shout.id).order_by( # DISTINCT ON включает поле сортировки
|
||||
nulls_last(query_order_by), Shout.id
|
||||
)
|
||||
elif order_str == "views_count":
|
||||
# Для views_count сортируем в Python, здесь только базовая сортировка по id
|
||||
q = q.distinct(Shout.id).order_by(Shout.id)
|
||||
else:
|
||||
published_at_col = getattr(Shout, "published_at", Shout.id)
|
||||
q = q.distinct(published_at_col, Shout.id).order_by(published_at_col.desc(), Shout.id)
|
||||
@@ -481,10 +509,10 @@ async def load_shouts_by(_: None, info: GraphQLResolveInfo, options: dict[str, A
|
||||
q = query_with_stat(info)
|
||||
|
||||
# Применяем остальные опции фильтрации
|
||||
q, limit, offset = apply_options(q, options)
|
||||
q, limit, offset, sort_meta = apply_options(q, options)
|
||||
|
||||
# Передача сформированного запроса в метод получения публикаций с учетом сортировки и пагинации
|
||||
return get_shouts_with_links(info, q, limit, offset)
|
||||
return get_shouts_with_links(info, q, limit, offset, sort_meta)
|
||||
|
||||
|
||||
@query.field("load_shouts_search")
|
||||
|
||||
Reference in New Issue
Block a user