diff --git a/.gitea/workflows/main.yml b/.gitea/workflows/main.yml index 72bc64cd..6645fdd2 100644 --- a/.gitea/workflows/main.yml +++ b/.gitea/workflows/main.yml @@ -1,10 +1,5 @@ -name: 'Deploy to core' -on: - push: - branches: - - main - - dev - +name: 'Deploy on push' +on: [push] jobs: deploy: runs-on: ubuntu-latest @@ -25,6 +20,6 @@ jobs: - name: Push to dokku uses: dokku/github-action@master with: - branch: 'dev' - git_remote_url: 'ssh://dokku@v2.discours.io:22/core' + branch: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && 'main' || 'dev' }} + git_remote_url: ${{ github.ref == 'refs/heads/dev' && 'ssh://dokku@v2.discours.io:22/core' || 'ssh://dokku@staging.discours.io:22/core' }} ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} diff --git a/resolvers/author.py b/resolvers/author.py index c228c9ca..51d72d4b 100644 --- a/resolvers/author.py +++ b/resolvers/author.py @@ -1,6 +1,5 @@ import json import time -from typing import List from sqlalchemy import and_, desc, select, or_, distinct, func from sqlalchemy.orm import aliased @@ -131,8 +130,12 @@ def load_author_with_stats(q): .count() ) author.stat['rating'] = likes_count - dislikes_count - author.stat['rating_shouts'] = count_author_shouts_rating(session, author.id) - author.stat['rating_comments'] = count_author_comments_rating(session, author.id) + author.stat['rating_shouts'] = count_author_shouts_rating( + session, author.id + ) + author.stat['rating_comments'] = count_author_comments_rating( + session, author.id + ) author.stat['commented'] = comments_count return author @@ -205,9 +208,7 @@ def load_authors_by(_, _info, by, limit, offset): @query.field('get_author_follows') -def get_author_follows( - _, _info, slug='', user=None, author_id=None -) -> List[Author]: +def get_author_follows(_, _info, slug='', user=None, author_id=None): with local_session() as session: if not user and (author_id or slug): user_query_result = ( @@ -270,28 +271,26 @@ def create_author(user_id: str, slug: str, name: str = ''): @query.field('get_author_followers') def get_author_followers(_, _info, slug): author_alias = aliased(Author) - author_follower_alias = aliased(AuthorFollower) - shout_author_alias = aliased(ShoutAuthor) + alias_author_followers = aliased(AuthorFollower) + alias_author_authors = aliased(AuthorFollower) + alias_author_follower_followers = aliased(AuthorFollower) + alias_shout_author = aliased(ShoutAuthor) q = ( select(author_alias) - .join(author_follower_alias, author_follower_alias.author == author_alias.id) - .join(Author, Author.id == author_follower_alias.follower) + .join(alias_author_authors, alias_author_authors.follower_id == author_alias.id) + .join( + alias_author_followers, alias_author_followers.author_id == author_alias.id + ) .filter(author_alias.slug == slug) .add_columns( - func.count(distinct(shout_author_alias.shout)).label('shouts_stat'), - func.count(distinct(author_follower_alias.author)).label('authors_stat'), - func.count(distinct(author_follower_alias.follower)).label('followers_stat') - ) - .outerjoin(shout_author_alias, author_alias.id == shout_author_alias.author) - .outerjoin( - aliased(AuthorFollower, name="author_follower_1"), - author_follower_alias.follower == author_alias.id - ) - .outerjoin( - aliased(AuthorFollower, name="author_follower_2"), - author_follower_alias.author == author_alias.id + func.count(distinct(alias_shout_author.shout)).label('shouts_stat'), + func.count(distinct(alias_author_authors.author_id)).label('authors_stat'), + func.count(distinct(alias_author_follower_followers.follower_id)).label( + 'followers_stat' + ), ) + .outerjoin(alias_shout_author, author_alias.id == alias_shout_author.author_id) .group_by(author_alias.id) ) diff --git a/resolvers/editor.py b/resolvers/editor.py index f5a41934..384e1269 100644 --- a/resolvers/editor.py +++ b/resolvers/editor.py @@ -60,10 +60,18 @@ def create_shout(_, info, inp): 'published_at': None, 'created_at': current_time, # Set created_at as Unix timestamp } - same_slug_shout = session.query(Shout).filter(Shout.slug == shout_dict.get('slug')).first() + same_slug_shout = ( + session.query(Shout) + .filter(Shout.slug == shout_dict.get('slug')) + .first() + ) c = 1 while same_slug_shout is not None: - same_slug_shout = session.query(Shout).filter(Shout.slug == shout_dict.get('slug')).first() + same_slug_shout = ( + session.query(Shout) + .filter(Shout.slug == shout_dict.get('slug')) + .first() + ) c += 1 shout_dict['slug'] += f'-{c}' new_shout = Shout(**shout_dict) @@ -174,10 +182,18 @@ async def update_shout(_, info, shout_id, shout_input=None, publish=False): if slug: shout_by_id = session.query(Shout).filter(Shout.id == shout_id).first() if shout_by_id and slug != shout_by_id.slug: - same_slug_shout = session.query(Shout).filter(Shout.slug == shout_input.get('slug')).first() + same_slug_shout = ( + session.query(Shout) + .filter(Shout.slug == shout_input.get('slug')) + .first() + ) c = 1 while same_slug_shout is not None: - same_slug_shout = session.query(Shout).filter(Shout.slug == shout_input.get('slug')).first() + same_slug_shout = ( + session.query(Shout) + .filter(Shout.slug == shout_input.get('slug')) + .first() + ) c += 1 slug += f'-{c}' shout_input['slug'] = slug diff --git a/resolvers/follower.py b/resolvers/follower.py index 8db05c3a..ff0a8b6a 100644 --- a/resolvers/follower.py +++ b/resolvers/follower.py @@ -2,19 +2,18 @@ import json import time from typing import List -from sqlalchemy import select, or_, func -from sqlalchemy.orm import aliased +from sqlalchemy import select, or_ from sqlalchemy.sql import and_ from orm.author import Author, AuthorFollower # from orm.community import Community from orm.reaction import Reaction -from orm.shout import Shout, ShoutReactionsFollower, ShoutAuthor, ShoutTopic +from orm.shout import Shout, ShoutReactionsFollower from orm.topic import Topic, TopicFollower from resolvers.community import community_follow, community_unfollow from resolvers.topic import topic_follow, topic_unfollow -from resolvers.stat import get_authors_with_stat +from resolvers.stat import get_authors_with_stat, query_follows from services.auth import login_required from services.db import local_session from services.follows import DEFAULT_FOLLOWS @@ -86,66 +85,11 @@ async def unfollow(_, info, what, slug): return {} -def query_follows(user_id: str): - logger.debug(f'query follows for {user_id} from database') - topics = [] - authors = [] - with local_session() as session: - author = session.query(Author).filter(Author.user == user_id).first() - if isinstance(author, Author): - author_id = author.id - aliased_author = aliased(Author) - aliased_author_followers = aliased(AuthorFollower) - aliased_author_authors = aliased(AuthorFollower) - - authors = ( - session.query( - aliased_author, - func.count(func.distinct(ShoutAuthor.shout)).label("shouts_stat"), - func.count(func.distinct(AuthorFollower.author)).label("authors_stat"), - func.count(func.distinct(AuthorFollower.follower)).label("followers_stat") - ) - .select_from(aliased_author) - .filter(AuthorFollower.author == aliased_author.id) - .join(AuthorFollower, AuthorFollower.follower == aliased_author.id) - .outerjoin(ShoutAuthor, ShoutAuthor.author == author_id) - .outerjoin(aliased_author_authors, AuthorFollower.follower == author_id) - .outerjoin(aliased_author_followers, AuthorFollower.author == author_id) - .group_by(aliased_author.id) - .all() - ) - - aliased_shout_authors = aliased(ShoutAuthor) - aliased_topic_followers = aliased(TopicFollower) - aliased_topic = aliased(Topic) - topics = ( - session.query( - aliased_topic, - func.count(func.distinct(ShoutTopic.shout)).label("shouts_stat"), - func.count(func.distinct(ShoutAuthor.author)).label("authors_stat"), - func.count(func.distinct(TopicFollower.follower)).label("followers_stat") - ) - .select_from(aliased_topic) - .join(TopicFollower, TopicFollower.topic == aliased_topic.id) - .outerjoin(ShoutTopic, aliased_topic.id == ShoutTopic.topic) - .outerjoin(aliased_shout_authors, ShoutTopic.shout == aliased_shout_authors.shout) - .outerjoin(aliased_topic_followers, aliased_topic_followers.topic == aliased_topic.id) - .group_by(aliased_topic.id) - .all() - ) - - return { - 'topics': topics, - 'authors': authors, - 'communities': [{'id': 1, 'name': 'Дискурс', 'slug': 'discours'}], - } - - async def get_follows_by_user_id(user_id: str): if user_id: author = await redis.execute('GET', f'user:{user_id}:author') follows = DEFAULT_FOLLOWS - day_old = int(time.time()) - author.get('last_seen', 0) > 24*60*60 + day_old = int(time.time()) - author.get('last_seen', 0) > 24 * 60 * 60 if day_old: follows = query_follows(user_id) else: diff --git a/resolvers/stat.py b/resolvers/stat.py index 38d35d32..7cac380e 100644 --- a/resolvers/stat.py +++ b/resolvers/stat.py @@ -1,4 +1,4 @@ -from sqlalchemy import func, distinct +from sqlalchemy import func, select, distinct, alias from sqlalchemy.orm import aliased from orm.topic import TopicFollower, Topic @@ -14,10 +14,20 @@ def add_topic_stat_columns(q): q = ( q.outerjoin(ShoutTopic, aliased_topic.id == ShoutTopic.topic) .add_columns(func.count(distinct(ShoutTopic.shout)).label('shouts_stat')) - .outerjoin(aliased_shout_authors, ShoutTopic.shout == aliased_shout_authors.shout) - .add_columns(func.count(distinct(aliased_shout_authors.author)).label('authors_stat')) - .outerjoin(aliased_topic_followers, aliased_topic.id == aliased_topic_followers.topic) - .add_columns(func.count(distinct(aliased_topic_followers.follower)).label('followers_stat')) + .outerjoin( + aliased_shout_authors, ShoutTopic.shout == aliased_shout_authors.shout + ) + .add_columns( + func.count(distinct(aliased_shout_authors.author)).label('authors_stat') + ) + .outerjoin( + aliased_topic_followers, aliased_topic.id == aliased_topic_followers.topic + ) + .add_columns( + func.count(distinct(aliased_topic_followers.follower)).label( + 'followers_stat' + ) + ) ) q = q.group_by(aliased_topic.id) @@ -27,15 +37,28 @@ def add_topic_stat_columns(q): def add_author_stat_columns(q): aliased_author_authors = aliased(AuthorFollower, name='af_authors') - aliased_author_followers = aliased(AuthorFollower, name='af_followers') # Добавлен второй псевдоним + aliased_author_followers = aliased( + AuthorFollower, name='af_followers' + ) # Добавлен второй псевдоним aliased_author = aliased(Author) q = ( q.outerjoin(ShoutAuthor, aliased_author.id == ShoutAuthor.author) .add_columns(func.count(distinct(ShoutAuthor.shout)).label('shouts_stat')) - .outerjoin(aliased_author_authors, aliased_author_authors.follower == aliased_author.id) - .add_columns(func.count(distinct(aliased_author_authors.author)).label('authors_stat')) - .outerjoin(aliased_author_followers, aliased_author_followers.author == aliased_author.id) # Используется второй псевдоним - .add_columns(func.count(distinct(aliased_author_followers.follower)).label('followers_stat')) # Используется второй псевдоним + .outerjoin( + aliased_author_authors, aliased_author_authors.follower == aliased_author.id + ) + .add_columns( + func.count(distinct(aliased_author_authors.author)).label('authors_stat') + ) + .outerjoin( + aliased_author_followers, + aliased_author_followers.author == aliased_author.id, + ) # Используется второй псевдоним + .add_columns( + func.count(distinct(aliased_author_followers.follower)).label( + 'followers_stat' + ) + ) # Используется второй псевдоним ) q = q.group_by(aliased_author.id) @@ -50,7 +73,7 @@ def execute_with_ministat(q): entity.stat = { 'shouts': shouts_stat, 'authors': authors_stat, - 'followers': followers_stat + 'followers': followers_stat, } records.append(entity) @@ -65,3 +88,145 @@ def get_authors_with_stat(q): def get_topics_with_stat(q): q = add_topic_stat_columns(q) return execute_with_ministat(q) + + +def query_follows(author_id: int): + subquery_shout_author = ( + select( + [ + ShoutAuthor.author, + func.count(distinct(ShoutAuthor.shout)).label('shouts_stat'), + ] + ) + .group_by(ShoutAuthor.author) + .where(ShoutAuthor.author == author_id) + .alias() + ) + + subquery_author_followers = ( + select( + [ + AuthorFollower.author, + func.count(distinct(AuthorFollower.author)).label('authors_stat'), + ] + ) + .group_by(AuthorFollower.author) + .where(AuthorFollower.author == author_id) + .alias() + ) + + subquery_author_followers = ( + select( + [ + AuthorFollower.follower, + func.count(distinct(AuthorFollower.follower)).label('followers_stat'), + ] + ) + .group_by(AuthorFollower.follower) + .where(AuthorFollower.follower == author_id) + .alias() + ) + + subq_shout_author_alias = alias(subquery_shout_author) + subq_author_followers_alias = alias( + subquery_author_followers, name='subq_author_followers' + ) + subq_author_authors_alias = alias( + subquery_author_followers, name='subq_author_authors' + ) + + authors_query = ( + select( + [ + Author.id, + subq_shout_author_alias.c.shouts_stat, + subq_author_authors_alias.c.authors_stat, + subq_author_followers_alias.c.followers_stat, + ] + ) + .select_from(Author) + .outerjoin( + subq_shout_author_alias, Author.id == subq_shout_author_alias.c.author + ) + .outerjoin( + subq_author_authors_alias, Author.id == subq_author_followers_alias.c.author + ) + .outerjoin( + subq_author_followers_alias, + Author.id == subq_author_followers_alias.c.follower, + ) + ) + + authors = execute_with_ministat(authors_query) + + subquery_shout_topic = ( + select( + [ + ShoutTopic.topic, + func.count(distinct(ShoutTopic.shout)).label('shouts_stat'), + ] + ) + .group_by(ShoutTopic.topic) + .alias() + ) + + subquery_shout_topic_authors = ( + select( + [ + ShoutTopic.topic, + func.count(distinct(ShoutTopic.author)).label('authors_stat'), + ] + ) + .group_by(ShoutTopic.topic) + .alias() + ) + + subquery_topic_followers = ( + select( + [ + TopicFollower.topic, + func.count(distinct(TopicFollower.follower)).label('followers_stat'), + ] + ) + .group_by(TopicFollower.topic_id) + .alias() + ) + + subq_shout_topic_alias = alias(subquery_shout_topic) + subq_shout_topic_authors_alias = alias( + subquery_shout_topic_authors, name='subq_shout_topic_authors' + ) + subq_topic_followers_alias = alias( + subquery_topic_followers, name='subq_topic_followers' + ) + + topics_query = ( + select( + [ + Topic.id, + subq_shout_topic_alias.columns.shouts_stat, + subq_shout_topic_authors_alias.columns.authors_stat, + subq_topic_followers_alias.columns.followers_stat, + ] + ) + .select_from(Topic) + .outerjoin( + subq_shout_topic_alias, Topic.id == subq_shout_topic_alias.columns.topic_id + ) + .outerjoin( + subq_shout_topic_authors_alias, + Topic.id == subq_shout_topic_authors_alias.columns.topic_id, + ) + .outerjoin( + subq_topic_followers_alias, + Topic.id == subq_topic_followers_alias.columns.topic_id, + ) + ) + + topics = execute_with_ministat(topics_query) + + return { + 'topics': topics, + 'authors': authors, + 'communities': [{'id': 1, 'name': 'Дискурс', 'slug': 'discours'}], + } diff --git a/services/auth.py b/services/auth.py index 1da9a72b..7b72b09f 100644 --- a/services/auth.py +++ b/services/auth.py @@ -34,12 +34,7 @@ async def check_auth(req): logger.debug(f'{token}') query_name = 'validate_jwt_token' operation = 'ValidateToken' - variables = { - 'params': { - 'token_type': 'access_token', - 'token': token, - } - } + variables = {'params': {'token_type': 'access_token', 'token': token}} gql = { 'query': f'query {operation}($params: ValidateJWTTokenInput!) {{' diff --git a/services/db.py b/services/db.py index 1592a276..f3c02c93 100644 --- a/services/db.py +++ b/services/db.py @@ -33,9 +33,7 @@ def after_cursor_execute(conn, cursor, statement, parameters, context, executema elapsed = time.time() - conn.query_start_time del conn.query_start_time if elapsed > 0.9: # Adjust threshold as needed - logger.debug( - f"\n{statement}\n{'*' * math.floor(elapsed)} {elapsed:.3f} s" - ) + logger.debug(f"\n{statement}\n{'*' * math.floor(elapsed)} {elapsed:.3f} s") def local_session(src=''): diff --git a/services/follows.py b/services/follows.py index 9860828f..d6d7507e 100644 --- a/services/follows.py +++ b/services/follows.py @@ -12,13 +12,11 @@ from services.rediscache import redis DEFAULT_FOLLOWS = { 'topics': [], 'authors': [], - 'communities': [ - {'slug': 'discours', 'name': 'Дискурс', 'id': 1, 'desc': ''} - ], + 'communities': [{'slug': 'discours', 'name': 'Дискурс', 'id': 1, 'desc': ''}], } -async def update_author(author: Author, ttl = 25 * 60 * 60): +async def update_author(author: Author, ttl=25 * 60 * 60): redis_key = f'user:{author.user}:author' await redis.execute('SETEX', redis_key, ttl, json.dumps(author.dict())) diff --git a/tests/query_follows_test.py b/tests/query_follows_test.py new file mode 100644 index 00000000..2701888f --- /dev/null +++ b/tests/query_follows_test.py @@ -0,0 +1,78 @@ +from unittest.mock import Mock +from resolvers.stat import query_follows + + +def test_query_follows(): + user_id = 'user123' + + # Mocking database session and ORM models + mock_session = Mock() + mock_Author = Mock() + mock_ShoutAuthor = Mock() + mock_AuthorFollower = Mock() + mock_Topic = Mock() + mock_ShoutTopic = Mock() + mock_TopicFollower = Mock() + + # Mocking expected query results + expected_result = { + 'topics': [(1, 5, 10, 15), (2, 8, 12, 20)], # Example topics query result + 'authors': [(101, 3, 6, 9), (102, 4, 7, 11)], # Example authors query result + 'communities': [{'id': 1, 'name': 'Дискурс', 'slug': 'discours'}], + } + + # Set up mocks to return expected results when queried + mock_session.query().select_from().outerjoin().all.side_effect = [ + expected_result['authors'], # Authors query result + expected_result['topics'], # Topics query result + ] + + # Call the function to test + result = query_follows( + user_id, + session=mock_session, + Author=mock_Author, + ShoutAuthor=mock_ShoutAuthor, + AuthorFollower=mock_AuthorFollower, + Topic=mock_Topic, + ShoutTopic=mock_ShoutTopic, + TopicFollower=mock_TopicFollower, + ) + + # Assertions + assert result['topics'] == expected_result['topics'] + assert result['authors'] == expected_result['authors'] + assert result['communities'] == expected_result['communities'] + + # Assert that mock session was called with expected queries + expected_queries = [ + mock_session.query( + mock_Author.id, + mock_ShoutAuthor.author_id, + mock_AuthorFollower.author_id, + mock_AuthorFollower.follower_id, + ) + .select_from(mock_Author) + .outerjoin(mock_ShoutAuthor, mock_Author.id == mock_ShoutAuthor.author_id) + .outerjoin(mock_AuthorFollower, mock_Author.id == mock_AuthorFollower.author_id) + .outerjoin( + mock_AuthorFollower, mock_Author.id == mock_AuthorFollower.follower_id + ) + .all, + mock_session.query( + mock_Topic.id, + mock_ShoutTopic.topic_id, + mock_ShoutTopic.topic_id, + mock_TopicFollower.topic_id, + ) + .select_from(mock_Topic) + .outerjoin(mock_ShoutTopic, mock_Topic.id == mock_ShoutTopic.topic_id) + .outerjoin(mock_ShoutTopic, mock_Topic.id == mock_ShoutTopic.topic_id) + .outerjoin(mock_TopicFollower, mock_Topic.id == mock_TopicFollower.topic_id) + .all, + ] + mock_session.query.assert_has_calls(expected_queries) + + +# Run the test +test_query_follows()