Skip to content

Commit f3c7a5f

Browse files
authored
Merge pull request #145 from hgfejcwoefGleb/gipilipenko
add sorting by likes
2 parents 8348534 + 2d664f1 commit f3c7a5f

File tree

3 files changed

+179
-12
lines changed

3 files changed

+179
-12
lines changed

rating_api/models/db.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
String,
1919
UnaryExpression,
2020
and_,
21+
case,
2122
desc,
2223
func,
2324
nulls_last,
2425
or_,
26+
select,
2527
true,
2628
)
2729
from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property
@@ -186,14 +188,64 @@ def search_by_subject(self, query: str) -> bool:
186188
return and_(Comment.review_status == ReviewStatus.APPROVED, func.lower(Comment.subject).contains(query))
187189

188190
@hybrid_property
189-
def like_count(self) -> int:
190-
"""Python access to like count"""
191-
return sum(1 for like in self.reactions if like.reaction == 'like')
191+
def like_count(self):
192+
"""Python доступ к числу лайков"""
193+
return sum(1 for reaction in self.reactions if reaction.reaction == Reaction.LIKE)
194+
195+
@like_count.expression
196+
def like_count(cls):
197+
"""SQL выражение для подсчета лайков"""
198+
return (
199+
select(func.count(CommentReaction.uuid))
200+
.where(and_(CommentReaction.comment_uuid == cls.uuid, CommentReaction.reaction == Reaction.LIKE))
201+
.label('like_count')
202+
)
192203

193204
@hybrid_property
194-
def dislike_count(self) -> int:
195-
"""Python access to dislike count"""
196-
return sum(1 for like in self.reactions if like.reaction == 'dislike')
205+
def dislike_count(self):
206+
"""Python доступ к числу дизлайков"""
207+
return sum(1 for reaction in self.reactions if reaction.reaction == Reaction.DISLIKE)
208+
209+
@dislike_count.expression
210+
def dislike_count(cls):
211+
"""SQL выражение для подсчета дизлайков"""
212+
return (
213+
select(func.count(CommentReaction.uuid))
214+
.where(and_(CommentReaction.comment_uuid == cls.uuid, CommentReaction.reaction == Reaction.DISLIKE))
215+
.label('dislike_count')
216+
)
217+
218+
@hybrid_property
219+
def like_dislike_diff(self):
220+
"""Python доступ к разнице лайков и дизлайков"""
221+
if hasattr(self, '_like_dislike_diff'):
222+
return self._like_dislike_diff
223+
return self.like_count - self.dislike_count
224+
225+
@like_dislike_diff.expression
226+
def like_dislike_diff(cls):
227+
"""SQL выражение для вычисления разницы лайков/дизлайков"""
228+
return (
229+
select(
230+
func.sum(
231+
case(
232+
(CommentReaction.reaction == Reaction.LIKE, 1),
233+
(CommentReaction.reaction == Reaction.DISLIKE, -1),
234+
else_=0,
235+
)
236+
)
237+
)
238+
.where(CommentReaction.comment_uuid == cls.uuid)
239+
.label('like_dislike_diff')
240+
)
241+
242+
@hybrid_method
243+
def order_by_like_diff(cls, asc_order: bool = False):
244+
"""Метод для сортировки по разнице лайков/дизлайков"""
245+
if asc_order:
246+
return cls.like_dislike_diff.asc()
247+
else:
248+
return cls.like_dislike_diff.desc()
197249

198250

199251
class LecturerUserComment(BaseDbModel):

