This commit is contained in:
parent
1114c7766d
commit
62370b94b3
|
@ -29,23 +29,34 @@ from utils.logger import root_logger as logger
|
|||
|
||||
def query_shouts():
|
||||
"""
|
||||
Базовый запрос для получения публикаций с подзапросами статистики, авторов и тем,
|
||||
с агрегированием в JSON.
|
||||
Базовый запрос для получения публикаций с подзапросами статистики, авторов и тем.
|
||||
"""
|
||||
comments_reaction = aliased(Reaction, name="comments_reaction")
|
||||
ratings_reaction = aliased(Reaction, name="ratings_reaction")
|
||||
last_reaction = aliased(Reaction, name="last_reaction")
|
||||
# Подзапрос для реакций и статистики (объединяем только эту часть)
|
||||
reactions_subquery = (
|
||||
select(
|
||||
func.sum(
|
||||
case(
|
||||
(Reaction.kind == ReactionKind.LIKE.value, 1),
|
||||
(Reaction.kind == ReactionKind.DISLIKE.value, -1),
|
||||
else_=0,
|
||||
)
|
||||
).label("rating_stat"),
|
||||
func.count(distinct(case((Reaction.kind == ReactionKind.COMMENT.value, Reaction.id), else_=None))).label(
|
||||
"comments_stat"
|
||||
),
|
||||
func.max(Reaction.created_at).label("last_reacted_at"),
|
||||
)
|
||||
.select_from(Reaction)
|
||||
.where(and_(Reaction.shout == Shout.id, Reaction.reply_to.is_(None), Reaction.deleted_at.is_(None)))
|
||||
.correlate(Shout)
|
||||
.scalar_subquery()
|
||||
)
|
||||
|
||||
# Подзапрос для уникальных авторов, агрегированных в JSON
|
||||
# Остальные подзапросы оставляем как есть
|
||||
authors_subquery = (
|
||||
select(
|
||||
func.json_agg(
|
||||
func.json_build_object(
|
||||
"id", Author.id,
|
||||
"name", Author.name,
|
||||
"slug", Author.slug,
|
||||
"pic", Author.pic
|
||||
)
|
||||
func.json_build_object("id", Author.id, "name", Author.name, "slug", Author.slug, "pic", Author.pic)
|
||||
).label("authors")
|
||||
)
|
||||
.select_from(ShoutAuthor)
|
||||
|
@ -58,13 +69,9 @@ def query_shouts():
|
|||
# Подзапрос для уникальных тем, агрегированных в JSON
|
||||
topics_subquery = (
|
||||
select(
|
||||
func.json_agg(
|
||||
func.json_build_object(
|
||||
"id", Topic.id,
|
||||
"title", Topic.title,
|
||||
"slug", Topic.slug
|
||||
func.json_agg(func.json_build_object("id", Topic.id, "title", Topic.title, "slug", Topic.slug)).label(
|
||||
"topics"
|
||||
)
|
||||
).label("topics")
|
||||
)
|
||||
.select_from(ShoutTopic)
|
||||
.join(Topic, ShoutTopic.topic == Topic.id)
|
||||
|
@ -83,58 +90,30 @@ def query_shouts():
|
|||
.scalar_subquery()
|
||||
)
|
||||
|
||||
# Подзапрос для комментариев
|
||||
comments_subq = (
|
||||
select(func.count(distinct(comments_reaction.id)))
|
||||
.select_from(comments_reaction)
|
||||
.where(
|
||||
and_(
|
||||
comments_reaction.shout == Shout.id,
|
||||
comments_reaction.kind == ReactionKind.COMMENT.value,
|
||||
comments_reaction.deleted_at.is_(None),
|
||||
comments_reaction.reply_to.is_(None),
|
||||
)
|
||||
)
|
||||
.scalar_subquery()
|
||||
.label("comments_stat")
|
||||
)
|
||||
|
||||
# Подзапрос для рейтинга
|
||||
ratings_subq = (
|
||||
captions_subquery = (
|
||||
select(
|
||||
func.sum(
|
||||
case(
|
||||
(ratings_reaction.kind == ReactionKind.LIKE.value, 1),
|
||||
(ratings_reaction.kind == ReactionKind.DISLIKE.value, -1),
|
||||
else_=0,
|
||||
)
|
||||
)
|
||||
)
|
||||
.select_from(ratings_reaction)
|
||||
.where(
|
||||
and_(
|
||||
ratings_reaction.shout == Shout.id,
|
||||
ratings_reaction.reply_to.is_(None),
|
||||
ratings_reaction.deleted_at.is_(None),
|
||||
func.json_agg(func.json_build_object("author_id", Author.id, "caption", ShoutAuthor.caption)).label(
|
||||
"captions"
|
||||
)
|
||||
)
|
||||
.select_from(ShoutAuthor)
|
||||
.join(Author, ShoutAuthor.author == Author.id)
|
||||
.where(ShoutAuthor.shout == Shout.id)
|
||||
.correlate(Shout)
|
||||
.scalar_subquery()
|
||||
.label("rating_stat")
|
||||
)
|
||||
|
||||
# Основной запрос с использованием подзапросов
|
||||
# Основной запрос
|
||||
q = (
|
||||
select(
|
||||
Shout,
|
||||
comments_subq,
|
||||
ratings_subq,
|
||||
func.max(Reaction.created_at).label("last_reacted_at"),
|
||||
reactions_subquery,
|
||||
authors_subquery,
|
||||
captions_subquery,
|
||||
topics_subquery,
|
||||
main_topic_subquery,
|
||||
)
|
||||
.outerjoin(last_reaction, and_(last_reaction.shout == Shout.id, last_reaction.deleted_at.is_(None)))
|
||||
.outerjoin(ShoutReactionsFollower, ShoutReactionsFollower.shout == Shout.id)
|
||||
.outerjoin(Reaction, Reaction.shout == Shout.id)
|
||||
.where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None)))
|
||||
.group_by(Shout.id)
|
||||
)
|
||||
|
@ -144,140 +123,65 @@ def query_shouts():
|
|||
|
||||
def get_shouts_with_stats(q, limit=20, offset=0, author_id=None):
|
||||
"""
|
||||
Получение публикаций со статистикой, и подзапросами авторов и тем.
|
||||
Получение публикаций со статистикой.
|
||||
|
||||
:param q: Запрос
|
||||
:param limit: Ограничение на количество результатов.
|
||||
:param offset: Смещение для пагинации.
|
||||
:return: Список публикаций с включенной статистикой.
|
||||
:param q: Базовый запрос публикаций
|
||||
:param limit: Ограничение количества результатов
|
||||
:param offset: Смещение для пагинации
|
||||
:param author_id: Опциональный ID автора для фильтрации
|
||||
:return: Список публикаций с статистикой
|
||||
"""
|
||||
|
||||
# Скалярный подзапрос для авторов
|
||||
authors_subquery = (
|
||||
select(
|
||||
func.json_agg(
|
||||
func.json_build_object(
|
||||
"id", Author.id,
|
||||
"name", Author.name,
|
||||
"slug", Author.slug,
|
||||
"pic", Author.pic
|
||||
)
|
||||
).label("authors")
|
||||
)
|
||||
.select_from(ShoutAuthor)
|
||||
.join(Author, ShoutAuthor.author == Author.id)
|
||||
.where(ShoutAuthor.shout == Shout.id)
|
||||
.correlate(Shout)
|
||||
.scalar_subquery()
|
||||
)
|
||||
|
||||
# Подзапрос для captions
|
||||
captions_subquery = (
|
||||
select(
|
||||
func.json_agg(
|
||||
func.json_build_object(
|
||||
"author_id", Author.id,
|
||||
"caption", ShoutAuthor.caption
|
||||
)
|
||||
).label("captions")
|
||||
)
|
||||
.select_from(ShoutAuthor)
|
||||
.join(Author, ShoutAuthor.author == Author.id)
|
||||
.where(ShoutAuthor.shout == Shout.id)
|
||||
.correlate(Shout)
|
||||
.scalar_subquery()
|
||||
)
|
||||
|
||||
# Скалярный подзапрос для тем
|
||||
topics_subquery = (
|
||||
select(
|
||||
func.json_agg(
|
||||
func.json_build_object(
|
||||
"id", Topic.id,
|
||||
"title", Topic.title,
|
||||
"slug", Topic.slug
|
||||
)
|
||||
).label("topics"),
|
||||
)
|
||||
.select_from(ShoutTopic)
|
||||
.join(Topic, ShoutTopic.topic == Topic.id)
|
||||
.where(ShoutTopic.shout == Shout.id)
|
||||
.correlate(Shout)
|
||||
.scalar_subquery()
|
||||
)
|
||||
|
||||
# Скалярный подзапрос для основного топика
|
||||
main_topic_subquery = (
|
||||
select(func.max(Topic.slug).label("main_topic_slug"))
|
||||
.select_from(ShoutTopic)
|
||||
.join(Topic, ShoutTopic.topic == Topic.id)
|
||||
.where(
|
||||
and_(
|
||||
ShoutTopic.shout == Shout.id,
|
||||
ShoutTopic.main.is_(True),
|
||||
)
|
||||
)
|
||||
.correlate(Shout)
|
||||
.scalar_subquery()
|
||||
)
|
||||
|
||||
# Основной запрос
|
||||
query = (
|
||||
select(
|
||||
Shout,
|
||||
func.count(distinct(Reaction.id)).label("comments_stat"),
|
||||
func.sum(
|
||||
case(
|
||||
(Reaction.kind == ReactionKind.LIKE.value, 1),
|
||||
(Reaction.kind == ReactionKind.DISLIKE.value, -1),
|
||||
else_=0,
|
||||
)
|
||||
).label("rating_stat"),
|
||||
func.max(Reaction.created_at).label("last_reacted_at"),
|
||||
authors_subquery,
|
||||
captions_subquery,
|
||||
topics_subquery,
|
||||
main_topic_subquery,
|
||||
)
|
||||
.outerjoin(Reaction, Reaction.shout == Shout.id)
|
||||
.where(and_(Shout.published_at.is_not(None), Shout.deleted_at.is_(None), Shout.featured_at.is_not(None)))
|
||||
.group_by(Shout.id)
|
||||
.order_by(Shout.published_at.desc().nulls_last())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
|
||||
# Применение дополнительных фильтров, если необходимо
|
||||
if author_id:
|
||||
query = query.filter(Shout.created_by == author_id)
|
||||
q = q.filter(Shout.created_by == author_id)
|
||||
|
||||
q = q.order_by(Shout.published_at.desc().nulls_last())
|
||||
|
||||
if limit:
|
||||
q = q.limit(limit)
|
||||
if offset:
|
||||
q = q.offset(offset)
|
||||
|
||||
# Формирование списка публикаций с их данными
|
||||
shouts = []
|
||||
with local_session() as session:
|
||||
for [shout, comments_stat, rating_stat, last_reacted_at, authors_json, captions_json, topics_json, main_topic_slug] in (
|
||||
session.execute(query).all() or []
|
||||
):
|
||||
viewed_stat = ViewedStorage.get_shout(shout.id)
|
||||
results = session.execute(q).all()
|
||||
|
||||
for [shout, reactions_stat, authors_json, captions_json, topics_json, main_topic_slug] in results:
|
||||
# Базовые данные публикации
|
||||
shout_dict = shout.dict()
|
||||
# Преобразование JSON данных в объекты
|
||||
captions = {int(ca['author_id']): ca['caption'] for ca in captions_json} if captions_json else {}
|
||||
# patch json to add captions to authors
|
||||
authors = [
|
||||
{**a, "caption": captions.get(int(a["id"]), "")} for a in authors_json
|
||||
] if authors_json else []
|
||||
shout_dict["authors"] = authors
|
||||
# patch json to add is_main to topics
|
||||
topics = [
|
||||
{**t, "is_main": t["slug"] == main_topic_slug} for t in topics_json
|
||||
] if topics_json else []
|
||||
shout_dict["topics"] = topics
|
||||
shout_dict["stat"] = {
|
||||
|
||||
# Добавление статистики просмотров
|
||||
viewed_stat = ViewedStorage.get_shout(shout_slug=shout.slug)
|
||||
|
||||
# Обработка авторов и их подписей
|
||||
authors = authors_json or []
|
||||
captions = captions_json or []
|
||||
|
||||
# Объединяем авторов с их подписями
|
||||
for author in authors:
|
||||
caption_item = next((c for c in captions if c["author_id"] == author["id"]), None)
|
||||
if caption_item:
|
||||
author["caption"] = caption_item["caption"]
|
||||
|
||||
# Обработка тем
|
||||
topics = topics_json or []
|
||||
for topic in topics:
|
||||
topic["is_main"] = topic["slug"] == main_topic_slug
|
||||
|
||||
# Формирование финальной структуры
|
||||
shout_dict.update(
|
||||
{
|
||||
"authors": authors,
|
||||
"topics": topics,
|
||||
"main_topic": main_topic_slug,
|
||||
"stat": {
|
||||
"viewed": viewed_stat or 0,
|
||||
"rating": rating_stat or 0,
|
||||
"commented": comments_stat or 0,
|
||||
"last_reacted_at": last_reacted_at,
|
||||
"rating": reactions_stat.rating_stat or 0,
|
||||
"commented": reactions_stat.comments_stat or 0,
|
||||
"last_reacted_at": reactions_stat.last_reacted_at,
|
||||
},
|
||||
}
|
||||
shout_dict["main_topic"] = main_topic_slug # Присваиваем основной топик
|
||||
)
|
||||
|
||||
shouts.append(shout_dict)
|
||||
|
||||
return shouts
|
||||
|
@ -362,54 +266,34 @@ def apply_filters(q, filters, author_id=None):
|
|||
@query.field("get_shout")
|
||||
async def get_shout(_, _info, slug="", shout_id=0):
|
||||
"""
|
||||
Получение публикации по slug.
|
||||
Получение публикации по slug или id.
|
||||
|
||||
:param _: Корневой объект запроса (не используется).
|
||||
:param info: Информация о контексте GraphQL.
|
||||
:param slug: Уникальный идентификатор шута.
|
||||
:return: Данные шута с включенной статистикой.
|
||||
:param _: Корневой объект запроса (не используется)
|
||||
:param _info: Информация о контексте GraphQL
|
||||
:param slug: Уникальный идентификатор публикации
|
||||
:param shout_id: ID публикации
|
||||
:return: Данные публикации с включенной статистикой
|
||||
"""
|
||||
try:
|
||||
with local_session() as session:
|
||||
# Отключение автосохранения
|
||||
with session.no_autoflush:
|
||||
# Получаем базовый запрос с подзапросами статистики
|
||||
q = query_shouts()
|
||||
|
||||
# Применяем фильтр по slug или id
|
||||
if slug:
|
||||
q = q.where(Shout.slug == slug)
|
||||
elif shout_id:
|
||||
q = q.where(Shout.id == shout_id)
|
||||
else:
|
||||
return None
|
||||
|
||||
results = session.execute(q).first()
|
||||
if results:
|
||||
[
|
||||
shout,
|
||||
commented_stat,
|
||||
# followers_stat,
|
||||
rating_stat,
|
||||
last_reaction_at,
|
||||
authors_json,
|
||||
topics_json,
|
||||
main_topic_slug,
|
||||
] = results
|
||||
viewed_stat = ViewedStorage.get_shout(shout.id)
|
||||
shout_dict = shout.dict()
|
||||
shout_dict["stat"] = {
|
||||
"viewed": viewed_stat or 0,
|
||||
"commented": commented_stat or 0,
|
||||
"rating": rating_stat or 0,
|
||||
"last_reacted_at": last_reaction_at or 0,
|
||||
}
|
||||
# Получаем результат через get_shouts_with_stats с limit=1
|
||||
shouts = get_shouts_with_stats(q, limit=1)
|
||||
|
||||
shout_dict["authors"] = authors_json or []
|
||||
shout_dict["topics"] = topics_json or []
|
||||
shout_dict["main_topic"] = main_topic_slug
|
||||
# Возвращаем первую (и единственную) публикацию, если она найдена
|
||||
return shouts[0] if shouts else None
|
||||
|
||||
return shout_dict
|
||||
except Exception as _exc:
|
||||
import traceback
|
||||
|
||||
logger.error(traceback.format_exc())
|
||||
except Exception as exc:
|
||||
logger.error(f"Error in get_shout: {exc}", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
|
@ -524,7 +408,7 @@ async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0):
|
|||
rating_reaction.shout == Shout.id,
|
||||
rating_reaction.reply_to.is_(None),
|
||||
rating_reaction.deleted_at.is_(None),
|
||||
rating_reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value])
|
||||
rating_reaction.kind.in_([ReactionKind.LIKE.value, ReactionKind.DISLIKE.value]),
|
||||
)
|
||||
)
|
||||
.correlate(Shout)
|
||||
|
@ -538,12 +422,7 @@ async def load_shouts_unrated(_, info, limit: int = 50, offset: int = 0):
|
|||
q = q.add_columns(ratings_count)
|
||||
|
||||
# Фильтруем только опубликованные и не удаленные публикации
|
||||
q = q.filter(
|
||||
and_(
|
||||
Shout.deleted_at.is_(None),
|
||||
Shout.published_at.is_not(None)
|
||||
)
|
||||
)
|
||||
q = q.filter(and_(Shout.deleted_at.is_(None), Shout.published_at.is_not(None)))
|
||||
|
||||
# Сортируем по количеству оценок (по возрастанию) и случайно среди равных
|
||||
q = q.order_by(ratings_count.asc(), func.random())
|
||||
|
@ -618,10 +497,10 @@ async def load_shouts_coauthored(_, info, limit=50, offset=0):
|
|||
"""
|
||||
Загрузка публикаций, написанных в соавторстве с пользователем.
|
||||
|
||||
:param info: Информация о контексте GraphQL.
|
||||
:param info: Информаци<EFBFBD><EFBFBD> о контексте GraphQL.
|
||||
:param limit: Максимальное количество публикаций.
|
||||
:param offset: Смещение для пагинации.
|
||||
:return: Список публикаций в соавторстве.
|
||||
:return: Список публикаций в соавто<EFBFBD><EFBFBD>стве.
|
||||
"""
|
||||
author_id = info.context.get("author", {}).get("id")
|
||||
if not author_id:
|
||||
|
@ -639,7 +518,7 @@ async def load_shouts_discussed(_, info, limit=50, offset=0):
|
|||
|
||||
:param info: Информация о контексте GraphQL.
|
||||
:param limit: Максимальное количество публикаций.
|
||||
:param offset: Смещение для пагинации.
|
||||
:param offset: Смещене для пагинации.
|
||||
:return: Список публикаций, обсужденных пользователем.
|
||||
"""
|
||||
author_id = info.context.get("author", {}).get("id")
|
||||
|
|
|
@ -27,7 +27,8 @@ VIEWS_FILEPATH = "/dump/views.json"
|
|||
|
||||
class ViewedStorage:
|
||||
lock = asyncio.Lock()
|
||||
views_by_shout = {}
|
||||
views_by_shout_slug = {}
|
||||
views_by_shout_id = {}
|
||||
shouts_by_topic = {}
|
||||
shouts_by_author = {}
|
||||
views = None
|
||||
|
@ -83,9 +84,16 @@ class ViewedStorage:
|
|||
|
||||
with open(viewfile_path, "r") as file:
|
||||
precounted_views = json.load(file)
|
||||
self.views_by_shout.update(precounted_views)
|
||||
self.views_by_shout_slug.update(precounted_views)
|
||||
logger.info(f" * {len(precounted_views)} shouts with views was loaded.")
|
||||
|
||||
# get shout_id by slug
|
||||
with local_session() as session:
|
||||
for slug, views_count in self.views_by_shout_slug.items():
|
||||
shout_id = session.query(Shout.id).filter(Shout.slug == slug).scalar()
|
||||
if isinstance(shout_id, int):
|
||||
self.views_by_shout_id.update({shout_id: views_count})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"precounted views loading error: {e}")
|
||||
|
||||
|
@ -137,10 +145,10 @@ class ViewedStorage:
|
|||
self.disabled = True
|
||||
|
||||
@staticmethod
|
||||
def get_shout(shout_slug) -> int:
|
||||
"""Получение метрики просмотров shout по slug."""
|
||||
def get_shout(shout_slug="", shout_id=0) -> int:
|
||||
"""Получение метрики просмотров shout по slug или id."""
|
||||
self = ViewedStorage
|
||||
return self.views_by_shout.get(shout_slug, 0)
|
||||
return self.views_by_shout_slug.get(shout_slug, self.views_by_shout_id.get(shout_id, 0))
|
||||
|
||||
@staticmethod
|
||||
def get_shout_media(shout_slug) -> Dict[str, int]:
|
||||
|
|
Loading…
Reference in New Issue
Block a user