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