From 1063ec7e2d16e939805befc019dc7bd359d47aff Mon Sep 17 00:00:00 2001 From: gipilipenko Date: Sat, 13 Sep 2025 13:08:08 +0300 Subject: [PATCH 1/2] sorting by likes --- rating_api/models/db.py | 64 ++++++++++++-- rating_api/routes/comment.py | 15 ++-- tests/test_routes/test_comment.py | 141 +++++++++++++++++++++++++++++- 3 files changed, 208 insertions(+), 12 deletions(-) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 6ad4a52..d942a00 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -18,10 +18,12 @@ String, UnaryExpression, and_, + case, desc, func, nulls_last, or_, + select, true, ) from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property @@ -170,14 +172,64 @@ def search_by_subject(self, query: str) -> bool: return and_(Comment.review_status == ReviewStatus.APPROVED, func.lower(Comment.subject).contains(query)) @hybrid_property - def like_count(self) -> int: - """Python access to like count""" - return sum(1 for like in self.reactions if like.reaction == 'like') + def like_count(self): + """Python доступ к числу лайков""" + return sum(1 for reaction in self.reactions if reaction.reaction == Reaction.LIKE) + + @like_count.expression + def like_count(cls): + """SQL выражение для подсчета лайков""" + return ( + select(func.count(CommentReaction.uuid)) + .where(and_(CommentReaction.comment_uuid == cls.uuid, CommentReaction.reaction == Reaction.LIKE)) + .label('like_count') + ) + + @hybrid_property + def dislike_count(self): + """Python доступ к числу дизлайков""" + return sum(1 for reaction in self.reactions if reaction.reaction == Reaction.DISLIKE) + + @dislike_count.expression + def dislike_count(cls): + """SQL выражение для подсчета дизлайков""" + return ( + select(func.count(CommentReaction.uuid)) + .where(and_(CommentReaction.comment_uuid == cls.uuid, CommentReaction.reaction == Reaction.DISLIKE)) + .label('dislike_count') + ) @hybrid_property - def dislike_count(self) -> int: - """Python access to dislike count""" - return sum(1 for like in self.reactions if like.reaction == 'dislike') + def like_dislike_diff(self): + """Python доступ к разнице лайков и дизлайков""" + if hasattr(self, '_like_dislike_diff'): + return self._like_dislike_diff + return self.like_count - self.dislike_count + + @like_dislike_diff.expression + def like_dislike_diff(cls): + """SQL выражение для вычисления разницы лайков/дизлайков""" + return ( + select( + func.sum( + case( + (CommentReaction.reaction == Reaction.LIKE, 1), + (CommentReaction.reaction == Reaction.DISLIKE, -1), + else_=0, + ) + ) + ) + .where(CommentReaction.comment_uuid == cls.uuid) + .label('like_dislike_diff') + ) + + @hybrid_method + def order_by_like_diff(cls, asc_order: bool = False): + """Метод для сортировки по разнице лайков/дизлайков""" + if asc_order: + return cls.like_dislike_diff.asc() + else: + return cls.like_dislike_diff.desc() class LecturerUserComment(BaseDbModel): diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index d24a130..2353479 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -174,7 +174,7 @@ async def get_comments( user_id: int | None = None, subject: str | None = None, order_by: str = Query( - enum=["create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general"], + enum=["create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "like_diff"], default="create_ts", ), unreviewed: bool = False, @@ -190,9 +190,10 @@ async def get_comments( Если без смещения возвращается комментарий с условным номером N, то при значении offset = X будет возвращаться комментарий с номером N + X - `order_by` - возможные значения `"create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general"`. - Если передано `'create_ts'` - возвращается список комментариев отсортированных по времени - Если передано `'mark_...'` - возвращается список комментариев отсортированных по конкретной оценке + `order_by` - возможные значения `"create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "like_diff"`. + Если передано `'create_ts'` - возвращается список комментариев, отсортированных по времени + Если передано `'mark_...'` - возвращается список комментариев, отсортированных по конкретной оценке + Если передано `'like_diff'` - возвращается список комментариев, отсортированных по разнице лайков и дизлайков `lecturer_id` - вернет все комментарии для преподавателя с конкретным id, по дефолту возвращает вообще все аппрувнутые комментарии. @@ -210,7 +211,11 @@ async def get_comments( .order_by( Comment.order_by_mark(order_by, asc_order) if "mark" in order_by - else Comment.order_by_create_ts(order_by, asc_order) + else ( + Comment.order_by_like_diff(asc_order) + if order_by == "like_diff" + else Comment.order_by_create_ts(order_by, asc_order) + ) ) ) comments = comments_query.limit(limit).offset(offset).all() diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 6952e66..1a30bdb 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -5,7 +5,7 @@ import pytest from starlette import status -from rating_api.models import Comment, LecturerUserComment, ReviewStatus +from rating_api.models import Comment, LecturerUserComment, ReviewStatus, Reaction, CommentReaction from rating_api.settings import get_settings @@ -204,6 +204,145 @@ def test_get_comment(client, comment): response = client.get(f'{url}/{random_uuid}') assert response.status_code == status.HTTP_404_NOT_FOUND +@pytest.fixture +def comments_with_likes(client, dbsession, lecturers): + """ + Создает несколько комментариев с разным количеством лайков/дизлайков + """ + comments = [] + + user_id = 9999 + + comment_data = [ + { + "user_id": user_id, + "lecturer_id": lecturers[0].id, + "subject": "test_subject", + "text": "Comment with many likes", + "mark_kindness": 1, + "mark_freebie": 0, + "mark_clarity": 0, + "review_status": ReviewStatus.APPROVED + }, + { + "user_id": user_id, + "lecturer_id": lecturers[0].id, + "subject": "test_subject", + "text": "Comment with many dislikes", + "mark_kindness": 1, + "mark_freebie": 0, + "mark_clarity": 0, + "review_status": ReviewStatus.APPROVED + }, + { + "user_id": user_id, + "lecturer_id": lecturers[0].id, + "subject": "test_subject", + "text": "Comment with balanced reactions", + "mark_kindness": 1, + "mark_freebie": 0, + "mark_clarity": 0, + "review_status": ReviewStatus.APPROVED + } + ] + + for data in comment_data: + comment = Comment(**data) + dbsession.add(comment) + comments.append(comment) + + dbsession.commit() + + + for _ in range(10): + reaction = CommentReaction( + comment_uuid=comments[0].uuid, + user_id=user_id, + reaction=Reaction.LIKE + ) + dbsession.add(reaction) + for _ in range(2): + reaction = CommentReaction( + comment_uuid=comments[0].uuid, + user_id=user_id, + reaction=Reaction.DISLIKE + ) + dbsession.add(reaction) + + for _ in range(3): + reaction = CommentReaction( + comment_uuid=comments[1].uuid, + user_id=user_id, + reaction=Reaction.LIKE + ) + dbsession.add(reaction) + for _ in range(8): + reaction = CommentReaction( + comment_uuid=comments[1].uuid, + user_id=user_id, + reaction=Reaction.DISLIKE + ) + dbsession.add(reaction) + + for _ in range(5): + reaction = CommentReaction( + comment_uuid=comments[2].uuid, + user_id=user_id, + reaction=Reaction.LIKE + ) + dbsession.add(reaction) + for _ in range(5): + reaction = CommentReaction( + comment_uuid=comments[2].uuid, + user_id=user_id, + reaction=Reaction.DISLIKE + ) + dbsession.add(reaction) + + dbsession.commit() + + for comment in comments: + dbsession.refresh(comment) + + return comments + + +@pytest.mark.parametrize( + 'order_by, asc_order', + [ + ('like_diff', False), + ('like_diff', True), + ] +) +def test_comments_sort_by_like_diff(client, comments_with_likes, order_by, asc_order): + """ + Тестирует сортировку комментариев по разнице лайков (like_diff) + """ + params = { + "order_by": order_by, + "asc_order": asc_order, + "limit": 10 + } + + response = client.get('/comment', params=params) + assert response.status_code == status.HTTP_200_OK + + json_response = response.json() + returned_comments = json_response["comments"] + + + if order_by == 'like_diff': + if asc_order: + for i in range(len(returned_comments) - 1): + current_like_diff = returned_comments[i]["like_count"] - returned_comments[i]["dislike_count"] + next_like_diff = returned_comments[i + 1]["like_count"] - returned_comments[i + 1]["dislike_count"] + assert current_like_diff <= next_like_diff + else: + for i in range(len(returned_comments) - 1): + current_like_diff = returned_comments[i]["like_count"] - returned_comments[i]["dislike_count"] + next_like_diff = returned_comments[i + 1]["like_count"] - returned_comments[i + 1]["dislike_count"] + assert current_like_diff >= next_like_diff + @pytest.mark.parametrize( 'lecturer_n,response_status', From b57691209964cc76850d1478be097f1306c54ab9 Mon Sep 17 00:00:00 2001 From: MikhailPI1 Date: Sun, 26 Oct 2025 15:55:22 +0400 Subject: [PATCH 2/2] fix linting --- tests/test_routes/test_comment.py | 83 ++++++++++--------------------- 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 1a30bdb..5f62bba 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -5,7 +5,7 @@ import pytest from starlette import status -from rating_api.models import Comment, LecturerUserComment, ReviewStatus, Reaction, CommentReaction +from rating_api.models import Comment, CommentReaction, LecturerUserComment, Reaction, ReviewStatus from rating_api.settings import get_settings @@ -204,15 +204,16 @@ def test_get_comment(client, comment): response = client.get(f'{url}/{random_uuid}') assert response.status_code == status.HTTP_404_NOT_FOUND + @pytest.fixture def comments_with_likes(client, dbsession, lecturers): """ Создает несколько комментариев с разным количеством лайков/дизлайков """ comments = [] - + user_id = 9999 - + comment_data = [ { "user_id": user_id, @@ -222,7 +223,7 @@ def comments_with_likes(client, dbsession, lecturers): "mark_kindness": 1, "mark_freebie": 0, "mark_clarity": 0, - "review_status": ReviewStatus.APPROVED + "review_status": ReviewStatus.APPROVED, }, { "user_id": user_id, @@ -232,7 +233,7 @@ def comments_with_likes(client, dbsession, lecturers): "mark_kindness": 1, "mark_freebie": 0, "mark_clarity": 0, - "review_status": ReviewStatus.APPROVED + "review_status": ReviewStatus.APPROVED, }, { "user_id": user_id, @@ -242,94 +243,64 @@ def comments_with_likes(client, dbsession, lecturers): "mark_kindness": 1, "mark_freebie": 0, "mark_clarity": 0, - "review_status": ReviewStatus.APPROVED - } + "review_status": ReviewStatus.APPROVED, + }, ] - + for data in comment_data: comment = Comment(**data) dbsession.add(comment) comments.append(comment) - + dbsession.commit() - for _ in range(10): - reaction = CommentReaction( - comment_uuid=comments[0].uuid, - user_id=user_id, - reaction=Reaction.LIKE - ) + reaction = CommentReaction(comment_uuid=comments[0].uuid, user_id=user_id, reaction=Reaction.LIKE) dbsession.add(reaction) for _ in range(2): - reaction = CommentReaction( - comment_uuid=comments[0].uuid, - user_id=user_id, - reaction=Reaction.DISLIKE - ) + reaction = CommentReaction(comment_uuid=comments[0].uuid, user_id=user_id, reaction=Reaction.DISLIKE) dbsession.add(reaction) - + for _ in range(3): - reaction = CommentReaction( - comment_uuid=comments[1].uuid, - user_id=user_id, - reaction=Reaction.LIKE - ) + reaction = CommentReaction(comment_uuid=comments[1].uuid, user_id=user_id, reaction=Reaction.LIKE) dbsession.add(reaction) for _ in range(8): - reaction = CommentReaction( - comment_uuid=comments[1].uuid, - user_id=user_id, - reaction=Reaction.DISLIKE - ) + reaction = CommentReaction(comment_uuid=comments[1].uuid, user_id=user_id, reaction=Reaction.DISLIKE) dbsession.add(reaction) - + for _ in range(5): - reaction = CommentReaction( - comment_uuid=comments[2].uuid, - user_id=user_id, - reaction=Reaction.LIKE - ) + reaction = CommentReaction(comment_uuid=comments[2].uuid, user_id=user_id, reaction=Reaction.LIKE) dbsession.add(reaction) for _ in range(5): - reaction = CommentReaction( - comment_uuid=comments[2].uuid, - user_id=user_id, - reaction=Reaction.DISLIKE - ) + reaction = CommentReaction(comment_uuid=comments[2].uuid, user_id=user_id, reaction=Reaction.DISLIKE) dbsession.add(reaction) - + dbsession.commit() - + for comment in comments: dbsession.refresh(comment) - + return comments @pytest.mark.parametrize( 'order_by, asc_order', [ - ('like_diff', False), - ('like_diff', True), - ] + ('like_diff', False), + ('like_diff', True), + ], ) def test_comments_sort_by_like_diff(client, comments_with_likes, order_by, asc_order): """ Тестирует сортировку комментариев по разнице лайков (like_diff) """ - params = { - "order_by": order_by, - "asc_order": asc_order, - "limit": 10 - } - + params = {"order_by": order_by, "asc_order": asc_order, "limit": 10} + response = client.get('/comment', params=params) assert response.status_code == status.HTTP_200_OK - + json_response = response.json() returned_comments = json_response["comments"] - if order_by == 'like_diff': if asc_order: