diff --git a/rating_api/exceptions.py b/rating_api/exceptions.py index 8252729..2ac6ca7 100644 --- a/rating_api/exceptions.py +++ b/rating_api/exceptions.py @@ -79,3 +79,11 @@ def __init__(self): f"The comment contains forbidden symbols. Letters of English and Russian languages, numbers and punctuation marks are allowed", f"Комментарий содержит запрещенный символ. Разрешены буквы английского и русского языков, цифры и знаки препинания", ) + + +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 cfb2377..a911c1a 100644 --- a/rating_api/routes/comment.py +++ b/rating_api/routes/comment.py @@ -15,10 +15,11 @@ 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 +from rating_api.schemas.models import CommentGet, CommentGetAll, CommentImportAll, CommentPost, CommentUpdate from rating_api.settings import Settings, get_settings @@ -224,7 +225,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, @@ -246,6 +247,42 @@ 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: 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) + + # Получаем только переданные для обновления поля + update_data = comment_update.model_dump(exclude_unset=True) + + # Если не передано ни одного параметра + if not update_data: + 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 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( + 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( uuid: UUID, _=Depends(UnionAuth(scopes=["rating.comment.delete"], allow_none=False, auto_error=True)) diff --git a/rating_api/routes/exc_handlers.py b/rating_api/routes/exc_handlers.py index 8c8e277..3873387 100644 --- a/rating_api/routes/exc_handlers.py +++ b/rating_api/routes/exc_handlers.py @@ -9,6 +9,7 @@ ObjectNotFound, TooManyCommentRequests, TooManyCommentsToLecturer, + UpdateError, WrongMark, ) from rating_api.schemas.base import StatusResponseModel @@ -70,3 +71,10 @@ async def forbidden_symbol_handler(req: starlette.requests.Request, exc: Forbidd 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 + ) diff --git a/rating_api/schemas/models.py b/rating_api/schemas/models.py index 6b84bce..e24dd53 100644 --- a/rating_api/schemas/models.py +++ b/rating_api/schemas/models.py @@ -39,6 +39,21 @@ def validate_mark(cls, value): return value +class CommentUpdate(Base): + 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 + def validate_mark(cls, value): + if value not in [-2, -1, 0, 1, 2]: + raise WrongMark() + return value + + class CommentImport(CommentPost): lecturer_id: int subject: str | None = None diff --git a/tests/conftest.py b/tests/conftest.py index f5d7f16..d02ef1e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -81,6 +81,26 @@ def unreviewed_comment(dbsession, lecturer): dbsession.commit() +@pytest.fixture +def nonanonymous_comment(dbsession, lecturer): + _comment = Comment( + subject="subject", + text="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 110187a..69292a4 100644 --- a/tests/test_routes/test_comment.py +++ b/tests/test_routes/test_comment.py @@ -25,7 +25,6 @@ "mark_kindness": 1, "mark_freebie": 0, "mark_clarity": 0, - "is_anonymous": False, }, 0, status.HTTP_200_OK, @@ -48,7 +47,6 @@ "mark_kindness": -2, "mark_freebie": -2, "mark_clarity": -2, - "is_anonymous": False, }, 1, status.HTTP_200_OK, @@ -60,7 +58,6 @@ "mark_kindness": 5, "mark_freebie": -2, "mark_clarity": 0, - "is_anonymous": False, }, 2, status.HTTP_400_BAD_REQUEST, @@ -72,7 +69,6 @@ "mark_kindness": 1, "mark_freebie": -2, "mark_clarity": 0, - "is_anonymous": False, }, 3, status.HTTP_404_NOT_FOUND, @@ -101,19 +97,18 @@ 0, status.HTTP_200_OK, ), - ( # Bad anonymity + ( # Not provided anonymity { "subject": "test_subject", "text": "test text", "mark_kindness": 1, "mark_freebie": -2, "mark_clarity": 0, - "is_anonymous": 'asd', }, 0, - status.HTTP_422_UNPROCESSABLE_ENTITY, + status.HTTP_200_OK, ), - ( # Not provided anonymity + ( # Bad anonymity { "subject": "test_subject", "text": "test text", @@ -261,13 +256,91 @@ def test_comments_by_user_id(client, lecturers_with_comments, user_id, response_ 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) 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_200_OK, + ), + ( + { + "subject": "test_subject", + "text": "test_text", + "mark_kindness": 5, + "mark_freebie": -2, + "mark_clarity": 0, + }, + status.HTTP_400_BAD_REQUEST, + ), + ( # Отсутсвует все поля + {}, + status.HTTP_409_CONFLICT, + ), + ( # Переданы НЕизмененные поля + { + "subject": "subject", + "text": "comment", + "mark_kindness": 1, + "mark_clarity": 1, + "mark_freebie": 1, + }, + status.HTTP_409_CONFLICT, + ), + ( # НЕизмененным перелано одно поле + { + "subject": "asf", + "text": "asf", + "mark_kindness": 2, + "mark_clarity": 2, + "mark_freebie": 1, + }, + status.HTTP_409_CONFLICT, + ), + ], +) +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(nonanonymous_comment) + assert nonanonymous_comment.review_status == ReviewStatus.PENDING + for k, v in body.items(): + assert getattr(nonanonymous_comment, k, None) == v # Есть ли изменения в БД + + def test_delete_comment(client, dbsession, comment): response = client.delete(f'{url}/{comment.uuid}') assert response.status_code == status.HTTP_200_OK