From e090cadb617c1d393d2019d648857e6313ae82bd Mon Sep 17 00:00:00 2001 From: Max Chis Date: Sun, 4 May 2025 10:56:02 -0400 Subject: [PATCH] feat(app): Add special error message for annotation user conflict --- core/AsyncCore.py | 36 +++++++---- core/classes/ErrorManager.py | 44 +++++++++++++ core/enums.py | 5 ++ core/helpers.py | 4 ++ .../api/helpers/RequestValidator.py | 4 +- .../integration/api/test_annotate.py | 62 +++++++++++++++++++ 6 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 core/classes/ErrorManager.py diff --git a/core/AsyncCore.py b/core/AsyncCore.py index f1d69fb2..46ccca0d 100644 --- a/core/AsyncCore.py +++ b/core/AsyncCore.py @@ -1,6 +1,7 @@ from typing import Optional from pydantic import BaseModel +from sqlalchemy.exc import IntegrityError from collector_db.AsyncDatabaseClient import AsyncDatabaseClient from collector_db.DTOs.BatchInfo import BatchInfo @@ -27,7 +28,8 @@ from core.DTOs.MessageResponse import MessageResponse from core.DTOs.SearchURLResponse import SearchURLResponse from core.TaskManager import TaskManager -from core.enums import BatchStatus, RecordType +from core.classes.ErrorManager import ErrorManager +from core.enums import BatchStatus, RecordType, AnnotationType from security_manager.SecurityManager import AccessInfo @@ -149,11 +151,17 @@ async def submit_url_relevance_annotation( url_id: int, relevant: bool ): - return await self.adb_client.add_user_relevant_suggestion( - user_id=user_id, - url_id=url_id, - relevant=relevant - ) + try: + return await self.adb_client.add_user_relevant_suggestion( + user_id=user_id, + url_id=url_id, + relevant=relevant + ) + except IntegrityError as e: + return await ErrorManager.raise_annotation_exists_error( + annotation_type=AnnotationType.RELEVANCE, + url_id=url_id + ) async def get_next_url_for_relevance_annotation( self, @@ -187,11 +195,17 @@ async def submit_url_record_type_annotation( url_id: int, record_type: RecordType, ): - await self.adb_client.add_user_record_type_suggestion( - user_id=user_id, - url_id=url_id, - record_type=record_type - ) + try: + return await self.adb_client.add_user_record_type_suggestion( + user_id=user_id, + url_id=url_id, + record_type=record_type + ) + except IntegrityError as e: + return await ErrorManager.raise_annotation_exists_error( + annotation_type=AnnotationType.RECORD_TYPE, + url_id=url_id + ) async def get_next_url_agency_for_annotation( diff --git a/core/classes/ErrorManager.py b/core/classes/ErrorManager.py new file mode 100644 index 00000000..ba763054 --- /dev/null +++ b/core/classes/ErrorManager.py @@ -0,0 +1,44 @@ +from enum import Enum +from http import HTTPStatus + +from fastapi import HTTPException +from pydantic import BaseModel + +from core.enums import AnnotationType + + +class ErrorTypes(Enum): + ANNOTATION_EXISTS = "ANNOTATION_EXISTS" + +class ErrorFormat(BaseModel): + code: ErrorTypes + message: str + + +class ErrorManager: + + @staticmethod + async def raise_error( + error_type: ErrorTypes, + message: str, + status_code: HTTPStatus = HTTPStatus.BAD_REQUEST + ): + raise HTTPException( + status_code=status_code, + detail=ErrorFormat( + code=error_type, + message=message + ).model_dump(mode='json') + ) + + @staticmethod + async def raise_annotation_exists_error( + annotation_type: AnnotationType, + url_id: int + ): + await ErrorManager.raise_error( + error_type=ErrorTypes.ANNOTATION_EXISTS, + message=f"Annotation of type {annotation_type.value} already exists" + f" for url {url_id}", + status_code=HTTPStatus.CONFLICT + ) diff --git a/core/enums.py b/core/enums.py index 173c66e9..019572b8 100644 --- a/core/enums.py +++ b/core/enums.py @@ -1,5 +1,10 @@ from enum import Enum +class AnnotationType(Enum): + RELEVANCE = "RELEVANCE" + RECORD_TYPE = "RECORD_TYPE" + AGENCY = "AGENCY" + class BatchStatus(Enum): READY_TO_LABEL = "ready to label" diff --git a/core/helpers.py b/core/helpers.py index bac603bd..1fc51cde 100644 --- a/core/helpers.py +++ b/core/helpers.py @@ -1,3 +1,7 @@ +from http import HTTPStatus + +from fastapi import HTTPException + from core.DTOs.URLAgencySuggestionInfo import URLAgencySuggestionInfo from core.enums import SuggestionType from core.exceptions import MatchAgencyError diff --git a/tests/test_automated/integration/api/helpers/RequestValidator.py b/tests/test_automated/integration/api/helpers/RequestValidator.py index c2d246f5..91d27729 100644 --- a/tests/test_automated/integration/api/helpers/RequestValidator.py +++ b/tests/test_automated/integration/api/helpers/RequestValidator.py @@ -246,7 +246,7 @@ def post_record_type_annotation_and_get_next( url_id: int, record_type_annotation_post_info: RecordTypeAnnotationPostInfo ) -> GetNextRecordTypeAnnotationResponseOuterInfo: - data = self.post( + data = self.post_v2( url=f"/annotate/record-type/{url_id}", json=record_type_annotation_post_info.model_dump(mode='json') ) @@ -257,7 +257,7 @@ def post_relevance_annotation_and_get_next( url_id: int, relevance_annotation_post_info: RelevanceAnnotationPostInfo ) -> GetNextRelevanceAnnotationResponseOuterInfo: - data = self.post( + data = self.post_v2( url=f"/annotate/relevance/{url_id}", json=relevance_annotation_post_info.model_dump(mode='json') ) diff --git a/tests/test_automated/integration/api/test_annotate.py b/tests/test_automated/integration/api/test_annotate.py index a03540a1..03088cd7 100644 --- a/tests/test_automated/integration/api/test_annotate.py +++ b/tests/test_automated/integration/api/test_annotate.py @@ -1,6 +1,7 @@ from http import HTTPStatus import pytest +from fastapi import HTTPException from collector_db.DTOs.InsertURLsInfo import InsertURLsInfo from collector_db.DTOs.URLMapping import URLMapping @@ -11,6 +12,7 @@ from core.DTOs.GetNextURLForAgencyAnnotationResponse import URLAgencyAnnotationPostInfo from core.DTOs.RecordTypeAnnotationPostInfo import RecordTypeAnnotationPostInfo from core.DTOs.RelevanceAnnotationPostInfo import RelevanceAnnotationPostInfo +from core.classes.ErrorManager import ErrorTypes from core.enums import RecordType, SuggestionType from core.exceptions import FailedValidationException from tests.helpers.complex_test_data_functions import AnnotateAgencySetupInfo, setup_for_annotate_agency, \ @@ -130,6 +132,36 @@ async def test_annotate_relevancy(api_test_helper): assert results[0].relevant is True +@pytest.mark.asyncio +async def test_annotate_relevancy_already_annotated_by_different_user( + api_test_helper +): + ath = api_test_helper + + creation_info: BatchURLCreationInfo = await ath.db_data_creator.batch_and_urls( + url_count=1 + ) + + await ath.db_data_creator.user_relevant_suggestion( + url_id=creation_info.url_ids[0], + user_id=2, + relevant=True + ) + + # Annotate with different user (default is 1) and get conflict error + try: + response = await ath.request_validator.post_relevance_annotation_and_get_next( + url_id=creation_info.url_ids[0], + relevance_annotation_post_info=RelevanceAnnotationPostInfo( + is_relevant=False + ) + ) + except HTTPException as e: + assert e.status_code == HTTPStatus.CONFLICT + assert e.detail["detail"]["code"] == ErrorTypes.ANNOTATION_EXISTS.value + assert e.detail["detail"]["message"] == f"Annotation of type RELEVANCE already exists for url {creation_info.url_ids[0]}" + + @pytest.mark.asyncio async def test_annotate_relevancy_no_html(api_test_helper): ath = api_test_helper @@ -250,6 +282,36 @@ async def test_annotate_record_type(api_test_helper): if result.url_id == inner_info_1.url_info.url_id: assert result.record_type == RecordType.BOOKING_REPORTS.value +@pytest.mark.asyncio +async def test_annotate_record_type_already_annotated_by_different_user( + api_test_helper +): + ath = api_test_helper + + creation_info: BatchURLCreationInfo = await ath.db_data_creator.batch_and_urls( + url_count=1 + ) + + await ath.db_data_creator.user_record_type_suggestion( + url_id=creation_info.url_ids[0], + user_id=2, + record_type=RecordType.ACCIDENT_REPORTS + ) + + # Annotate with different user (default is 1) and get conflict error + try: + response = await ath.request_validator.post_record_type_annotation_and_get_next( + url_id=creation_info.url_ids[0], + record_type_annotation_post_info=RecordTypeAnnotationPostInfo( + record_type=RecordType.ANNUAL_AND_MONTHLY_REPORTS + ) + ) + except HTTPException as e: + assert e.status_code == HTTPStatus.CONFLICT + assert e.detail["detail"]["code"] == ErrorTypes.ANNOTATION_EXISTS.value + assert e.detail["detail"]["message"] == f"Annotation of type RECORD_TYPE already exists for url {creation_info.url_ids[0]}" + + @pytest.mark.asyncio async def test_annotate_record_type_no_html_info(api_test_helper): ath = api_test_helper