From 37aed9160fc5e826aaeabe99a8b5ce0d9ac014d3 Mon Sep 17 00:00:00 2001 From: Tishanov Artem Date: Thu, 13 Nov 2025 00:00:43 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=84=D0=BB=D0=B0=D0=B3=D0=BE=D0=B2=20is?= =?UTF-8?q?=5Fliked=20=D0=B8=20is=5Fdisliked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- rating_api/models/db.py | 15 +++++++++- rating_api/routes/comment.py | 8 ++++-- rating_api/schemas/models.py | 5 +++- tests/test_routes/test_comment.py | 47 +++++++++++++++++++++++++++---- 5 files changed, 65 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index aaaea38..8296261 100644 --- a/README.md +++ b/README.md @@ -25,4 +25,4 @@ ``` ## ENV-file description -- `DB_DSN=postgresql://postgres@localhost:5432/postgres` – Данные для подключения к БД +- – Данные для подключения к БД`DB_DSN=postgresql://postgres@localhost:5432/postgres` diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 4724411..fb3d493 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -246,7 +246,20 @@ def order_by_like_diff(cls, asc_order: bool = False): return cls.like_dislike_diff.asc() else: return cls.like_dislike_diff.desc() - + + @hybrid_method + def has_reaction(self, user_id: int, react: Reaction) -> bool: + return any(reaction.user_id == user_id and reaction.reaction == react for reaction in self.reactions) + @has_reaction.expression + def has_reaction(cls, user_id: int, react: Reaction): + return (select([true()]). + where(and_(CommentReaction.comment_uuid == cls.uuid, + CommentReaction.user_id == user_id, + CommentReaction.reaction == react + )). + exists() + ) + class LecturerUserComment(BaseDbModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 9011277..44fd994 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -28,6 +28,7 @@ CommentImportAll, CommentPost, CommentUpdate, + CommentGetWithLike, ) from rating_api.settings import Settings, get_settings @@ -155,15 +156,16 @@ async def import_comments( return result -@comment.get("/{uuid}", response_model=CommentGet) -async def get_comment(uuid: UUID) -> CommentGet: +@comment.get("/{uuid}", response_model=CommentGetWithLike) +async def get_comment(uuid: UUID, user = Depends(UnionAuth())) -> CommentGetWithLike: """ Возвращает комментарий по его UUID в базе данных RatingAPI """ comment: Comment = Comment.query(session=db.session).filter(Comment.uuid == uuid).one_or_none() if comment is None: raise ObjectNotFound(Comment, uuid) - return CommentGet.model_validate(comment) + base_data = CommentGet.model_validate(comment) + return CommentGetWithLike(**base_data.model_dump(), is_liked=comment.has_reaction(user.get("id"), Reaction.LIKE), is_disliked=comment.has_reaction(user.get("id"), Reaction.DISLIKE)) @comment.get("", response_model=Union[CommentGetAll, CommentGetAllWithAllInfo, CommentGetAllWithStatus]) diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index ad1b0a0..2abfb91 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -7,7 +7,7 @@ from pydantic import ValidationInfo, field_validator from rating_api.exceptions import WrongMark -from rating_api.models import Lecturer, ReviewStatus +from rating_api.models import Lecturer, ReviewStatus, Reaction from rating_api.schemas.base import Base @@ -26,6 +26,9 @@ class CommentGet(Base): like_count: int dislike_count: int +class CommentGetWithLike(CommentGet): + is_liked: bool + is_disliked: bool class CommentGetWithStatus(CommentGet): review_status: ReviewStatus diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 5f62bba..333b12c 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -196,13 +196,48 @@ def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response assert user_comment is not None -def test_get_comment(client, comment): +@pytest.mark.parametrize( +"reaction_data, expected_reaction, comment_user_id", +[ + (None, None, 0), + ((0, Reaction.LIKE), "is_liked", 0), #my like on my comment + ((0, Reaction.DISLIKE), "is_disliked", 0), + ((999, Reaction.LIKE), None, 0), #someone else's like on my comment + ((999, Reaction.DISLIKE), None, 0), + ((0, Reaction.LIKE), "is_liked", 999), # my like on someone else's comment + ((0, Reaction.DISLIKE), "is_disliked", 999), + ((333, Reaction.LIKE), None, 999), # someone else's like on another person's comment + ((333, Reaction.DISLIKE), None, 999), + (None, None, None) #anonymous + +], +) +def test_get_comment_with_reaction(client, dbsession, comment, reaction_data, expected_reaction, comment_user_id): + comment.user_id = comment_user_id + + if reaction_data: + user_id, reaction_type = reaction_data + reaction = CommentReaction( + user_id = user_id, + comment_uuid = comment.uuid, + reaction = reaction_type + ) + dbsession.add(reaction) + + dbsession.commit() + response_comment = client.get(f'{url}/{comment.uuid}') - print("1") - assert response_comment.status_code == status.HTTP_200_OK - random_uuid = uuid.uuid4() - response = client.get(f'{url}/{random_uuid}') - assert response.status_code == status.HTTP_404_NOT_FOUND + + if response_comment.status_code == status.HTTP_404_NOT_FOUND: + return + + data = response_comment.json() + if expected_reaction: + assert data[expected_reaction] + else: + assert data["is_liked"] == False + assert data["is_disliked"] == False + @pytest.fixture From b8be80052cb915774911ee84d7c17314c04f03ef Mon Sep 17 00:00:00 2001 From: Tishanov Artem Date: Thu, 13 Nov 2025 00:13:42 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D0=B3=D0=BE=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D1=8F=20=D0=B2=20=D1=82=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_routes/test_comment.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 333b12c..e5b9356 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -228,15 +228,15 @@ def test_get_comment_with_reaction(client, dbsession, comment, reaction_data, ex response_comment = client.get(f'{url}/{comment.uuid}') - if response_comment.status_code == status.HTTP_404_NOT_FOUND: - return - - data = response_comment.json() - if expected_reaction: - assert data[expected_reaction] + if response_comment: + data = response_comment.json() + if expected_reaction: + assert data[expected_reaction] + else: + assert data["is_liked"] == False + assert data["is_disliked"] == False else: - assert data["is_liked"] == False - assert data["is_disliked"] == False + assert response_comment.status_code == status.HTTP_404_NOT_FOUND From b58ea66326931c8c8dc8ebb25bb5c532d95cf629 Mon Sep 17 00:00:00 2001 From: Tishanov Artem Date: Thu, 13 Nov 2025 00:43:17 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=D0=9D=D0=B5=D0=B1=D0=BE=D0=BB=D1=8C=D1=88?= =?UTF-8?q?=D0=BE=D0=B5=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8296261..aaaea38 100644 --- a/README.md +++ b/README.md @@ -25,4 +25,4 @@ ``` ## ENV-file description -- – Данные для подключения к БД`DB_DSN=postgresql://postgres@localhost:5432/postgres` +- `DB_DSN=postgresql://postgres@localhost:5432/postgres` – Данные для подключения к БД From 4f161785984957cfeb0206be177eae832d42edae Mon Sep 17 00:00:00 2001 From: Tishanov Artem Date: Thu, 13 Nov 2025 00:52:51 +0300 Subject: [PATCH 4/5] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20black=20=D0=B8=20isort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rating_api/models/db.py | 21 +++++++++++------- rating_api/routes/comment.py | 10 ++++++--- rating_api/schemas/models.py | 4 +++- tests/test_routes/test_comment.py | 37 +++++++++++++------------------ 4 files changed, 38 insertions(+), 34 deletions(-) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index fb3d493..46d8f0b 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -246,20 +246,25 @@ def order_by_like_diff(cls, asc_order: bool = False): return cls.like_dislike_diff.asc() else: return cls.like_dislike_diff.desc() - + @hybrid_method def has_reaction(self, user_id: int, react: Reaction) -> bool: return any(reaction.user_id == user_id and reaction.reaction == react for reaction in self.reactions) + @has_reaction.expression def has_reaction(cls, user_id: int, react: Reaction): - return (select([true()]). - where(and_(CommentReaction.comment_uuid == cls.uuid, - CommentReaction.user_id == user_id, - CommentReaction.reaction == react - )). - exists() + return ( + select([true()]) + .where( + and_( + CommentReaction.comment_uuid == cls.uuid, + CommentReaction.user_id == user_id, + CommentReaction.reaction == react, ) - + ) + .exists() + ) + class LecturerUserComment(BaseDbModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 44fd994..0f733d3 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -24,11 +24,11 @@ CommentGetAllWithAllInfo, CommentGetAllWithStatus, CommentGetWithAllInfo, + CommentGetWithLike, CommentGetWithStatus, CommentImportAll, CommentPost, CommentUpdate, - CommentGetWithLike, ) from rating_api.settings import Settings, get_settings @@ -157,7 +157,7 @@ async def import_comments( @comment.get("/{uuid}", response_model=CommentGetWithLike) -async def get_comment(uuid: UUID, user = Depends(UnionAuth())) -> CommentGetWithLike: +async def get_comment(uuid: UUID, user=Depends(UnionAuth())) -> CommentGetWithLike: """ Возвращает комментарий по его UUID в базе данных RatingAPI """ @@ -165,7 +165,11 @@ async def get_comment(uuid: UUID, user = Depends(UnionAuth())) -> CommentGetWith if comment is None: raise ObjectNotFound(Comment, uuid) base_data = CommentGet.model_validate(comment) - return CommentGetWithLike(**base_data.model_dump(), is_liked=comment.has_reaction(user.get("id"), Reaction.LIKE), is_disliked=comment.has_reaction(user.get("id"), Reaction.DISLIKE)) + return CommentGetWithLike( + **base_data.model_dump(), + is_liked=comment.has_reaction(user.get("id"), Reaction.LIKE), + is_disliked=comment.has_reaction(user.get("id"), Reaction.DISLIKE), + ) @comment.get("", response_model=Union[CommentGetAll, CommentGetAllWithAllInfo, CommentGetAllWithStatus]) diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index 2abfb91..91b6a60 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -7,7 +7,7 @@ from pydantic import ValidationInfo, field_validator from rating_api.exceptions import WrongMark -from rating_api.models import Lecturer, ReviewStatus, Reaction +from rating_api.models import Lecturer, ReviewStatus from rating_api.schemas.base import Base @@ -26,10 +26,12 @@ class CommentGet(Base): like_count: int dislike_count: int + class CommentGetWithLike(CommentGet): is_liked: bool is_disliked: bool + class CommentGetWithStatus(CommentGet): review_status: ReviewStatus diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index e5b9356..065fa5d 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -1,6 +1,5 @@ import datetime import logging -import uuid import pytest from starlette import status @@ -197,31 +196,26 @@ def test_create_comment(client, dbsession, lecturers, body, lecturer_n, response @pytest.mark.parametrize( -"reaction_data, expected_reaction, comment_user_id", -[ - (None, None, 0), - ((0, Reaction.LIKE), "is_liked", 0), #my like on my comment - ((0, Reaction.DISLIKE), "is_disliked", 0), - ((999, Reaction.LIKE), None, 0), #someone else's like on my comment - ((999, Reaction.DISLIKE), None, 0), - ((0, Reaction.LIKE), "is_liked", 999), # my like on someone else's comment - ((0, Reaction.DISLIKE), "is_disliked", 999), - ((333, Reaction.LIKE), None, 999), # someone else's like on another person's comment - ((333, Reaction.DISLIKE), None, 999), - (None, None, None) #anonymous - -], + "reaction_data, expected_reaction, comment_user_id", + [ + (None, None, 0), + ((0, Reaction.LIKE), "is_liked", 0), # my like on my comment + ((0, Reaction.DISLIKE), "is_disliked", 0), + ((999, Reaction.LIKE), None, 0), # someone else's like on my comment + ((999, Reaction.DISLIKE), None, 0), + ((0, Reaction.LIKE), "is_liked", 999), # my like on someone else's comment + ((0, Reaction.DISLIKE), "is_disliked", 999), + ((333, Reaction.LIKE), None, 999), # someone else's like on another person's comment + ((333, Reaction.DISLIKE), None, 999), + (None, None, None), # anonymous + ], ) def test_get_comment_with_reaction(client, dbsession, comment, reaction_data, expected_reaction, comment_user_id): comment.user_id = comment_user_id if reaction_data: - user_id, reaction_type = reaction_data - reaction = CommentReaction( - user_id = user_id, - comment_uuid = comment.uuid, - reaction = reaction_type - ) + user_id, reaction_type = reaction_data + reaction = CommentReaction(user_id=user_id, comment_uuid=comment.uuid, reaction=reaction_type) dbsession.add(reaction) dbsession.commit() @@ -239,7 +233,6 @@ def test_get_comment_with_reaction(client, dbsession, comment, reaction_data, ex assert response_comment.status_code == status.HTTP_404_NOT_FOUND - @pytest.fixture def comments_with_likes(client, dbsession, lecturers): """ From c7a2444be7082e99831b3f0fd6904100760205ba Mon Sep 17 00:00:00 2001 From: Tishanov Artem Date: Thu, 11 Dec 2025 18:43:15 +0300 Subject: [PATCH 5/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=80=D0=B5=D0=B0=D0=BA=D1=86=D0=B8=D0=B9?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20get=5Fcomments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rating_api/models/db.py | 12 ++++++++++++ rating_api/routes/comment.py | 36 ++++++++++++++++++++++++++++++---- rating_api/schemas/models.py | 38 ++++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 4 deletions(-) diff --git a/rating_api/models/db.py b/rating_api/models/db.py index 46d8f0b..28f5a4f 100644 --- a/rating_api/models/db.py +++ b/rating_api/models/db.py @@ -265,6 +265,18 @@ def has_reaction(cls, user_id: int, react: Reaction): .exists() ) + @classmethod + def reactions_for_comments(cls, user_id: int, session, comments): + if not user_id or not comments: + return {} + comments_uuid = [c.uuid for c in comments] + reactions = ( + session.query(CommentReaction) + .filter(CommentReaction.user_id == user_id, CommentReaction.comment_uuid.in_(comments_uuid)) + .all() + ) + return {r.comment_uuid: r.reaction for r in reactions} + class LecturerUserComment(BaseDbModel): id: Mapped[int] = mapped_column(Integer, primary_key=True) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 0f733d3..5012afc 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -22,6 +22,7 @@ CommentGet, CommentGetAll, CommentGetAllWithAllInfo, + CommentGetAllWithLike, CommentGetAllWithStatus, CommentGetWithAllInfo, CommentGetWithLike, @@ -172,7 +173,9 @@ async def get_comment(uuid: UUID, user=Depends(UnionAuth())) -> CommentGetWithLi ) -@comment.get("", response_model=Union[CommentGetAll, CommentGetAllWithAllInfo, CommentGetAllWithStatus]) +@comment.get( + "", response_model=Union[CommentGetAll, CommentGetAllWithLike, CommentGetAllWithAllInfo, CommentGetAllWithStatus] +) async def get_comments( limit: int = 10, offset: int = 0, @@ -186,7 +189,7 @@ async def get_comments( unreviewed: bool = False, asc_order: bool = False, user=Depends(UnionAuth(scopes=["rating.comment.review"], auto_error=False, allow_none=False)), -) -> CommentGetAll: +) -> Union[CommentGetAll, CommentGetAllWithLike, CommentGetAllWithAllInfo, CommentGetAllWithStatus]: """ Scopes: `["rating.comment.review"]` @@ -209,6 +212,7 @@ async def get_comments( `asc_order` -Если передано true, сортировать в порядке возрастания. Иначе - в порядке убывания """ + comments_query = ( Comment.query(session=db.session) .filter(Comment.search_by_lectorer_id(lecturer_id)) @@ -225,6 +229,7 @@ async def get_comments( ) ) comments = comments_query.limit(limit).offset(offset).all() + like = False if not comments: raise ObjectNotFound(Comment, 'all') if user and "rating.comment.review" in [scope['name'] for scope in user.get('session_scopes')]: @@ -234,8 +239,13 @@ async def get_comments( result = CommentGetAllWithStatus(limit=limit, offset=offset, total=len(comments)) comment_validator = CommentGetWithStatus else: - result = CommentGetAll(limit=limit, offset=offset, total=len(comments)) + result = ( + CommentGetAllWithLike(limit=limit, offset=offset, total=len(comments)) + if user + else CommentGetAll(limit=limit, offset=offset, total=len(comments)) + ) comment_validator = CommentGet + like = True result.comments = comments @@ -250,8 +260,26 @@ async def get_comments( result.comments = [comment for comment in result.comments if comment.review_status is ReviewStatus.APPROVED] result.total = len(result.comments) - result.comments = [comment_validator.model_validate(comment) for comment in result.comments] + comments_with_like = [] + current_user_id = user.get("id") if user else None + if current_user_id and result.comments: + user_reactions = Comment.reactions_for_comments(current_user_id, db.session, result.comments) + else: + user_reactions = {} + + for comment in result.comments: + base_data = comment_validator.model_validate(comment) + + if current_user_id: + reaction = user_reactions.get(comment.uuid) + comment_with_reactions = CommentGetWithLike( + **base_data.model_dump(), is_liked=reaction == Reaction.LIKE, is_disliked=reaction == Reaction.DISLIKE + ) + comments_with_like.append(comment_with_reactions) + else: + comments_with_like.append(base_data) + result.comments = comments_with_like return result diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index 91b6a60..38d61e0 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -36,11 +36,24 @@ class CommentGetWithStatus(CommentGet): review_status: ReviewStatus +""" +class CommentGetWithLikeAndStatus(CommentGetWithLike): + review_status: ReviewStatus +""" + + class CommentGetWithAllInfo(CommentGet): review_status: ReviewStatus approved_by: int | None = None +""" +class CommentGetWithAllInfoAndLike(CommentGetWithLike): + review_status: ReviewStatus + approved_by: int | None = None +""" + + class CommentUpdate(Base): subject: str = None text: str = None @@ -79,6 +92,13 @@ class CommentGetAll(Base): total: int +class CommentGetAllWithLike(Base): + comments: list[CommentGetWithLike] = [] + limit: int + offset: int + total: int + + class CommentGetAllWithStatus(Base): comments: list[CommentGetWithStatus] = [] limit: int @@ -86,6 +106,15 @@ class CommentGetAllWithStatus(Base): total: int +""" +class CommentGetAllWithStatusAndLike(Base): + comments: list[CommentGetWithLikeAndStatus] = [] + limit: int + offset: int + total: int +""" + + class CommentGetAllWithAllInfo(Base): comments: list[CommentGetWithAllInfo] = [] limit: int @@ -93,6 +122,15 @@ class CommentGetAllWithAllInfo(Base): total: int +""" +class CommentGetAllWithAllInfoAndLike(Base): + comments: list[CommentGetWithAllInfoAndLike] = [] + limit: int + offset: int + total: int +""" + + class LecturerUserCommentPost(Base): lecturer_id: int user_id: int