Merge pull request #46 from Discours/prepare-comments
Prepare comments WIP
This commit is contained in:
commit
1bb13eb1e0
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -146,4 +146,5 @@ migration/content/**/*.md
|
|||
dump
|
||||
.vscode
|
||||
*dump.sql
|
||||
*.csv
|
||||
*.csv
|
||||
dev-server-status.txt
|
||||
|
|
|
@ -9,7 +9,7 @@ from starlette.requests import HTTPConnection
|
|||
from auth.credentials import AuthCredentials, AuthUser
|
||||
from auth.jwtcodec import JWTCodec
|
||||
from auth.tokenstorage import TokenStorage
|
||||
from base.exceptions import InvalidToken
|
||||
from base.exceptions import ExpiredToken, InvalidToken
|
||||
from services.auth.users import UserStorage
|
||||
from settings import SESSION_TOKEN_HEADER
|
||||
|
||||
|
@ -28,16 +28,17 @@ class SessionToken:
|
|||
token is of specified type
|
||||
"""
|
||||
try:
|
||||
print('[auth.authenticate] session token verify')
|
||||
payload = JWTCodec.decode(token)
|
||||
except ExpiredSignatureError:
|
||||
payload = JWTCodec.decode(token, verify_exp=False)
|
||||
if not await cls.get(payload.user_id, token):
|
||||
raise InvalidToken("Session token has expired, please try again")
|
||||
raise ExpiredToken("Token signature has expired, please try again")
|
||||
except DecodeError as e:
|
||||
raise InvalidToken("token format error") from e
|
||||
else:
|
||||
if not await cls.get(payload.user_id, token):
|
||||
raise InvalidToken("Session token has expired, please login again")
|
||||
raise ExpiredToken("Session token has expired, please login again")
|
||||
return payload
|
||||
|
||||
@classmethod
|
||||
|
@ -58,6 +59,8 @@ class JWTAuthenticate(AuthenticationBackend):
|
|||
try:
|
||||
payload = await SessionToken.verify(token)
|
||||
except Exception as exc:
|
||||
print("[auth.authenticate] session token verify error")
|
||||
print(exc)
|
||||
return AuthCredentials(scopes=[], error_message=str(exc)), AuthUser(
|
||||
user_id=None
|
||||
)
|
||||
|
|
|
@ -81,6 +81,7 @@ class Identity:
|
|||
@staticmethod
|
||||
async def onetime(token: str) -> User:
|
||||
try:
|
||||
print('[auth.identity] using one time token')
|
||||
payload = JWTCodec.decode(token)
|
||||
if not await TokenStorage.exist(f"{payload.user_id}-{token}"):
|
||||
raise InvalidToken("Login token has expired, please login again")
|
||||
|
|
|
@ -8,10 +8,8 @@ from settings import JWT_ALGORITHM, JWT_SECRET_KEY
|
|||
class JWTCodec:
|
||||
@staticmethod
|
||||
def encode(user: AuthInput, exp: datetime) -> str:
|
||||
issued = int(datetime.now().timestamp())
|
||||
print('[jwtcodec] issued at %r' % issued)
|
||||
expires = int(exp.timestamp())
|
||||
print('[jwtcodec] expires at %r' % expires)
|
||||
expires = int(exp.timestamp() * 1000)
|
||||
issued = int(datetime.now().timestamp() * 1000)
|
||||
payload = {
|
||||
"user_id": user.id,
|
||||
"username": user.email or user.phone,
|
||||
|
@ -23,7 +21,7 @@ class JWTCodec:
|
|||
try:
|
||||
return jwt.encode(payload, JWT_SECRET_KEY, JWT_ALGORITHM)
|
||||
except Exception as e:
|
||||
print('[jwtcodec] JWT encode error %r' % e)
|
||||
print('[auth.jwtcodec] JWT encode error %r' % e)
|
||||
|
||||
@staticmethod
|
||||
def decode(token: str, verify_exp: bool = True) -> TokenPayload:
|
||||
|
@ -39,11 +37,13 @@ class JWTCodec:
|
|||
issuer="discours"
|
||||
)
|
||||
r = TokenPayload(**payload)
|
||||
print('[jwtcodec] debug payload %r' % r)
|
||||
print('[auth.jwtcodec] debug payload %r' % r)
|
||||
return r
|
||||
except jwt.InvalidIssuedAtError:
|
||||
print('[auth.jwtcodec] invalid issued at: %r' % r)
|
||||
raise ExpiredToken('check token issued time')
|
||||
except jwt.ExpiredSignatureError:
|
||||
print('[auth.jwtcodec] expired signature %r' % r)
|
||||
raise ExpiredToken('check token lifetime')
|
||||
except jwt.InvalidTokenError:
|
||||
raise InvalidToken('token is not valid')
|
||||
|
|
|
@ -36,7 +36,9 @@ class TokenStorage:
|
|||
|
||||
@staticmethod
|
||||
async def revoke(token: str) -> bool:
|
||||
payload = None
|
||||
try:
|
||||
print("[auth.tokenstorage] revoke token")
|
||||
payload = JWTCodec.decode(token)
|
||||
except: # noqa
|
||||
pass
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from aioredis import from_url
|
||||
|
||||
from asyncio import sleep
|
||||
from settings import REDIS_URL
|
||||
|
||||
|
||||
|
@ -21,7 +21,12 @@ class RedisCache:
|
|||
self._instance = None
|
||||
|
||||
async def execute(self, command, *args, **kwargs):
|
||||
return await self._instance.execute_command(command, *args, **kwargs)
|
||||
while not self._instance:
|
||||
await sleep(1)
|
||||
try:
|
||||
await self._instance.execute_command(command, *args, **kwargs)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def lrange(self, key, start, stop):
|
||||
return await self._instance.lrange(key, start, stop)
|
||||
|
|
21
main.py
21
main.py
|
@ -1,6 +1,6 @@
|
|||
import asyncio
|
||||
from importlib import import_module
|
||||
|
||||
from os.path import exists
|
||||
from ariadne import load_schema_from_path, make_executable_schema
|
||||
from ariadne.asgi import GraphQL
|
||||
from starlette.applications import Starlette
|
||||
|
@ -21,6 +21,8 @@ from services.stat.topicstat import TopicStat
|
|||
from services.stat.viewed import ViewedStorage
|
||||
from services.zine.gittask import GitTask
|
||||
from services.zine.shoutauthor import ShoutAuthorStorage
|
||||
from settings import DEV_SERVER_STATUS_FILE_NAME
|
||||
|
||||
import_module("resolvers")
|
||||
schema = make_executable_schema(load_schema_from_path("schema.graphql"), resolvers) # type: ignore
|
||||
|
||||
|
@ -45,6 +47,15 @@ async def start_up():
|
|||
git_task = asyncio.create_task(GitTask.git_task_worker())
|
||||
print(git_task)
|
||||
|
||||
async def dev_start_up():
|
||||
if exists(DEV_SERVER_STATUS_FILE_NAME):
|
||||
return
|
||||
else:
|
||||
with open(DEV_SERVER_STATUS_FILE_NAME, 'w', encoding='utf-8') as f:
|
||||
f.write('running')
|
||||
|
||||
await start_up()
|
||||
|
||||
|
||||
async def shutdown():
|
||||
await redis.disconnect()
|
||||
|
@ -64,3 +75,11 @@ app = Starlette(
|
|||
routes=routes,
|
||||
)
|
||||
app.mount("/", GraphQL(schema, debug=True))
|
||||
|
||||
dev_app = app = Starlette(
|
||||
debug=True,
|
||||
on_startup=[dev_start_up],
|
||||
middleware=middleware,
|
||||
routes=routes,
|
||||
)
|
||||
dev_app.mount("/", GraphQL(schema, debug=True))
|
||||
|
|
|
@ -42,6 +42,7 @@ async def get_current_user(_, info):
|
|||
async def confirm_email(_, info, token):
|
||||
"""confirm owning email address"""
|
||||
try:
|
||||
print('[resolvers.auth] confirm email by token')
|
||||
payload = JWTCodec.decode(token)
|
||||
user_id = payload.user_id
|
||||
await TokenStorage.get(f"{user_id}-{token}")
|
||||
|
@ -175,7 +176,7 @@ async def login(_, info, email: str, password: str = "", lang: str = "ru"):
|
|||
}
|
||||
except InvalidPassword:
|
||||
print(f"[auth] {email}: invalid password")
|
||||
raise InvalidPassword("invalid passoword") # contains webserver status
|
||||
raise InvalidPassword("invalid password") # contains webserver status
|
||||
# return {"error": "invalid password"}
|
||||
|
||||
|
||||
|
|
|
@ -33,18 +33,24 @@ async def load_messages(chatId: str, limit: int, offset: int):
|
|||
async def load_chats(_, info, limit: int, offset: int):
|
||||
""" load :limit chats of current user with :offset """
|
||||
user = info.context["request"].user
|
||||
chats = await redis.execute("GET", f"chats_by_user/{user.slug}")
|
||||
if chats:
|
||||
chats = list(json.loads(chats))[offset:offset + limit]
|
||||
if not chats:
|
||||
chats = []
|
||||
for c in chats:
|
||||
c['messages'] = await load_messages(c['id'], limit, offset)
|
||||
c['unread'] = await get_unread_counter(c['id'], user.slug)
|
||||
return {
|
||||
"chats": chats,
|
||||
"error": None
|
||||
}
|
||||
if user:
|
||||
chats = await redis.execute("GET", f"chats_by_user/{user.slug}")
|
||||
if chats:
|
||||
chats = list(json.loads(chats))[offset:offset + limit]
|
||||
if not chats:
|
||||
chats = []
|
||||
for c in chats:
|
||||
c['messages'] = await load_messages(c['id'], limit, offset)
|
||||
c['unread'] = await get_unread_counter(c['id'], user.slug)
|
||||
return {
|
||||
"chats": chats,
|
||||
"error": None
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"error": "please login",
|
||||
"chats": []
|
||||
}
|
||||
|
||||
|
||||
@query.field("loadMessagesBy")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from datetime import datetime, timedelta
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm import joinedload
|
||||
from sqlalchemy.sql.expression import desc, asc, select, case
|
||||
from base.orm import local_session
|
||||
from base.resolvers import query
|
||||
|
@ -32,42 +32,13 @@ def apply_filters(q, filters, user=None):
|
|||
return q
|
||||
|
||||
|
||||
def extract_order(o, q):
|
||||
if o:
|
||||
q = q.add_columns(sa.func.count(Reaction.id).label(o))
|
||||
if o == 'comments':
|
||||
q = q.join(Reaction, Shout.slug == Reaction.shout)
|
||||
q = q.filter(Reaction.body.is_not(None))
|
||||
elif o == 'reacted':
|
||||
q = q.join(
|
||||
Reaction
|
||||
).add_columns(
|
||||
sa.func.max(Reaction.createdAt).label(o)
|
||||
)
|
||||
elif o == "rating":
|
||||
q = q.join(Reaction).add_columns(sa.func.sum(case(
|
||||
(Reaction.kind == ReactionKind.AGREE, 1),
|
||||
(Reaction.kind == ReactionKind.DISAGREE, -1),
|
||||
(Reaction.kind == ReactionKind.PROOF, 1),
|
||||
(Reaction.kind == ReactionKind.DISPROOF, -1),
|
||||
(Reaction.kind == ReactionKind.ACCEPT, 1),
|
||||
(Reaction.kind == ReactionKind.REJECT, -1),
|
||||
(Reaction.kind == ReactionKind.LIKE, 1),
|
||||
(Reaction.kind == ReactionKind.DISLIKE, -1),
|
||||
else_=0
|
||||
)).label(o))
|
||||
return o
|
||||
else:
|
||||
return 'createdAt'
|
||||
|
||||
|
||||
@query.field("loadShout")
|
||||
async def load_shout(_, info, slug):
|
||||
with local_session() as session:
|
||||
shout = session.query(Shout).options(
|
||||
# TODO add cation
|
||||
selectinload(Shout.authors),
|
||||
selectinload(Shout.topics),
|
||||
joinedload(Shout.authors),
|
||||
joinedload(Shout.topics),
|
||||
).filter(
|
||||
Shout.slug == slug
|
||||
).filter(
|
||||
|
@ -77,6 +48,12 @@ async def load_shout(_, info, slug):
|
|||
return shout
|
||||
|
||||
|
||||
def map_result_item(result_item):
|
||||
shout = result_item[0]
|
||||
shout.rating = result_item[1]
|
||||
return shout
|
||||
|
||||
|
||||
@query.field("loadShouts")
|
||||
async def load_shouts_by(_, info, options):
|
||||
"""
|
||||
|
@ -100,16 +77,40 @@ async def load_shouts_by(_, info, options):
|
|||
"""
|
||||
|
||||
q = select(Shout).options(
|
||||
# TODO add caption
|
||||
selectinload(Shout.authors),
|
||||
selectinload(Shout.topics),
|
||||
joinedload(Shout.authors),
|
||||
joinedload(Shout.topics),
|
||||
).where(
|
||||
Shout.deletedAt.is_(None)
|
||||
)
|
||||
user = info.context["request"].user
|
||||
q = apply_filters(q, options.get("filters"), user)
|
||||
q = q.join(Reaction).add_columns(sa.func.sum(case(
|
||||
(Reaction.kind == ReactionKind.AGREE, 1),
|
||||
(Reaction.kind == ReactionKind.DISAGREE, -1),
|
||||
(Reaction.kind == ReactionKind.PROOF, 1),
|
||||
(Reaction.kind == ReactionKind.DISPROOF, -1),
|
||||
(Reaction.kind == ReactionKind.ACCEPT, 1),
|
||||
(Reaction.kind == ReactionKind.REJECT, -1),
|
||||
(Reaction.kind == ReactionKind.LIKE, 1),
|
||||
(Reaction.kind == ReactionKind.DISLIKE, -1),
|
||||
else_=0
|
||||
)).label('rating'))
|
||||
|
||||
order_by = extract_order(options.get("order_by"), q)
|
||||
o = options.get("order_by")
|
||||
if o:
|
||||
q = q.add_columns(sa.func.count(Reaction.id).label(o))
|
||||
if o == 'comments':
|
||||
q = q.join(Reaction, Shout.slug == Reaction.shout)
|
||||
q = q.filter(Reaction.body.is_not(None))
|
||||
elif o == 'reacted':
|
||||
q = q.join(
|
||||
Reaction
|
||||
).add_columns(
|
||||
sa.func.max(Reaction.createdAt).label(o)
|
||||
)
|
||||
order_by = o
|
||||
else:
|
||||
order_by = Shout.createdAt
|
||||
|
||||
order_by_desc = True if options.get('order_by_desc') is None else options.get('order_by_desc')
|
||||
|
||||
|
@ -119,10 +120,13 @@ async def load_shouts_by(_, info, options):
|
|||
q = q.group_by(Shout.id).order_by(query_order_by).limit(limit).offset(offset)
|
||||
|
||||
with local_session() as session:
|
||||
shouts = list(map(lambda r: r.Shout, session.execute(q)))
|
||||
for s in shouts:
|
||||
s.stat = await ReactedStorage.get_shout_stat(s.slug)
|
||||
for a in s.authors:
|
||||
a.caption = await ShoutAuthorStorage.get_author_caption(s.slug, a.slug)
|
||||
shouts = list(map(map_result_item, session.execute(q).unique()))
|
||||
|
||||
for shout in shouts:
|
||||
shout.stat = await ReactedStorage.get_shout_stat(shout.slug, shout.rating)
|
||||
|
||||
del shout.rating
|
||||
for author in shout.authors:
|
||||
author.caption = await ShoutAuthorStorage.get_author_caption(shout.slug, author.slug)
|
||||
|
||||
return shouts
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import and_, asc, desc, select, text, func
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy.orm import aliased
|
||||
|
||||
from auth.authenticate import login_required
|
||||
from base.orm import local_session
|
||||
from base.resolvers import mutation, query
|
||||
|
@ -23,12 +23,10 @@ async def get_reaction_stat(reaction_id):
|
|||
def reactions_follow(user: User, slug: str, auto=False):
|
||||
with local_session() as session:
|
||||
following = (
|
||||
session.query(ShoutReactionsFollower)
|
||||
.where(and_(
|
||||
session.query(ShoutReactionsFollower).where(and_(
|
||||
ShoutReactionsFollower.follower == user.slug,
|
||||
ShoutReactionsFollower.shout == slug
|
||||
))
|
||||
.first()
|
||||
)).first()
|
||||
)
|
||||
if not following:
|
||||
following = ShoutReactionsFollower.create(
|
||||
|
@ -43,12 +41,10 @@ def reactions_follow(user: User, slug: str, auto=False):
|
|||
def reactions_unfollow(user, slug):
|
||||
with local_session() as session:
|
||||
following = (
|
||||
session.query(ShoutReactionsFollower)
|
||||
.where(and_(
|
||||
session.query(ShoutReactionsFollower).where(and_(
|
||||
ShoutReactionsFollower.follower == user.slug,
|
||||
ShoutReactionsFollower.shout == slug
|
||||
))
|
||||
.first()
|
||||
)).first()
|
||||
)
|
||||
if following:
|
||||
session.delete(following)
|
||||
|
@ -200,43 +196,23 @@ async def delete_reaction(_, info, rid):
|
|||
return {}
|
||||
|
||||
|
||||
def prepare_reactions(q, by):
|
||||
""" query filters and order """
|
||||
if by.get("shout"):
|
||||
q = q.filter(Shout.slug == by["shout"])
|
||||
elif by.get("shouts"):
|
||||
q = q.filter(Shout.slug.in_(by["shouts"]))
|
||||
if by.get("createdBy"):
|
||||
q = q.filter(Reaction.createdBy == by.get("createdBy"))
|
||||
if by.get("topic"):
|
||||
q = q.filter(Shout.topics.contains(by["topic"]))
|
||||
if by.get("body"):
|
||||
if by["body"] is True:
|
||||
q = q.filter(func.length(Reaction.body) > 0)
|
||||
else:
|
||||
q = q.filter(Reaction.body.ilike(f'%{by["body"]}%'))
|
||||
if by.get("days"):
|
||||
before = datetime.now() - timedelta(days=int(by["days"]) or 30)
|
||||
q = q.filter(Reaction.createdAt > before)
|
||||
order_way = asc if by.get("sort", "").startswith("-") else desc
|
||||
order_field = by.get("sort") or Reaction.createdAt
|
||||
q = q.group_by(
|
||||
Reaction.id
|
||||
).order_by(
|
||||
order_way(order_field)
|
||||
)
|
||||
return q
|
||||
def map_result_item(result_item):
|
||||
reaction = result_item[0]
|
||||
user = result_item[1]
|
||||
reaction.createdBy = user
|
||||
return reaction
|
||||
|
||||
|
||||
@query.field("loadReactionsBy")
|
||||
async def load_reactions_by(_, info, by, limit=50, offset=0):
|
||||
async def load_reactions_by(_, _info, by, limit=50, offset=0):
|
||||
"""
|
||||
:param by: {
|
||||
:shout - filter by slug
|
||||
:shouts - filer by shouts luglist
|
||||
:createdBy - to filter by author
|
||||
:topic - to filter by topic
|
||||
:body - to search by body
|
||||
:search - to search by reactions' body
|
||||
:comment - true if body.length > 0
|
||||
:days - a number of days ago
|
||||
:sort - a fieldname to sort desc by default
|
||||
}
|
||||
|
@ -244,33 +220,45 @@ async def load_reactions_by(_, info, by, limit=50, offset=0):
|
|||
:param offset: int offset in this order
|
||||
:return: Reaction[]
|
||||
"""
|
||||
user = None
|
||||
try:
|
||||
user = info.context["request"].user
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
CreatedByUser = aliased(User)
|
||||
|
||||
q = select(
|
||||
Reaction
|
||||
).options(
|
||||
selectinload(Reaction.createdBy),
|
||||
selectinload(Reaction.shout)
|
||||
).join(
|
||||
User, Reaction.createdBy == User.slug
|
||||
).join(
|
||||
Shout, Reaction.shout == Shout.slug
|
||||
).where(
|
||||
Reaction.deletedAt.is_(None)
|
||||
Reaction, CreatedByUser
|
||||
).join(CreatedByUser, Reaction.createdBy == CreatedByUser.slug)
|
||||
|
||||
if by.get("shout"):
|
||||
q = q.filter(Reaction.shout == by["shout"])
|
||||
elif by.get("shouts"):
|
||||
q = q.filter(Reaction.shout.in_(by["shouts"]))
|
||||
if by.get("createdBy"):
|
||||
q = q.filter(Reaction.createdBy == by.get("createdBy"))
|
||||
if by.get("topic"):
|
||||
q = q.filter(Shout.topics.contains(by["topic"]))
|
||||
if by.get("comment"):
|
||||
q = q.filter(func.length(Reaction.body) > 0)
|
||||
if by.get('search', 0) > 2:
|
||||
q = q.filter(Reaction.body.ilike(f'%{by["body"]}%'))
|
||||
if by.get("days"):
|
||||
after = datetime.now() - timedelta(days=int(by["days"]) or 30)
|
||||
q = q.filter(Reaction.createdAt > after)
|
||||
order_way = asc if by.get("sort", "").startswith("-") else desc
|
||||
order_field = by.get("sort") or Reaction.createdAt
|
||||
q = q.group_by(
|
||||
Reaction.id, CreatedByUser.id
|
||||
).order_by(
|
||||
order_way(order_field)
|
||||
)
|
||||
q = prepare_reactions(q, by, user)
|
||||
|
||||
q = q.where(Reaction.deletedAt.is_(None))
|
||||
q = q.limit(limit).offset(offset)
|
||||
|
||||
rrr = []
|
||||
with local_session() as session:
|
||||
# post query stats and author's captions
|
||||
for r in list(map(lambda r: r.Reaction, session.execute(q))):
|
||||
r.stat = await get_reaction_stat(r.id)
|
||||
rrr.append(r)
|
||||
reactions = list(map(map_result_item, session.execute(q)))
|
||||
for reaction in reactions:
|
||||
reaction.stat = await get_reaction_stat(reaction.id)
|
||||
|
||||
if by.get("stat"):
|
||||
rrr.sort(lambda r: r.stat.get(by["stat"]) or r.createdAt)
|
||||
return rrr
|
||||
reactions.sort(lambda r: r.stat.get(by["stat"]) or r.createdAt)
|
||||
|
||||
return reactions
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import random
|
||||
|
||||
from sqlalchemy import and_
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import and_, select
|
||||
from auth.authenticate import login_required
|
||||
from base.orm import local_session
|
||||
from base.resolvers import mutation, query
|
||||
from orm import Shout
|
||||
from orm.topic import Topic, TopicFollower
|
||||
from services.zine.topics import TopicStorage
|
||||
from services.stat.reacted import ReactedStorage
|
||||
# from services.stat.reacted import ReactedStorage
|
||||
from services.stat.topicstat import TopicStat
|
||||
|
||||
|
||||
# from services.stat.viewed import ViewedStorage
|
||||
|
||||
|
||||
|
@ -18,9 +19,9 @@ async def get_topic_stat(slug):
|
|||
"authors": len(TopicStat.authors_by_topic.get(slug, {}).keys()),
|
||||
"followers": len(TopicStat.followers_by_topic.get(slug, {}).keys()),
|
||||
# "viewed": await ViewedStorage.get_topic(slug),
|
||||
"reacted": len(await ReactedStorage.get_topic(slug)),
|
||||
"commented": len(await ReactedStorage.get_topic_comments(slug)),
|
||||
"rating": await ReactedStorage.get_topic_rating(slug)
|
||||
# "reacted": len(await ReactedStorage.get_topic(slug)),
|
||||
# "commented": len(await ReactedStorage.get_topic_comments(slug)),
|
||||
# "rating": await ReactedStorage.get_topic_rating(slug)
|
||||
}
|
||||
|
||||
|
||||
|
@ -98,10 +99,10 @@ async def topic_unfollow(user, slug):
|
|||
with local_session() as session:
|
||||
sub = (
|
||||
session.query(TopicFollower)
|
||||
.filter(
|
||||
.filter(
|
||||
and_(TopicFollower.follower == user.slug, TopicFollower.topic == slug)
|
||||
)
|
||||
.first()
|
||||
.first()
|
||||
)
|
||||
if not sub:
|
||||
raise Exception("[resolvers.topics] follower not exist")
|
||||
|
@ -113,11 +114,8 @@ async def topic_unfollow(user, slug):
|
|||
|
||||
@query.field("topicsRandom")
|
||||
async def topics_random(_, info, amount=12):
|
||||
topics = await TopicStorage.get_topics_all()
|
||||
normalized_topics = []
|
||||
for topic in topics:
|
||||
topic.stat = await get_topic_stat(topic.slug)
|
||||
if topic.stat["shouts"] > 2:
|
||||
normalized_topics.append(topic)
|
||||
sample_length = min(len(normalized_topics), amount)
|
||||
return random.sample(normalized_topics, sample_length)
|
||||
with local_session() as session:
|
||||
q = select(Topic).join(Shout).group_by(Topic.id).having(sa.func.count(Shout.id) > 2).order_by(
|
||||
sa.func.random()).limit(amount)
|
||||
random_topics = list(map(lambda result_item: result_item.Topic, session.execute(q)))
|
||||
return random_topics
|
||||
|
|
|
@ -248,13 +248,14 @@ input LoadShoutsOptions {
|
|||
}
|
||||
|
||||
input ReactionBy {
|
||||
shout: String
|
||||
shout: String # slug
|
||||
shouts: [String]
|
||||
body: String
|
||||
topic: String
|
||||
createdBy: String
|
||||
days: Int
|
||||
sort: String
|
||||
search: String # fts on body
|
||||
comment: Boolean
|
||||
topic: String # topic.slug
|
||||
createdBy: String # user.slug
|
||||
days: Int # before
|
||||
sort: String # how to sort, default createdAt
|
||||
}
|
||||
################################### Query
|
||||
|
||||
|
@ -476,9 +477,9 @@ type TopicStat {
|
|||
followers: Int!
|
||||
authors: Int!
|
||||
# viewed: Int
|
||||
reacted: Int!
|
||||
commented: Int
|
||||
rating: Int
|
||||
# reacted: Int!
|
||||
#commented: Int
|
||||
# rating: Int
|
||||
}
|
||||
|
||||
type Topic {
|
||||
|
|
14
server.py
14
server.py
|
@ -1,8 +1,8 @@
|
|||
import sys
|
||||
|
||||
import os
|
||||
import uvicorn
|
||||
|
||||
from settings import PORT
|
||||
from settings import PORT, DEV_SERVER_STATUS_FILE_NAME
|
||||
|
||||
log_settings = {
|
||||
'version': 1,
|
||||
|
@ -54,6 +54,9 @@ if __name__ == "__main__":
|
|||
x = sys.argv[1]
|
||||
if x == "dev":
|
||||
print("DEV MODE")
|
||||
if os.path.exists(DEV_SERVER_STATUS_FILE_NAME):
|
||||
os.remove(DEV_SERVER_STATUS_FILE_NAME)
|
||||
|
||||
headers = [
|
||||
("Access-Control-Allow-Methods", "GET, POST, OPTIONS, HEAD"),
|
||||
("Access-Control-Allow-Origin", "http://localhost:3000"),
|
||||
|
@ -65,14 +68,15 @@ if __name__ == "__main__":
|
|||
("Access-Control-Allow-Credentials", "true"),
|
||||
]
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
"main:dev_app",
|
||||
host="localhost",
|
||||
port=8080,
|
||||
headers=headers,
|
||||
# log_config=LOGGING_CONFIG,
|
||||
log_level=None,
|
||||
access_log=True
|
||||
) # , ssl_keyfile="discours.key", ssl_certfile="discours.crt", reload=True)
|
||||
access_log=False,
|
||||
reload=True
|
||||
) # , ssl_keyfile="discours.key", ssl_certfile="discours.crt")
|
||||
elif x == "migrate":
|
||||
from migration import migrate
|
||||
|
||||
|
|
|
@ -23,10 +23,12 @@ class UserStorage:
|
|||
async def get_user(id):
|
||||
with local_session() as session:
|
||||
user = (
|
||||
session.query(User)
|
||||
.options(selectinload(User.roles), selectinload(User.ratings))
|
||||
.filter(User.id == id)
|
||||
.one()
|
||||
session.query(User).options(
|
||||
selectinload(User.roles),
|
||||
selectinload(User.ratings)
|
||||
).filter(
|
||||
User.id == id
|
||||
).one()
|
||||
)
|
||||
|
||||
return user
|
||||
|
|
|
@ -34,13 +34,15 @@ class ReactedStorage:
|
|||
modified_shouts = set([])
|
||||
|
||||
@staticmethod
|
||||
async def get_shout_stat(slug):
|
||||
async def get_shout_stat(slug, rating):
|
||||
viewed = int(await ViewedStorage.get_shout(slug))
|
||||
# print(viewed)
|
||||
return {
|
||||
# TODO
|
||||
"viewed": 0, # await ViewedStorage.get_shout(slug),
|
||||
"viewed": viewed,
|
||||
"reacted": len(await ReactedStorage.get_shout(slug)),
|
||||
"commented": len(await ReactedStorage.get_comments(slug)),
|
||||
"rating": await ReactedStorage.get_rating(slug),
|
||||
# "rating": await ReactedStorage.get_rating(slug),
|
||||
"rating": rating
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -3,7 +3,7 @@ from datetime import timedelta, timezone, datetime
|
|||
from gql import Client, gql
|
||||
from gql.transport.aiohttp import AIOHTTPTransport
|
||||
from base.orm import local_session
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import func
|
||||
from orm.shout import ShoutTopic
|
||||
from orm.viewed import ViewedEntry
|
||||
from ssl import create_default_context
|
||||
|
@ -119,12 +119,14 @@ class ViewedStorage:
|
|||
if not shout_views:
|
||||
shout_views = 0
|
||||
with local_session() as session:
|
||||
shout_views_q = select(func.sum(ViewedEntry.amount)).where(
|
||||
ViewedEntry.shout == shout_slug
|
||||
)
|
||||
shout_views = session.execute(shout_views_q)
|
||||
self.by_shouts[shout_slug] = shout_views
|
||||
self.update_topics(session, shout_slug)
|
||||
try:
|
||||
shout_views = session.query(func.sum(ViewedEntry.amount)).where(
|
||||
ViewedEntry.shout == shout_slug
|
||||
).all()[0][0]
|
||||
self.by_shouts[shout_slug] = shout_views
|
||||
self.update_topics(session, shout_slug)
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
return shout_views
|
||||
|
||||
|
|
|
@ -25,3 +25,6 @@ for provider in OAUTH_PROVIDERS:
|
|||
|
||||
SHOUTS_REPO = "content"
|
||||
SESSION_TOKEN_HEADER = "Authorization"
|
||||
|
||||
# for local development
|
||||
DEV_SERVER_STATUS_FILE_NAME = 'dev-server-status.txt'
|
||||
|
|
Loading…
Reference in New Issue
Block a user