From 046789083451f83bf7c60e3c7fe7b4871b3007d2 Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Fri, 22 Nov 2024 11:43:05 +0300 Subject: [PATCH 01/21] Revert tests --- tests/test_routes/test_comment.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index f6a806a..ea68fd6 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -18,13 +18,12 @@ 'body,lecturer_n,response_status', [ ( - { + { "subject": "test_subject", "text": "test_text", "mark_kindness": 1, "mark_freebie": 0, "mark_clarity": 0, - "is_anonymous": False, }, 0, status.HTTP_200_OK, @@ -41,13 +40,12 @@ status.HTTP_200_OK, ), ( - { + { "subject": "test1_subject", "text": "test_text", "mark_kindness": -2, "mark_freebie": -2, "mark_clarity": -2, - "is_anonymous": False, }, 1, status.HTTP_200_OK, @@ -59,7 +57,6 @@ "mark_kindness": 5, "mark_freebie": -2, "mark_clarity": 0, - "is_anonymous": False, }, 2, status.HTTP_400_BAD_REQUEST, @@ -71,7 +68,6 @@ "mark_kindness": 1, "mark_freebie": -2, "mark_clarity": 0, - "is_anonymous": False, }, 3, status.HTTP_404_NOT_FOUND, @@ -88,31 +84,30 @@ 0, status.HTTP_200_OK, ), - ( # NotAnonymous comment - { + ( + { # Not provided anonymity "subject": "test_subject", "text": "test_text", "mark_kindness": 1, "mark_freebie": -2, "mark_clarity": 0, - "is_anonymous": False, }, - 0, + 1, status.HTTP_200_OK, ), - ( # Bad anonymity + ( # NotAnonymous comment { "subject": "test_subject", "text": "test_text", "mark_kindness": 1, "mark_freebie": -2, "mark_clarity": 0, - "is_anonymous": 'asd', + "is_anonymous": False, }, 0, - status.HTTP_422_UNPROCESSABLE_ENTITY, + status.HTTP_200_OK, ), - ( # Not provided anonymity + ( # Bad anonymity { "subject": "test_subject", "text": "test_text", From 8ef3dfae8fc90c2c1fe83f89b3ea41a7ab82c0ec Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Sat, 23 Nov 2024 12:30:55 +0300 Subject: [PATCH 02/21] Add PATCH to own comment --- rating_api/routes/comment.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index d3b5c78..462cb8a 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -118,7 +118,7 @@ async def get_comments( return result -@comment.patch("/{uuid}", response_model=CommentGet) +@comment.patch("/{uuid}/review", response_model=CommentGet) async def review_comment( uuid: UUID, review_status: Literal[ReviewStatus.APPROVED, ReviewStatus.DISMISSED] = ReviewStatus.DISMISSED, @@ -140,6 +140,21 @@ async def review_comment( return CommentGet.model_validate(Comment.update(session=db.session, id=uuid, review_status=review_status)) +@comment.patch("/{uuid}", response_model=CommentGet) +async def update_comment(uuid: UUID, + comment_update: CommentPost = None, + user=Depends(UnionAuth())) -> CommentGet: + """ + Изменить комментарий только свой неанонимный. + Должны быть переданы все поля в теле запроса: оценки, предмет, текст. Их можно получить из GET и изменить нужное, остальное оставить + """ + comment: Comment = Comment.get(session=db.session, id=uuid) # Ошибка, если не найден + + if comment.user_id != user.get("id"): + raise ForbiddenAction(Comment) + + return CommentGet.model_validate(Comment.update(session=db.session, id=uuid, **comment_update.model_dump(), update_ts=datetime.datetime.utcnow(),review_status=ReviewStatus.PENDING)) + @comment.delete("/{uuid}", response_model=StatusResponseModel) async def delete_comment( uuid: UUID, _=Depends(UnionAuth(scopes=["rating.comment.delete"], allow_none=False, auto_error=True)) From 8627d30e21223301ec087ab861597a240b3769c4 Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Sat, 23 Nov 2024 12:38:45 +0300 Subject: [PATCH 03/21] Fix route in PATCH tests --- tests/test_routes/test_comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index ea68fd6..17d7f26 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -176,7 +176,7 @@ def test_comments_by_lecturer_id(client, lecturers_with_comments, lecturer_n, re def test_review_comment(client, dbsession, unreviewed_comment, comment, review_status, response_status, is_reviewed): commment_to_reivew = comment if is_reviewed else unreviewed_comment query = {"review_status": review_status} - response = client.patch(f"{url}/{commment_to_reivew.uuid}", params=query) + response = client.patch(f"{url}/{commment_to_reivew.uuid}/review", params=query) assert response.status_code == response_status if response.status_code == status.HTTP_200_OK: dbsession.refresh(commment_to_reivew) From 569242c89d7d7b25542e47a667579d8fdc22eab7 Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Sat, 23 Nov 2024 15:51:03 +0300 Subject: [PATCH 04/21] Add test_patch_comment - Add test to patch user comments routes - Add fixture for this tests - Add enable set anonym to patch comment --- rating_api/routes/comment.py | 5 ++++- tests/conftest.py | 18 ++++++++++++++++ tests/test_routes/test_comment.py | 36 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 462cb8a..fc08364 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -153,7 +153,10 @@ async def update_comment(uuid: UUID, if comment.user_id != user.get("id"): raise ForbiddenAction(Comment) - return CommentGet.model_validate(Comment.update(session=db.session, id=uuid, **comment_update.model_dump(), update_ts=datetime.datetime.utcnow(),review_status=ReviewStatus.PENDING)) + # Обрабатываем анонимность комментария, и удаляем этот флаг чтобы добавить запись в БД + user_id = None if comment_update.is_anonymous else comment.user_id + + return CommentGet.model_validate(Comment.update(session=db.session, id=uuid, **comment_update.model_dump(exclude={"is_anonymous"}), user_id=user_id, update_ts=datetime.datetime.utcnow(), review_status=ReviewStatus.PENDING)) @comment.delete("/{uuid}", response_model=StatusResponseModel) async def delete_comment( diff --git a/tests/conftest.py b/tests/conftest.py index a0a455e..7a1c8fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,6 +80,24 @@ def unreviewed_comment(dbsession, lecturer): dbsession.delete(_comment) dbsession.commit() +@pytest.fixture +def comment_update(dbsession, lecturer): + _comment = Comment( + subject="update_subject", + text="update_comment", + mark_kindness=1, + mark_clarity=1, + mark_freebie=1, + lecturer_id=lecturer.id, + review_status=ReviewStatus.APPROVED, + user_id=0 + ) + dbsession.add(_comment) + dbsession.commit() + yield _comment + dbsession.refresh(_comment) + dbsession.delete(_comment) + dbsession.commit() @pytest.fixture(scope='function') def lecturers(dbsession): diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 17d7f26..b670434 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -183,6 +183,42 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s assert commment_to_reivew.review_status == ReviewStatus(review_status) +@pytest.mark.parametrize('body, response_status', + [ + ( + {"subject": "test_subject", "text": "test_text", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0,}, + status.HTTP_200_OK + ), + ( + {"subject": 0, "text": "test_text", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0,}, + status.HTTP_422_UNPROCESSABLE_ENTITY + ), + ( + {"subject": "test_subject", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0,}, + status.HTTP_422_UNPROCESSABLE_ENTITY + ), + ( + {"subject": "test_subject", "text": "test_text", "mark_kindness": 5, "mark_freebie": -2, "mark_clarity": 0,}, + status.HTTP_400_BAD_REQUEST + ), + (#Ошибка для анонимных комментариев + {"subject": "test_subject", "text": "test_text", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0, "is_anonymous": True}, + status.HTTP_200_OK + ), + ]) +def test_update_comment(client, dbsession, comment_update, body, response_status): + response = client.patch(f"{url}/{comment_update.uuid}", json=body) + assert response.status_code == response_status + if response.status_code == status.HTTP_200_OK: + dbsession.refresh(comment_update) + assert comment_update.review_status == ReviewStatus.PENDING + + #Проверям, что мы изменяем только те комментарии, у которых user_id пользователя и не null + if "is_anonymous" in body and body['is_anonymous']: + response = client.patch(f"{url}/{comment_update.uuid}", json=body) + assert response.status_code == 403 + + def test_delete_comment(client, dbsession, comment): response = client.delete(f'{url}/{comment.uuid}') assert response.status_code == status.HTTP_200_OK From 9257710eefc1c15cba1aaf0e275076ab79b57fb8 Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Sat, 23 Nov 2024 15:55:45 +0300 Subject: [PATCH 05/21] Make format --- rating_api/routes/comment.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index fc08364..13500f5 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -141,22 +141,30 @@ async def review_comment( @comment.patch("/{uuid}", response_model=CommentGet) -async def update_comment(uuid: UUID, - comment_update: CommentPost = None, - user=Depends(UnionAuth())) -> CommentGet: +async def update_comment(uuid: UUID, comment_update: CommentPost = None, user=Depends(UnionAuth())) -> CommentGet: """ Изменить комментарий только свой неанонимный. Должны быть переданы все поля в теле запроса: оценки, предмет, текст. Их можно получить из GET и изменить нужное, остальное оставить """ comment: Comment = Comment.get(session=db.session, id=uuid) # Ошибка, если не найден - + if comment.user_id != user.get("id"): raise ForbiddenAction(Comment) # Обрабатываем анонимность комментария, и удаляем этот флаг чтобы добавить запись в БД user_id = None if comment_update.is_anonymous else comment.user_id - - return CommentGet.model_validate(Comment.update(session=db.session, id=uuid, **comment_update.model_dump(exclude={"is_anonymous"}), user_id=user_id, update_ts=datetime.datetime.utcnow(), review_status=ReviewStatus.PENDING)) + + return CommentGet.model_validate( + Comment.update( + session=db.session, + id=uuid, + **comment_update.model_dump(exclude={"is_anonymous"}), + user_id=user_id, + update_ts=datetime.datetime.utcnow(), + review_status=ReviewStatus.PENDING, + ) + ) + @comment.delete("/{uuid}", response_model=StatusResponseModel) async def delete_comment( From 04f668626dd428c03c2f02cb2bcdc6b54e8fc7ce Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Sat, 23 Nov 2024 16:08:16 +0300 Subject: [PATCH 06/21] Make format --- tests/conftest.py | 4 +- tests/test_routes/test_comment.py | 86 +++++++++++++++++++++---------- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7a1c8fc..89ad2c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -80,6 +80,7 @@ def unreviewed_comment(dbsession, lecturer): dbsession.delete(_comment) dbsession.commit() + @pytest.fixture def comment_update(dbsession, lecturer): _comment = Comment( @@ -90,7 +91,7 @@ def comment_update(dbsession, lecturer): mark_freebie=1, lecturer_id=lecturer.id, review_status=ReviewStatus.APPROVED, - user_id=0 + user_id=0, ) dbsession.add(_comment) dbsession.commit() @@ -99,6 +100,7 @@ def comment_update(dbsession, lecturer): dbsession.delete(_comment) dbsession.commit() + @pytest.fixture(scope='function') def lecturers(dbsession): """ diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index b670434..314f46b 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -18,7 +18,7 @@ 'body,lecturer_n,response_status', [ ( - { + { "subject": "test_subject", "text": "test_text", "mark_kindness": 1, @@ -40,7 +40,7 @@ status.HTTP_200_OK, ), ( - { + { "subject": "test1_subject", "text": "test_text", "mark_kindness": -2, @@ -85,7 +85,7 @@ status.HTTP_200_OK, ), ( - { # Not provided anonymity + { # Not provided anonymity "subject": "test_subject", "text": "test_text", "mark_kindness": 1, @@ -183,29 +183,61 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s assert commment_to_reivew.review_status == ReviewStatus(review_status) -@pytest.mark.parametrize('body, response_status', - [ - ( - {"subject": "test_subject", "text": "test_text", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0,}, - status.HTTP_200_OK - ), - ( - {"subject": 0, "text": "test_text", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0,}, - status.HTTP_422_UNPROCESSABLE_ENTITY - ), - ( - {"subject": "test_subject", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0,}, - status.HTTP_422_UNPROCESSABLE_ENTITY - ), - ( - {"subject": "test_subject", "text": "test_text", "mark_kindness": 5, "mark_freebie": -2, "mark_clarity": 0,}, - status.HTTP_400_BAD_REQUEST - ), - (#Ошибка для анонимных комментариев - {"subject": "test_subject", "text": "test_text", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0, "is_anonymous": True}, - status.HTTP_200_OK - ), - ]) +@pytest.mark.parametrize( + 'body, response_status', + [ + ( + { + "subject": "test_subject", + "text": "test_text", + "mark_kindness": 0, + "mark_freebie": -2, + "mark_clarity": 0, + }, + status.HTTP_200_OK, + ), + ( + { + "subject": 0, + "text": "test_text", + "mark_kindness": 0, + "mark_freebie": -2, + "mark_clarity": 0, + }, + status.HTTP_422_UNPROCESSABLE_ENTITY, + ), + ( + { + "subject": "test_subject", + "mark_kindness": 0, + "mark_freebie": -2, + "mark_clarity": 0, + }, + status.HTTP_422_UNPROCESSABLE_ENTITY, + ), + ( + { + "subject": "test_subject", + "text": "test_text", + "mark_kindness": 5, + "mark_freebie": -2, + "mark_clarity": 0, + }, + status.HTTP_400_BAD_REQUEST, + ), + ( # Ошибка для анонимных комментариев + { + "subject": "test_subject", + "text": "test_text", + "mark_kindness": 0, + "mark_freebie": -2, + "mark_clarity": 0, + "is_anonymous": True, + }, + status.HTTP_200_OK, + ), + ], +) def test_update_comment(client, dbsession, comment_update, body, response_status): response = client.patch(f"{url}/{comment_update.uuid}", json=body) assert response.status_code == response_status @@ -213,7 +245,7 @@ def test_update_comment(client, dbsession, comment_update, body, response_status dbsession.refresh(comment_update) assert comment_update.review_status == ReviewStatus.PENDING - #Проверям, что мы изменяем только те комментарии, у которых user_id пользователя и не null + # Проверям, что мы изменяем только те комментарии, у которых user_id пользователя и не null if "is_anonymous" in body and body['is_anonymous']: response = client.patch(f"{url}/{comment_update.uuid}", json=body) assert response.status_code == 403 From 18c992fee941e5901ef132f266bcaf108a81619b Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Mon, 25 Nov 2024 01:04:21 +0300 Subject: [PATCH 07/21] Fix up patch route - Fix up tests - Make allow unnecessary attibutes to patch - Add new scheme to CommentUpdate --- rating_api/routes/comment.py | 18 ++++++------------ rating_api/schemas/models.py | 15 +++++++++++++++ tests/test_routes/test_comment.py | 13 +++---------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 13500f5..4abe44e 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -9,7 +9,7 @@ from rating_api.exceptions import ForbiddenAction, ObjectNotFound, TooManyCommentRequests from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus from rating_api.schemas.base import StatusResponseModel -from rating_api.schemas.models import CommentGet, CommentGetAll, CommentPost +from rating_api.schemas.models import CommentGet, CommentGetAll, CommentPost, CommentUpdate from rating_api.settings import Settings, get_settings @@ -141,25 +141,19 @@ async def review_comment( @comment.patch("/{uuid}", response_model=CommentGet) -async def update_comment(uuid: UUID, comment_update: CommentPost = None, user=Depends(UnionAuth())) -> CommentGet: - """ - Изменить комментарий только свой неанонимный. - Должны быть переданы все поля в теле запроса: оценки, предмет, текст. Их можно получить из GET и изменить нужное, остальное оставить - """ +async def update_comment(uuid: UUID, comment_update: CommentUpdate, user=Depends(UnionAuth())) -> CommentGet: + """Позволяет изменить свой неанонимный комментарий. любое поле опционально,""" comment: Comment = Comment.get(session=db.session, id=uuid) # Ошибка, если не найден - if comment.user_id != user.get("id"): + if comment.user_id != user.get("id") or comment.user_id is None: raise ForbiddenAction(Comment) - # Обрабатываем анонимность комментария, и удаляем этот флаг чтобы добавить запись в БД - user_id = None if comment_update.is_anonymous else comment.user_id - return CommentGet.model_validate( Comment.update( session=db.session, id=uuid, - **comment_update.model_dump(exclude={"is_anonymous"}), - user_id=user_id, + # Исключаем атрибуты, котрые не переданы + **comment_update.model_dump(exclude=set(k for k, v in comment_update if v is None)), update_ts=datetime.datetime.utcnow(), review_status=ReviewStatus.PENDING, ) diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index 5f064a9..43072a1 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -36,6 +36,21 @@ def validate_mark(cls, value): return value +class CommentUpdate(Base): + subject: str | None = None + text: str | None = None + mark_kindness: int | None = None + mark_freebie: int | None = None + mark_clarity: int | None = None + + @field_validator('mark_kindness', 'mark_freebie', 'mark_clarity') + @classmethod + def validate_mark(cls, value): + if value not in [-2, -1, 0, 1, 2]: + raise WrongMark() + return value + + class CommentGetAll(Base): comments: list[CommentGet] = [] limit: int diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 314f46b..f87ba75 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -206,14 +206,14 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s }, status.HTTP_422_UNPROCESSABLE_ENTITY, ), - ( + ( # Отсутсвует одно поле { "subject": "test_subject", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0, }, - status.HTTP_422_UNPROCESSABLE_ENTITY, + status.HTTP_200_OK, ), ( { @@ -225,14 +225,12 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s }, status.HTTP_400_BAD_REQUEST, ), - ( # Ошибка для анонимных комментариев + ( # Отсутсвует все поля { "subject": "test_subject", - "text": "test_text", "mark_kindness": 0, "mark_freebie": -2, "mark_clarity": 0, - "is_anonymous": True, }, status.HTTP_200_OK, ), @@ -245,11 +243,6 @@ def test_update_comment(client, dbsession, comment_update, body, response_status dbsession.refresh(comment_update) assert comment_update.review_status == ReviewStatus.PENDING - # Проверям, что мы изменяем только те комментарии, у которых user_id пользователя и не null - if "is_anonymous" in body and body['is_anonymous']: - response = client.patch(f"{url}/{comment_update.uuid}", json=body) - assert response.status_code == 403 - def test_delete_comment(client, dbsession, comment): response = client.delete(f'{url}/{comment.uuid}') From 43794f64ce99fc1999b9ca6aefcae5a89255cb76 Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Mon, 25 Nov 2024 01:31:36 +0300 Subject: [PATCH 08/21] Fix up one test --- tests/test_routes/test_comment.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 99ad59b..669b1f5 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -303,12 +303,7 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s status.HTTP_400_BAD_REQUEST, ), ( # Отсутсвует все поля - { - "subject": "test_subject", - "mark_kindness": 0, - "mark_freebie": -2, - "mark_clarity": 0, - }, + {}, status.HTTP_200_OK, ), ], From 3cecc47f0dbc3e8ae2e058a736788ca7d8bb9fd4 Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Mon, 25 Nov 2024 13:02:23 +0300 Subject: [PATCH 09/21] Modify exclude params --- rating_api/routes/comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index a49b80e..da364ab 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -168,7 +168,7 @@ async def update_comment(uuid: UUID, comment_update: CommentUpdate, user=Depends session=db.session, id=uuid, # Исключаем атрибуты, котрые не переданы - **comment_update.model_dump(exclude=set(k for k, v in comment_update if v is None)), + **comment_update.model_dump(exclude_unset=True), update_ts=datetime.datetime.utcnow(), review_status=ReviewStatus.PENDING, ) From 6c13bf87ba3a9e53232931aba48bb13df0708b56 Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Sat, 30 Nov 2024 20:10:58 +0300 Subject: [PATCH 10/21] Fixup tests --- tests/conftest.py | 6 +++--- tests/test_routes/test_comment.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 4d79003..4fdbf1c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -82,10 +82,10 @@ def unreviewed_comment(dbsession, lecturer): @pytest.fixture -def comment_update(dbsession, lecturer): +def nonanonymous_comment(dbsession, lecturer): _comment = Comment( - subject="update_subject", - text="update_comment", + subject="subject", + text="omment", mark_kindness=1, mark_clarity=1, mark_freebie=1, diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 669b1f5..0f5e030 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -308,12 +308,12 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s ), ], ) -def test_update_comment(client, dbsession, comment_update, body, response_status): - response = client.patch(f"{url}/{comment_update.uuid}", json=body) +def test_update_comment(client, dbsession, nonanonymous_comment, body, response_status): + response = client.patch(f"{url}/{nonanonymous_comment.uuid}", json=body) assert response.status_code == response_status if response.status_code == status.HTTP_200_OK: - dbsession.refresh(comment_update) - assert comment_update.review_status == ReviewStatus.PENDING + dbsession.refresh(nonanonymous_comment) + assert nonanonymous_comment.review_status == ReviewStatus.PENDING def test_delete_comment(client, dbsession, comment): From b44aab318975d7a6df6365f8eb7406ed5bd20c24 Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Fri, 6 Dec 2024 15:36:21 +0300 Subject: [PATCH 11/21] Add validation - Validation if given attribute is still not changed - Validation if no attributes given - Add tests --- rating_api/routes/comment.py | 16 +++++++++++++--- rating_api/schemas/models.py | 10 +++++----- tests/conftest.py | 2 +- tests/test_routes/test_comment.py | 23 ++++++++++++++++++++++- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index da364ab..a6d691d 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -3,12 +3,13 @@ from uuid import UUID from auth_lib.fastapi import UnionAuth -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends, HTTPException, Query from fastapi_sqlalchemy import db from rating_api.exceptions import ForbiddenAction, ObjectNotFound, TooManyCommentRequests from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus from rating_api.schemas.base import StatusResponseModel +from starlette import status from rating_api.schemas.models import CommentGet, CommentGetAll, CommentPost, CommentUpdate from rating_api.settings import Settings, get_settings @@ -157,12 +158,21 @@ async def review_comment( @comment.patch("/{uuid}", response_model=CommentGet) async def update_comment(uuid: UUID, comment_update: CommentUpdate, user=Depends(UnionAuth())) -> CommentGet: - """Позволяет изменить свой неанонимный комментарий. любое поле опционально,""" + """Позволяет изменить свой неанонимный комментарий""" comment: Comment = Comment.get(session=db.session, id=uuid) # Ошибка, если не найден if comment.user_id != user.get("id") or comment.user_id is None: raise ForbiddenAction(Comment) - + + # Если не передано ни одного параметра + if not comment_update.model_dump(exclude_unset=True): + raise HTTPException(status_code=409, detail="Provide any parametr") # 409 + + # Если хоть одно поле было передано неизменным + if set(comment.__dict__.items()).intersection(set(comment_update.model_dump(exclude_unset=True).items())): + raise HTTPException(status_code=426, detail="No changes detected") # 426 + + # Проверить поле update_ts create comment на null return CommentGet.model_validate( Comment.update( session=db.session, diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index 61471c3..addb74a 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -40,11 +40,11 @@ def validate_mark(cls, value): class CommentUpdate(Base): - subject: str | None = None - text: str | None = None - mark_kindness: int | None = None - mark_freebie: int | None = None - mark_clarity: int | None = None + subject: str = None + text: str = None + mark_kindness: int = None + mark_freebie: int = None + mark_clarity: int = None @field_validator('mark_kindness', 'mark_freebie', 'mark_clarity') @classmethod diff --git a/tests/conftest.py b/tests/conftest.py index 4fdbf1c..d02ef1e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -85,7 +85,7 @@ def unreviewed_comment(dbsession, lecturer): def nonanonymous_comment(dbsession, lecturer): _comment = Comment( subject="subject", - text="omment", + text="comment", mark_kindness=1, mark_clarity=1, mark_freebie=1, diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 0f5e030..4d19c59 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -304,8 +304,29 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s ), ( # Отсутсвует все поля {}, - status.HTTP_200_OK, + status.HTTP_409_CONFLICT, + ), + ( # Переданы НЕизмененные поля + { + "subject":"subject", + "text":"comment", + "mark_kindness":1, + "mark_clarity":1, + "mark_freebie":1, + }, + status.HTTP_426_UPGRADE_REQUIRED, + ), + ( # НЕизмененным перелано одно поле + { + "subject":"asf", + "text":"asf", + "mark_kindness":2, + "mark_clarity":2, + "mark_freebie":1, + }, + status.HTTP_426_UPGRADE_REQUIRED, ), + ], ) def test_update_comment(client, dbsession, nonanonymous_comment, body, response_status): From 99841080c6bc612eda6f6fca743a983167336bbd Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Fri, 6 Dec 2024 15:41:34 +0300 Subject: [PATCH 12/21] Make format --- rating_api/routes/comment.py | 7 +++---- tests/test_routes/test_comment.py | 23 +++++++++++------------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index a6d691d..a423d27 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -9,7 +9,6 @@ from rating_api.exceptions import ForbiddenAction, ObjectNotFound, TooManyCommentRequests from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus from rating_api.schemas.base import StatusResponseModel -from starlette import status from rating_api.schemas.models import CommentGet, CommentGetAll, CommentPost, CommentUpdate from rating_api.settings import Settings, get_settings @@ -163,15 +162,15 @@ async def update_comment(uuid: UUID, comment_update: CommentUpdate, user=Depends if comment.user_id != user.get("id") or comment.user_id is None: raise ForbiddenAction(Comment) - + # Если не передано ни одного параметра if not comment_update.model_dump(exclude_unset=True): raise HTTPException(status_code=409, detail="Provide any parametr") # 409 - + # Если хоть одно поле было передано неизменным if set(comment.__dict__.items()).intersection(set(comment_update.model_dump(exclude_unset=True).items())): raise HTTPException(status_code=426, detail="No changes detected") # 426 - + # Проверить поле update_ts create comment на null return CommentGet.model_validate( Comment.update( diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 4d19c59..9575f16 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -308,25 +308,24 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s ), ( # Переданы НЕизмененные поля { - "subject":"subject", - "text":"comment", - "mark_kindness":1, - "mark_clarity":1, - "mark_freebie":1, + "subject": "subject", + "text": "comment", + "mark_kindness": 1, + "mark_clarity": 1, + "mark_freebie": 1, }, status.HTTP_426_UPGRADE_REQUIRED, ), - ( # НЕизмененным перелано одно поле + ( # НЕизмененным перелано одно поле { - "subject":"asf", - "text":"asf", - "mark_kindness":2, - "mark_clarity":2, - "mark_freebie":1, + "subject": "asf", + "text": "asf", + "mark_kindness": 2, + "mark_clarity": 2, + "mark_freebie": 1, }, status.HTTP_426_UPGRADE_REQUIRED, ), - ], ) def test_update_comment(client, dbsession, nonanonymous_comment, body, response_status): From b6f8b08d09b643c46f670f9f3a2aad63607ac66e Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Mon, 9 Dec 2024 13:40:54 +0300 Subject: [PATCH 13/21] Fix up tests --- tests/test_routes/test_comment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 9575f16..47e3f96 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -334,6 +334,8 @@ def test_update_comment(client, dbsession, nonanonymous_comment, body, response_ if response.status_code == status.HTTP_200_OK: dbsession.refresh(nonanonymous_comment) assert nonanonymous_comment.review_status == ReviewStatus.PENDING + for k, v in body.items(): + getattr(nonanonymous_comment, k, None) == v # Есть ли изменения в БД def test_delete_comment(client, dbsession, comment): From 5cc17d086f288374a58ea31229b1133055e9dae5 Mon Sep 17 00:00:00 2001 From: DR0P-database Date: Mon, 9 Dec 2024 13:45:17 +0300 Subject: [PATCH 14/21] IMPORTATNT fix ip tests --- tests/test_routes/test_comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 47e3f96..0a062fa 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -335,7 +335,7 @@ def test_update_comment(client, dbsession, nonanonymous_comment, body, response_ dbsession.refresh(nonanonymous_comment) assert nonanonymous_comment.review_status == ReviewStatus.PENDING for k, v in body.items(): - getattr(nonanonymous_comment, k, None) == v # Есть ли изменения в БД + assert getattr(nonanonymous_comment, k, None) == v # Есть ли изменения в БД def test_delete_comment(client, dbsession, comment): From c6fb5422f7974be17f5b5975ca079d49701a7481 Mon Sep 17 00:00:00 2001 From: DROPDATABASE Date: Sat, 8 Feb 2025 00:34:53 +0300 Subject: [PATCH 15/21] Fixup --- rating_api/routes/comment.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 9240ceb..a619f0f 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -216,26 +216,31 @@ async def update_comment(uuid: UUID, comment_update: CommentUpdate, user=Depends if comment.user_id != user.get("id") or comment.user_id is None: raise ForbiddenAction(Comment) + # Получаем только переданные для обновления поля + update_data = comment_update.model_dump(exclude_unset=True) + # Если не передано ни одного параметра - if not comment_update.model_dump(exclude_unset=True): + if not update_comment: raise HTTPException(status_code=409, detail="Provide any parametr") # 409 - # Если хоть одно поле было передано неизменным - if set(comment.__dict__.items()).intersection(set(comment_update.model_dump(exclude_unset=True).items())): - raise HTTPException(status_code=426, detail="No changes detected") # 426 + # Проверяем, есть ли неизмененные поля + current_data = {key: getattr(comment, key) for key in update_data} # Берем текущие значения из БД + unchanged_fields = {k for k, v in update_data.items() if current_data.get(k) == v} - # Проверить поле update_ts create comment на null - return CommentGet.model_validate( - Comment.update( - session=db.session, - id=uuid, - # Исключаем атрибуты, котрые не переданы - **comment_update.model_dump(exclude_unset=True), - update_ts=datetime.datetime.utcnow(), - review_status=ReviewStatus.PENDING, - ) + if unchanged_fields: + raise HTTPException(status_code=409, detail=f"No changes detected in fields: {', '.join(unchanged_fields)}") + + # Обновление комментария + updated_comment = Comment.update( + session=db.session, + id=uuid, + **update_data, + update_ts=datetime.datetime.utcnow(), + review_status=ReviewStatus.PENDING, ) + return CommentGet.model_validate(updated_comment) + @comment.delete("/{uuid}", response_model=StatusResponseModel) async def delete_comment( From 0ed81cb349ad8d72820f594cb480e95cad3f1b67 Mon Sep 17 00:00:00 2001 From: DROPDATABASE Date: Sat, 8 Feb 2025 00:45:03 +0300 Subject: [PATCH 16/21] Fixup - tests - pathc in not provide any parametr --- rating_api/routes/comment.py | 2 +- rating_api/routes/lecturer.py | 4 +++- tests/test_routes/test_comment.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index a619f0f..51dc2ea 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -220,7 +220,7 @@ async def update_comment(uuid: UUID, comment_update: CommentUpdate, user=Depends update_data = comment_update.model_dump(exclude_unset=True) # Если не передано ни одного параметра - if not update_comment: + if not update_data: raise HTTPException(status_code=409, detail="Provide any parametr") # 409 # Проверяем, есть ли неизмененные поля diff --git a/rating_api/routes/lecturer.py b/rating_api/routes/lecturer.py index c81e165..e7c0e4d 100644 --- a/rating_api/routes/lecturer.py +++ b/rating_api/routes/lecturer.py @@ -135,7 +135,9 @@ async def get_lecturers( if comment.review_status is ReviewStatus.APPROVED ] if "comments" in info and approved_comments: - lecturer_to_result.comments = sorted(approved_comments, key=lambda comment: comment.create_ts, reverse=True) + lecturer_to_result.comments = sorted( + approved_comments, key=lambda comment: comment.create_ts, reverse=True + ) if "mark" in info and approved_comments: lecturer_to_result.mark_freebie = sum([comment.mark_freebie for comment in approved_comments]) / len( approved_comments diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 6bda6cc..bb1688b 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -265,7 +265,7 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s "mark_clarity": 1, "mark_freebie": 1, }, - status.HTTP_426_UPGRADE_REQUIRED, + status.HTTP_409_CONFLICT, ), ( # НЕизмененным перелано одно поле { @@ -275,7 +275,7 @@ def test_review_comment(client, dbsession, unreviewed_comment, comment, review_s "mark_clarity": 2, "mark_freebie": 1, }, - status.HTTP_426_UPGRADE_REQUIRED, + status.HTTP_409_CONFLICT, ), ], ) From 90efde1684acc34a5cb9f26afe65edcf3d1622d0 Mon Sep 17 00:00:00 2001 From: DROPDATABASE Date: Sat, 8 Feb 2025 10:00:38 +0300 Subject: [PATCH 17/21] Cherry pick github actions --- .github/workflows/checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index adbafe2..233ac3c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -10,6 +10,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + - name: Init daemon.json + run: sudo touch /etc/docker/daemon.json - name: Set up docker uses: docker-practice/actions-setup-docker@master - name: Run postgres From 6016f69b9d7211cb924d7a8df4b94dbc0e42bc1e Mon Sep 17 00:00:00 2001 From: DROPDATABASE Date: Sat, 1 Mar 2025 21:09:21 +0300 Subject: [PATCH 18/21] Make custom raise exceptions --- rating_api/exceptions.py | 8 ++++++++ rating_api/routes/comment.py | 16 ++++++++++++---- rating_api/routes/exc_handlers.py | 8 ++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/rating_api/exceptions.py b/rating_api/exceptions.py index c0b57d4..6ab1a7a 100644 --- a/rating_api/exceptions.py +++ b/rating_api/exceptions.py @@ -63,3 +63,11 @@ def __init__(self): super().__init__( f"Ratings can only take values: -2, -1, 0, 1, 2", f"Оценки могут принимать только значения: -2, -1, 0, 1, 2" ) + + +class UpdateError(RatingAPIError): + def __init__(self, msg: str): + super().__init__( + f"{msg} Conflict with update a resource that already exists or has conflicting information.", + f"{msg} Конфликт с обновлением ресурса, который уже существует или имеет противоречивую информацию.", + ) diff --git a/rating_api/routes/comment.py b/rating_api/routes/comment.py index 51dc2ea..93518d3 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -3,10 +3,16 @@ from uuid import UUID from auth_lib.fastapi import UnionAuth -from fastapi import APIRouter, Depends, HTTPException, Query +from fastapi import APIRouter, Depends, Query from fastapi_sqlalchemy import db -from rating_api.exceptions import ForbiddenAction, ObjectNotFound, TooManyCommentRequests, TooManyCommentsToLecturer +from rating_api.exceptions import ( + ForbiddenAction, + ObjectNotFound, + TooManyCommentRequests, + TooManyCommentsToLecturer, + UpdateError, +) from rating_api.models import Comment, Lecturer, LecturerUserComment, ReviewStatus from rating_api.schemas.base import StatusResponseModel from rating_api.schemas.models import CommentGet, CommentGetAll, CommentImportAll, CommentPost, CommentUpdate @@ -221,14 +227,16 @@ async def update_comment(uuid: UUID, comment_update: CommentUpdate, user=Depends # Если не передано ни одного параметра if not update_data: - raise HTTPException(status_code=409, detail="Provide any parametr") # 409 + raise UpdateError(msg="Provide any parametr.") + # raise HTTPException(status_code=409, detail="Provide any parametr") # 409 # Проверяем, есть ли неизмененные поля current_data = {key: getattr(comment, key) for key in update_data} # Берем текущие значения из БД unchanged_fields = {k for k, v in update_data.items() if current_data.get(k) == v} if unchanged_fields: - raise HTTPException(status_code=409, detail=f"No changes detected in fields: {', '.join(unchanged_fields)}") + raise UpdateError(msg=f"No changes detected in fields: {', '.join(unchanged_fields)}.") + # raise HTTPException(status_code=409, detail=f"No changes detected in fields: {', '.join(unchanged_fields)}") # Обновление комментария updated_comment = Comment.update( diff --git a/rating_api/routes/exc_handlers.py b/rating_api/routes/exc_handlers.py index a0f0d4b..af7fd95 100644 --- a/rating_api/routes/exc_handlers.py +++ b/rating_api/routes/exc_handlers.py @@ -7,6 +7,7 @@ ObjectNotFound, TooManyCommentRequests, TooManyCommentsToLecturer, + UpdateError, WrongMark, ) from rating_api.schemas.base import StatusResponseModel @@ -54,3 +55,10 @@ async def wrong_mark_handler(req: starlette.requests.Request, exc: WrongMark): return JSONResponse( content=StatusResponseModel(status="Error", message=exc.eng, ru=exc.ru).model_dump(), status_code=400 ) + + +@app.exception_handler(UpdateError) +async def update_error_handler(req: starlette.requests.Request, exc: UpdateError): + return JSONResponse( + content=StatusResponseModel(status="Error", message=exc.eng, ru=exc.ru).model_dump(), status_code=409 + ) From 23bbf2c2cd8e89c670b8165e49fe86776a1c9616 Mon Sep 17 00:00:00 2001 From: DROPDATABASE Date: Sat, 1 Mar 2025 22:03:35 +0300 Subject: [PATCH 19/21] fixup test create_comment#7 --- tests/test_routes/test_comment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index c109f0f..a962d1b 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -97,7 +97,7 @@ 0, status.HTTP_200_OK, ), - ( # Bad anonymity + ( # Not provided anonymity { "subject": "test_subject", "text": "test text", @@ -106,9 +106,9 @@ "mark_clarity": 0, }, 0, - status.HTTP_422_UNPROCESSABLE_ENTITY, + status.HTTP_200_OK, ), - ( # Not provided anonymity + ( # Bad anonymity { "subject": "test_subject", "text": "test text", From 6cd41b6c14adb886b5d0a53c119c5ba2f943818c Mon Sep 17 00:00:00 2001 From: DROPDATABASE Date: Sat, 1 Mar 2025 22:12:27 +0300 Subject: [PATCH 20/21] Fixup --- tests/test_routes/test_comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index a962d1b..4180098 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -85,7 +85,7 @@ 0, status.HTTP_200_OK, ), - ( # NotAnonymous comment + ( # NotAnonymous comment { "subject": "test_subject", "text": "test text", From f9015e83642998499177cf6f0aba0c9a04861b95 Mon Sep 17 00:00:00 2001 From: DROPDATABASE Date: Sat, 1 Mar 2025 22:15:06 +0300 Subject: [PATCH 21/21] Format linting --- tests/test_routes/test_comment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_routes/test_comment.py b/tests/test_routes/test_comment.py index 4180098..69292a4 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -85,7 +85,7 @@ 0, status.HTTP_200_OK, ), - ( # NotAnonymous comment + ( # NotAnonymous comment { "subject": "test_subject", "text": "test text", @@ -97,7 +97,7 @@ 0, status.HTTP_200_OK, ), - ( # Not provided anonymity + ( # Not provided anonymity { "subject": "test_subject", "text": "test text", @@ -108,7 +108,7 @@ 0, status.HTTP_200_OK, ), - ( # Bad anonymity + ( # Bad anonymity { "subject": "test_subject", "text": "test text",