diff --git a/.gitignore b/.gitignore index 650774ab..b8332f4b 100644 --- a/.gitignore +++ b/.gitignore @@ -146,4 +146,5 @@ migration/content/**/*.md dump .vscode *dump.sql -*.csv \ No newline at end of file +*.csv +dev-server-status.txt diff --git a/auth/authenticate.py b/auth/authenticate.py index 4e8248c7..425b5d1d 100644 --- a/auth/authenticate.py +++ b/auth/authenticate.py @@ -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 ) diff --git a/auth/identity.py b/auth/identity.py index 197f9fd7..efcec0ec 100644 --- a/auth/identity.py +++ b/auth/identity.py @@ -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") diff --git a/auth/jwtcodec.py b/auth/jwtcodec.py index 5a6cfa4d..0c35dea5 100644 --- a/auth/jwtcodec.py +++ b/auth/jwtcodec.py @@ -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') diff --git a/auth/tokenstorage.py b/auth/tokenstorage.py index 2c84ca67..0d7a30e9 100644 --- a/auth/tokenstorage.py +++ b/auth/tokenstorage.py @@ -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 diff --git a/base/redis.py b/base/redis.py index 39f5d47a..7468af0a 100644 --- a/base/redis.py +++ b/base/redis.py @@ -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) diff --git a/main.py b/main.py index 7b29f901..4550028b 100644 --- a/main.py +++ b/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)) diff --git a/resolvers/auth.py b/resolvers/auth.py index b2923ce3..c167394b 100644 --- a/resolvers/auth.py +++ b/resolvers/auth.py @@ -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"} diff --git a/resolvers/inbox/load.py b/resolvers/inbox/load.py index 264e5c24..d3ca0ff1 100644 --- a/resolvers/inbox/load.py +++ b/resolvers/inbox/load.py @@ -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") diff --git a/resolvers/zine/load.py b/resolvers/zine/load.py index eea0408f..aef3c56e 100644 --- a/resolvers/zine/load.py +++ b/resolvers/zine/load.py @@ -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 diff --git a/resolvers/zine/reactions.py b/resolvers/zine/reactions.py index ee51dc20..1f4d48c2 100644 --- a/resolvers/zine/reactions.py +++ b/resolvers/zine/reactions.py @@ -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 diff --git a/resolvers/zine/topics.py b/resolvers/zine/topics.py index b5824fc0..81db4f91 100644 --- a/resolvers/zine/topics.py +++ b/resolvers/zine/topics.py @@ -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 diff --git a/schema.graphql b/schema.graphql index df0f465d..e42e61db 100644 --- a/schema.graphql +++ b/schema.graphql @@ -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 { diff --git a/server.py b/server.py index 8059d554..2cb70715 100644 --- a/server.py +++ b/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 diff --git a/services/auth/users.py b/services/auth/users.py index 0d76dfa8..df6a84ee 100644 --- a/services/auth/users.py +++ b/services/auth/users.py @@ -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 diff --git a/services/stat/reacted.py b/services/stat/reacted.py index 6258bfe7..ee3612fd 100644 --- a/services/stat/reacted.py +++ b/services/stat/reacted.py @@ -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 diff --git a/services/stat/viewed.py b/services/stat/viewed.py index ca3dabc1..ece3e8c1 100644 --- a/services/stat/viewed.py +++ b/services/stat/viewed.py @@ -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 diff --git a/settings.py b/settings.py index 6649b8ea..ccfdbed0 100644 --- a/settings.py +++ b/settings.py @@ -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'