Merge branch 'main' of github.com:Discours/discours-backend

This commit is contained in:
Untone 2023-12-24 17:26:17 +03:00
commit c30001547a
3 changed files with 234 additions and 62 deletions

View File

@ -1,8 +1,17 @@
import json import json
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta
from sqlalchemy.orm import aliased, joinedload from sqlalchemy.orm import aliased, joinedload
from sqlalchemy.sql.expression import and_, asc, case, desc, func, nulls_last, select from sqlalchemy.sql.expression import (
and_,
asc,
case,
desc,
distinct,
func,
nulls_last,
select,
)
from auth.authenticate import login_required from auth.authenticate import login_required
from auth.credentials import AuthCredentials from auth.credentials import AuthCredentials
@ -13,6 +22,42 @@ from orm import TopicFollower
from orm.reaction import Reaction, ReactionKind from orm.reaction import Reaction, ReactionKind
from orm.shout import Shout, ShoutAuthor, ShoutTopic from orm.shout import Shout, ShoutAuthor, ShoutTopic
from orm.user import AuthorFollower from orm.user import AuthorFollower
from resolvers.zine.topics import get_random_topic
def get_shouts_from_query(q):
shouts = []
with local_session() as session:
for [shout, reacted_stat, commented_stat, rating_stat, last_comment] in session.execute(
q
).unique():
shouts.append(shout)
shout.stat = {
"viewed": shout.views,
"reacted": reacted_stat,
"commented": commented_stat,
"rating": rating_stat,
}
return shouts
def get_rating_func(aliased_reaction):
return func.sum(
case(
# do not count comments' reactions
(aliased_reaction.replyTo.is_not(None), 0),
(aliased_reaction.kind == ReactionKind.AGREE, 1),
(aliased_reaction.kind == ReactionKind.DISAGREE, -1),
(aliased_reaction.kind == ReactionKind.PROOF, 1),
(aliased_reaction.kind == ReactionKind.DISPROOF, -1),
(aliased_reaction.kind == ReactionKind.ACCEPT, 1),
(aliased_reaction.kind == ReactionKind.REJECT, -1),
(aliased_reaction.kind == ReactionKind.LIKE, 1),
(aliased_reaction.kind == ReactionKind.DISLIKE, -1),
else_=0,
)
)
def add_stat_columns(q): def add_stat_columns(q):
@ -23,21 +68,7 @@ def add_stat_columns(q):
func.sum(case((aliased_reaction.kind == ReactionKind.COMMENT, 1), else_=0)).label( func.sum(case((aliased_reaction.kind == ReactionKind.COMMENT, 1), else_=0)).label(
"commented_stat" "commented_stat"
), ),
func.sum( get_rating_func(aliased_reaction).label("rating_stat"),
case(
# do not count comments' reactions
(aliased_reaction.replyTo.is_not(None), 0),
(aliased_reaction.kind == ReactionKind.AGREE, 1),
(aliased_reaction.kind == ReactionKind.DISAGREE, -1),
(aliased_reaction.kind == ReactionKind.PROOF, 1),
(aliased_reaction.kind == ReactionKind.DISPROOF, -1),
(aliased_reaction.kind == ReactionKind.ACCEPT, 1),
(aliased_reaction.kind == ReactionKind.REJECT, -1),
(aliased_reaction.kind == ReactionKind.LIKE, 1),
(aliased_reaction.kind == ReactionKind.DISLIKE, -1),
else_=0,
)
).label("rating_stat"),
func.max( func.max(
case( case(
(aliased_reaction.kind != ReactionKind.COMMENT, None), (aliased_reaction.kind != ReactionKind.COMMENT, None),
@ -49,7 +80,8 @@ def add_stat_columns(q):
return q return q
def apply_filters(q, filters, user_id=None): # noqa: C901 # use_published_date is a quick fix, will be reworked as a part of tech debt
def apply_filters(q, filters, user_id=None, use_published_date=False): # noqa: C901
if filters.get("reacted") and user_id: if filters.get("reacted") and user_id:
q.join(Reaction, Reaction.createdBy == user_id) q.join(Reaction, Reaction.createdBy == user_id)
@ -67,14 +99,20 @@ def apply_filters(q, filters, user_id=None): # noqa: C901
q = q.filter(Shout.authors.any(slug=filters.get("author"))) q = q.filter(Shout.authors.any(slug=filters.get("author")))
if filters.get("topic"): if filters.get("topic"):
q = q.filter(Shout.topics.any(slug=filters.get("topic"))) q = q.filter(Shout.topics.any(slug=filters.get("topic")))
if filters.get("title"): if filters.get("fromDate"):
q = q.filter(Shout.title.ilike(f'%{filters.get("title")}%')) # fromDate: '2022-12-31
if filters.get("body"): date_from = datetime.strptime(filters.get("fromDate"), "%Y-%m-%d")
q = q.filter(Shout.body.ilike(f'%{filters.get("body")}%s')) if use_published_date:
if filters.get("days"): q = q.filter(Shout.publishedAt >= date_from)
before = datetime.now(tz=timezone.utc) - timedelta(days=int(filters.get("days")) or 30) else:
q = q.filter(Shout.createdAt > before) q = q.filter(Shout.createdAt >= date_from)
if filters.get("toDate"):
# toDate: '2023-12-31'
date_to = datetime.strptime(filters.get("toDate"), "%Y-%m-%d")
if use_published_date:
q = q.filter(Shout.publishedAt < (date_to + timedelta(days=1)))
else:
q = q.filter(Shout.createdAt < (date_to + timedelta(days=1)))
return q return q
@ -136,7 +174,8 @@ async def load_shouts_by(_, info, options):
topic: 'culture', topic: 'culture',
title: 'something', title: 'something',
body: 'something else', body: 'something else',
days: 30 fromDate: '2022-12-31',
toDate: '2023-12-31'
} }
offset: 0 offset: 0
limit: 50 limit: 50
@ -169,23 +208,143 @@ async def load_shouts_by(_, info, options):
q = q.group_by(Shout.id).order_by(nulls_last(query_order_by)).limit(limit).offset(offset) q = q.group_by(Shout.id).order_by(nulls_last(query_order_by)).limit(limit).offset(offset)
shouts = [] return get_shouts_from_query(q)
with local_session() as session:
shouts_map = {}
for [shout, reacted_stat, commented_stat, rating_stat, last_comment] in session.execute(
q
).unique():
shouts.append(shout)
shout.stat = {
"viewed": shout.views,
"reacted": reacted_stat,
"commented": commented_stat,
"rating": rating_stat,
}
shouts_map[shout.id] = shout
return shouts @query.field("loadRandomTopShouts")
async def load_random_top_shouts(_, info, params):
"""
:param params: {
filters: {
layout: 'music',
excludeLayout: 'article',
fromDate: '2022-12-31'
toDate: '2023-12-31'
}
fromRandomCount: 100,
limit: 50
}
:return: Shout[]
"""
aliased_reaction = aliased(Reaction)
subquery = (
select(Shout.id)
.outerjoin(aliased_reaction)
.where(and_(Shout.deletedAt.is_(None), Shout.layout.is_not(None)))
)
subquery = apply_filters(subquery, params.get("filters", {}), use_published_date=True)
subquery = subquery.group_by(Shout.id).order_by(desc(get_rating_func(aliased_reaction)))
from_random_count = params.get("fromRandomCount")
if from_random_count:
subquery = subquery.limit(from_random_count)
q = (
select(Shout)
.options(
joinedload(Shout.authors),
joinedload(Shout.topics),
)
.where(Shout.id.in_(subquery))
)
q = add_stat_columns(q)
limit = params.get("limit", 10)
q = q.group_by(Shout.id).order_by(func.random()).limit(limit)
# print(q.compile(compile_kwargs={"literal_binds": True}))
return get_shouts_from_query(q)
@query.field("loadRandomTopicShouts")
async def load_random_topic_shouts(_, info, limit):
topic = get_random_topic()
q = (
select(Shout)
.options(
joinedload(Shout.authors),
joinedload(Shout.topics),
)
.join(ShoutTopic, and_(Shout.id == ShoutTopic.shout, ShoutTopic.topic == topic.id))
.where(
and_(Shout.deletedAt.is_(None), Shout.layout.is_not(None), Shout.visibility == "public")
)
)
q = add_stat_columns(q)
q = q.group_by(Shout.id).order_by(desc(Shout.createdAt)).limit(limit)
shouts = get_shouts_from_query(q)
return {"topic": topic, "shouts": shouts}
@query.field("loadUnratedShouts")
async def load_unrated_shouts(_, info, limit):
auth: AuthCredentials = info.context["request"].auth
user_id = auth.user_id
aliased_reaction = aliased(Reaction)
q = (
select(Shout)
.options(
joinedload(Shout.authors),
joinedload(Shout.topics),
)
.outerjoin(
Reaction,
and_(
Reaction.shout == Shout.id,
Reaction.replyTo.is_(None),
Reaction.kind.in_([ReactionKind.LIKE, ReactionKind.DISLIKE]),
),
)
)
if user_id:
q = q.outerjoin(
aliased_reaction,
and_(
aliased_reaction.shout == Shout.id,
aliased_reaction.replyTo.is_(None),
aliased_reaction.kind.in_([ReactionKind.LIKE, ReactionKind.DISLIKE]),
aliased_reaction.createdBy == user_id,
),
)
q = q.where(
and_(
Shout.deletedAt.is_(None),
Shout.layout.is_not(None),
Shout.createdAt >= (datetime.now() - timedelta(days=14)).date(),
)
)
if user_id:
q = q.where(Shout.createdBy != user_id)
# 3 or fewer votes is 0, 1, 2 or 3 votes (null, reaction id1, reaction id2, reaction id3)
q = q.having(func.count(distinct(Reaction.id)) <= 4)
if user_id:
q = q.having(func.count(distinct(aliased_reaction.id)) == 0)
q = add_stat_columns(q)
q = q.group_by(Shout.id).order_by(func.random()).limit(limit)
# print(q.compile(compile_kwargs={"literal_binds": True}))
return get_shouts_from_query(q)
@query.field("loadDrafts") @query.field("loadDrafts")
@ -256,17 +415,4 @@ async def get_my_feed(_, info, options):
# print(q.compile(compile_kwargs={"literal_binds": True})) # print(q.compile(compile_kwargs={"literal_binds": True}))
shouts = [] return get_shouts_from_query(q)
with local_session() as session:
for [shout, reacted_stat, commented_stat, rating_stat, last_comment] in session.execute(
q
).unique():
shouts.append(shout)
shout.stat = {
"viewed": shout.views,
"reacted": reacted_stat,
"commented": commented_stat,
"rating": rating_stat,
}
return shouts

View File

@ -12,11 +12,12 @@ from orm.topic import Topic, TopicFollower
def add_topic_stat_columns(q): def add_topic_stat_columns(q):
aliased_shout_author = aliased(ShoutAuthor) aliased_shout_author = aliased(ShoutAuthor)
aliased_topic_follower = aliased(TopicFollower) aliased_topic_follower = aliased(TopicFollower)
aliased_shout_topic = aliased(ShoutTopic)
q = ( q = (
q.outerjoin(ShoutTopic, Topic.id == ShoutTopic.topic) q.outerjoin(aliased_shout_topic, Topic.id == aliased_shout_topic.topic)
.add_columns(func.count(distinct(ShoutTopic.shout)).label("shouts_stat")) .add_columns(func.count(distinct(aliased_shout_topic.shout)).label("shouts_stat"))
.outerjoin(aliased_shout_author, ShoutTopic.shout == aliased_shout_author.shout) .outerjoin(aliased_shout_author, aliased_shout_topic.shout == aliased_shout_author.shout)
.add_columns(func.count(distinct(aliased_shout_author.user)).label("authors_stat")) .add_columns(func.count(distinct(aliased_shout_author.user)).label("authors_stat"))
.outerjoin(aliased_topic_follower) .outerjoin(aliased_topic_follower)
.add_columns(func.count(distinct(aliased_topic_follower.follower)).label("followers_stat")) .add_columns(func.count(distinct(aliased_topic_follower.follower)).label("followers_stat"))
@ -146,6 +147,18 @@ def topic_unfollow(user_id, slug):
return False return False
def get_random_topic():
q = select(Topic)
q = q.join(ShoutTopic)
q = q.group_by(Topic.id)
q = q.having(func.count(distinct(ShoutTopic.shout)) > 10)
q = q.order_by(func.random()).limit(1)
with local_session() as session:
[topic] = session.execute(q).first()
return topic
@query.field("topicsRandom") @query.field("topicsRandom")
async def topics_random(_, info, amount=12): async def topics_random(_, info, amount=12):
q = select(Topic) q = select(Topic)

View File

@ -212,14 +212,13 @@ input AuthorsBy {
} }
input LoadShoutsFilters { input LoadShoutsFilters {
title: String
body: String
topic: String topic: String
author: String author: String
layout: String layout: String
excludeLayout: String excludeLayout: String
visibility: String visibility: String
days: Int fromDate: String
toDate: String
reacted: Boolean reacted: Boolean
} }
@ -232,6 +231,12 @@ input LoadShoutsOptions {
order_by_desc: Boolean order_by_desc: Boolean
} }
input LoadRandomTopShoutsParams {
filters: LoadShoutsFilters
limit: Int!
fromRandomCount: Int
}
input ReactionBy { input ReactionBy {
shout: String # slug shout: String # slug
shouts: [String] shouts: [String]
@ -259,6 +264,11 @@ type MySubscriptionsQueryResult {
authors: [Author]! authors: [Author]!
} }
type RandomTopicShoutsQueryResult {
topic: Topic!
shouts: [Shout]!
}
type Query { type Query {
# inbox # inbox
loadChats( limit: Int, offset: Int): Result! # your chats loadChats( limit: Int, offset: Int): Result! # your chats
@ -276,6 +286,9 @@ type Query {
loadAuthorsBy(by: AuthorsBy, limit: Int, offset: Int): [Author]! loadAuthorsBy(by: AuthorsBy, limit: Int, offset: Int): [Author]!
loadShout(slug: String, shout_id: Int): Shout loadShout(slug: String, shout_id: Int): Shout
loadShouts(options: LoadShoutsOptions): [Shout]! loadShouts(options: LoadShoutsOptions): [Shout]!
loadRandomTopShouts(params: LoadRandomTopShoutsParams): [Shout]!
loadRandomTopicShouts(limit: Int!): RandomTopicShoutsQueryResult!
loadUnratedShouts(limit: Int!): [Shout]!
loadDrafts: [Shout]! loadDrafts: [Shout]!
loadReactionsBy(by: ReactionBy!, limit: Int, offset: Int): [Reaction]! loadReactionsBy(by: ReactionBy!, limit: Int, offset: Int): [Reaction]!
userFollowers(slug: String!): [Author]! userFollowers(slug: String!): [Author]!