rating_api/routes/comment.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ async def get_comments(
174174
user_id: int | None = None,
175175
subject: str | None = None,
176176
order_by: str = Query(
177-
enum=["create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general"],
177+
enum=["create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "like_diff"],
178178
default="create_ts",
179179
),
180180
unreviewed: bool = False,
@@ -190,9 +190,10 @@ async def get_comments(
190190
Если без смещения возвращается комментарий с условным номером N,
191191
то при значении offset = X будет возвращаться комментарий с номером N + X
192192
193-
`order_by` - возможные значения `"create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general"`.
194-
Если передано `'create_ts'` - возвращается список комментариев отсортированных по времени
195-
Если передано `'mark_...'` - возвращается список комментариев отсортированных по конкретной оценке
193+
`order_by` - возможные значения `"create_ts", "mark_kindness", "mark_freebie", "mark_clarity", "mark_general", "like_diff"`.
194+
Если передано `'create_ts'` - возвращается список комментариев, отсортированных по времени
195+
Если передано `'mark_...'` - возвращается список комментариев, отсортированных по конкретной оценке
196+
Если передано `'like_diff'` - возвращается список комментариев, отсортированных по разнице лайков и дизлайков
196197
197198
`lecturer_id` - вернет все комментарии для преподавателя с конкретным id, по дефолту возвращает вообще все аппрувнутые комментарии.
198199
@@ -210,7 +211,11 @@ async def get_comments(
210211
.order_by(
211212
Comment.order_by_mark(order_by, asc_order)
212213
if "mark" in order_by
213-
else Comment.order_by_create_ts(order_by, asc_order)
214+
else (
215+
Comment.order_by_like_diff(asc_order)
216+
if order_by == "like_diff"
217+
else Comment.order_by_create_ts(order_by, asc_order)
218+
)
214219
)
215220
)
216221
comments = comments_query.limit(limit).offset(offset).all()

tests/test_routes/test_comment.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66
from starlette import status
77

8-
from rating_api.models import Comment, LecturerUserComment, ReviewStatus
8+
from rating_api.models import Comment, CommentReaction, LecturerUserComment, Reaction, ReviewStatus
99
from rating_api.settings import get_settings
1010

1111

@@ -205,6 +205,116 @@ def test_get_comment(client, comment):
205205
assert response.status_code == status.HTTP_404_NOT_FOUND
206206

207207

208+
@pytest.fixture
209+
def comments_with_likes(client, dbsession, lecturers):
210+
"""
211+
Создает несколько комментариев с разным количеством лайков/дизлайков
212+
"""
213+
comments = []
214+
215+
user_id = 9999
216+
217+
comment_data = [
218+
{
219+
"user_id": user_id,
220+
"lecturer_id": lecturers[0].id,
221+
"subject": "test_subject",
222+
"text": "Comment with many likes",
223+
"mark_kindness": 1,
224+
"mark_freebie": 0,
225+
"mark_clarity": 0,
226+
"review_status": ReviewStatus.APPROVED,
227+
},
228+
{
229+
"user_id": user_id,
230+
"lecturer_id": lecturers[0].id,
231+
"subject": "test_subject",
232+
"text": "Comment with many dislikes",
233+
"mark_kindness": 1,
234+
"mark_freebie": 0,
235+
"mark_clarity": 0,
236+
"review_status": ReviewStatus.APPROVED,
237+
},
238+
{
239+
"user_id": user_id,
240+
"lecturer_id": lecturers[0].id,
241+
"subject": "test_subject",
242+
"text": "Comment with balanced reactions",
243+
"mark_kindness": 1,
244+
"mark_freebie": 0,
245+
"mark_clarity": 0,
246+
"review_status": ReviewStatus.APPROVED,
247+
},
248+
]
249+
250+
for data in comment_data:
251+
comment = Comment(**data)
252+
dbsession.add(comment)
253+
comments.append(comment)
254+
255+
dbsession.commit()
256+
257+
for _ in range(10):
258+
reaction = CommentReaction(comment_uuid=comments[0].uuid, user_id=user_id, reaction=Reaction.LIKE)
259+
dbsession.add(reaction)
260+
for _ in range(2):
261+
reaction = CommentReaction(comment_uuid=comments[0].uuid, user_id=user_id, reaction=Reaction.DISLIKE)
262+
dbsession.add(reaction)
263+
264+
for _ in range(3):
265+
reaction = CommentReaction(comment_uuid=comments[1].uuid, user_id=user_id, reaction=Reaction.LIKE)
266+
dbsession.add(reaction)
267+
for _ in range(8):
268+
reaction = CommentReaction(comment_uuid=comments[1].uuid, user_id=user_id, reaction=Reaction.DISLIKE)
269+
dbsession.add(reaction)
270+
271+
for _ in range(5):
272+
reaction = CommentReaction(comment_uuid=comments[2].uuid, user_id=user_id, reaction=Reaction.LIKE)
273+
dbsession.add(reaction)
274+
for _ in range(5):
275+
reaction = CommentReaction(comment_uuid=comments[2].uuid, user_id=user_id, reaction=Reaction.DISLIKE)
276+
dbsession.add(reaction)
277+
278+
dbsession.commit()
279+
280+
for comment in comments:
281+
dbsession.refresh(comment)
282+
283+
return comments
284+
285+
286+
@pytest.mark.parametrize(
287+
'order_by, asc_order',
288+
[
289+
('like_diff', False),
290+
('like_diff', True),
291+
],
292+
)
293+
def test_comments_sort_by_like_diff(client, comments_with_likes, order_by, asc_order):
294+
"""
295+
Тестирует сортировку комментариев по разнице лайков (like_diff)
296+
"""
297+
params = {"order_by": order_by, "asc_order": asc_order, "limit": 10}
298+
299+
response = client.get('/comment', params=params)
300+
assert response.status_code == status.HTTP_200_OK
301+
302+
json_response = response.json()
303+
returned_comments = json_response["comments"]
304+
305+
if order_by == 'like_diff':
306+
if asc_order:
307+
for i in range(len(returned_comments) - 1):
308+
current_like_diff = returned_comments[i]["like_count"] - returned_comments[i]["dislike_count"]
309+
next_like_diff = returned_comments[i + 1]["like_count"] - returned_comments[i + 1]["dislike_count"]
310+
assert current_like_diff <= next_like_diff
311+
else:
312+
for i in range(len(returned_comments) - 1):
313+
current_like_diff = returned_comments[i]["like_count"] - returned_comments[i]["dislike_count"]
314+
next_like_diff = returned_comments[i + 1]["like_count"] - returned_comments[i + 1]["dislike_count"]
315+
assert current_like_diff >= next_like_diff
316+
317+
208318
@pytest.mark.parametrize(
209319
'lecturer_n,response_status',
210320
[(0, status.HTTP_200_OK), (1, status.HTTP_200_OK), (2, status.HTTP_200_OK), (3, status.HTTP_404_NOT_FOUND)],

0 commit comments

Comments
 (0)