diff --git a/alembic/versions/2025_10_13_2007-7aace6587d1a_add_anonymous_annotation_tables.py b/alembic/versions/2025_10_13_2007-7aace6587d1a_add_anonymous_annotation_tables.py new file mode 100644 index 00000000..18cf4230 --- /dev/null +++ b/alembic/versions/2025_10_13_2007-7aace6587d1a_add_anonymous_annotation_tables.py @@ -0,0 +1,60 @@ +"""Add anonymous annotation tables + +Revision ID: 7aace6587d1a +Revises: 43077d7e08c5 +Create Date: 2025-10-13 20:07:18.388899 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +from src.util.alembic_helpers import url_id_column, agency_id_column, created_at_column, location_id_column, enum_column + +# revision identifiers, used by Alembic. +revision: str = '7aace6587d1a' +down_revision: Union[str, None] = '43077d7e08c5' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "anonymous_annotation_agency", + url_id_column(), + agency_id_column(), + created_at_column(), + sa.PrimaryKeyConstraint('url_id', 'agency_id') + ) + op.create_table( + "anonymous_annotation_location", + url_id_column(), + location_id_column(), + created_at_column(), + sa.PrimaryKeyConstraint('url_id', 'location_id') + ) + op.create_table( + "anonymous_annotation_record_type", + url_id_column(), + enum_column( + column_name="record_type", + enum_name="record_type" + ), + created_at_column(), + sa.PrimaryKeyConstraint('url_id', 'record_type') + ) + op.create_table( + "anonymous_annotation_url_type", + url_id_column(), + enum_column( + column_name="url_type", + enum_name="url_type" + ), + created_at_column(), + sa.PrimaryKeyConstraint('url_id', 'url_type') + ) + + +def downgrade() -> None: + pass diff --git a/src/api/endpoints/annotate/_shared/extract.py b/src/api/endpoints/annotate/_shared/extract.py new file mode 100644 index 00000000..390579d9 --- /dev/null +++ b/src/api/endpoints/annotate/_shared/extract.py @@ -0,0 +1,64 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from src.api.endpoints.annotate._shared.queries.get_annotation_batch_info import GetAnnotationBatchInfoQueryBuilder +from src.api.endpoints.annotate.all.get.models.agency import AgencyAnnotationResponseOuterInfo +from src.api.endpoints.annotate.all.get.models.location import LocationAnnotationResponseOuterInfo +from src.api.endpoints.annotate.all.get.models.name import NameAnnotationSuggestion +from src.api.endpoints.annotate.all.get.models.record_type import RecordTypeAnnotationSuggestion +from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse, \ + GetNextURLForAllAnnotationInnerResponse +from src.api.endpoints.annotate.all.get.models.url_type import URLTypeAnnotationSuggestion +from src.api.endpoints.annotate.all.get.queries.agency.core import GetAgencySuggestionsQueryBuilder +from src.api.endpoints.annotate.all.get.queries.convert import \ + convert_user_url_type_suggestion_to_url_type_annotation_suggestion, \ + convert_user_record_type_suggestion_to_record_type_annotation_suggestion +from src.api.endpoints.annotate.all.get.queries.location_.core import GetLocationSuggestionsQueryBuilder +from src.api.endpoints.annotate.all.get.queries.name.core import GetNameSuggestionsQueryBuilder +from src.db.dto_converter import DTOConverter +from src.db.dtos.url.mapping import URLMapping +from src.db.models.impl.url.core.sqlalchemy import URL +from src.db.models.impl.url.suggestion.agency.user import UserUrlAgencySuggestion + + +async def extract_and_format_get_annotation_result( + session: AsyncSession, + url: URL, + batch_id: int | None = None +): + html_response_info = DTOConverter.html_content_list_to_html_response_info( + url.html_content + ) + url_type_suggestions: list[URLTypeAnnotationSuggestion] = \ + convert_user_url_type_suggestion_to_url_type_annotation_suggestion( + url.user_relevant_suggestions + ) + record_type_suggestions: list[RecordTypeAnnotationSuggestion] = \ + convert_user_record_type_suggestion_to_record_type_annotation_suggestion( + url.user_record_type_suggestions + ) + agency_suggestions: AgencyAnnotationResponseOuterInfo = \ + await GetAgencySuggestionsQueryBuilder(url_id=url.id).run(session) + location_suggestions: LocationAnnotationResponseOuterInfo = \ + await GetLocationSuggestionsQueryBuilder(url_id=url.id).run(session) + name_suggestions: list[NameAnnotationSuggestion] = \ + await GetNameSuggestionsQueryBuilder(url_id=url.id).run(session) + return GetNextURLForAllAnnotationResponse( + next_annotation=GetNextURLForAllAnnotationInnerResponse( + url_info=URLMapping( + url_id=url.id, + url=url.url + ), + html_info=html_response_info, + url_type_suggestions=url_type_suggestions, + record_type_suggestions=record_type_suggestions, + agency_suggestions=agency_suggestions, + batch_info=await GetAnnotationBatchInfoQueryBuilder( + batch_id=batch_id, + models=[ + UserUrlAgencySuggestion, + ] + ).run(session), + location_suggestions=location_suggestions, + name_suggestions=name_suggestions + ) + ) diff --git a/src/api/endpoints/annotate/all/get/queries/core.py b/src/api/endpoints/annotate/all/get/queries/core.py index d8684f59..e37f2396 100644 --- a/src/api/endpoints/annotate/all/get/queries/core.py +++ b/src/api/endpoints/annotate/all/get/queries/core.py @@ -2,23 +2,9 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import joinedload -from src.api.endpoints.annotate._shared.queries.get_annotation_batch_info import GetAnnotationBatchInfoQueryBuilder -from src.api.endpoints.annotate.all.get.models.agency import AgencyAnnotationResponseOuterInfo -from src.api.endpoints.annotate.all.get.models.location import LocationAnnotationResponseOuterInfo -from src.api.endpoints.annotate.all.get.models.name import NameAnnotationSuggestion -from src.api.endpoints.annotate.all.get.models.record_type import RecordTypeAnnotationSuggestion -from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse, \ - GetNextURLForAllAnnotationInnerResponse -from src.api.endpoints.annotate.all.get.models.url_type import URLTypeAnnotationSuggestion -from src.api.endpoints.annotate.all.get.queries.agency.core import GetAgencySuggestionsQueryBuilder -from src.api.endpoints.annotate.all.get.queries.convert import \ - convert_user_url_type_suggestion_to_url_type_annotation_suggestion, \ - convert_user_record_type_suggestion_to_record_type_annotation_suggestion -from src.api.endpoints.annotate.all.get.queries.location_.core import GetLocationSuggestionsQueryBuilder -from src.api.endpoints.annotate.all.get.queries.name.core import GetNameSuggestionsQueryBuilder +from src.api.endpoints.annotate._shared.extract import extract_and_format_get_annotation_result +from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse from src.collectors.enums import URLStatus -from src.db.dto_converter import DTOConverter -from src.db.dtos.url.mapping import URLMapping from src.db.models.impl.flag.url_suspended.sqlalchemy import FlagURLSuspended from src.db.models.impl.link.batch_url.sqlalchemy import LinkBatchURL from src.db.models.impl.url.core.sqlalchemy import URL @@ -135,43 +121,5 @@ async def run( next_annotation=None ) - html_response_info = DTOConverter.html_content_list_to_html_response_info( - url.html_content - ) - - url_type_suggestions: list[URLTypeAnnotationSuggestion] = \ - convert_user_url_type_suggestion_to_url_type_annotation_suggestion( - url.user_relevant_suggestions - ) - record_type_suggestions: list[RecordTypeAnnotationSuggestion] = \ - convert_user_record_type_suggestion_to_record_type_annotation_suggestion( - url.user_record_type_suggestions - ) - agency_suggestions: AgencyAnnotationResponseOuterInfo = \ - await GetAgencySuggestionsQueryBuilder(url_id=url.id).run(session) - location_suggestions: LocationAnnotationResponseOuterInfo = \ - await GetLocationSuggestionsQueryBuilder(url_id=url.id).run(session) - name_suggestions: list[NameAnnotationSuggestion] = \ - await GetNameSuggestionsQueryBuilder(url_id=url.id).run(session) - + return await extract_and_format_get_annotation_result(session, url=url, batch_id=self.batch_id) - return GetNextURLForAllAnnotationResponse( - next_annotation=GetNextURLForAllAnnotationInnerResponse( - url_info=URLMapping( - url_id=url.id, - url=url.url - ), - html_info=html_response_info, - url_type_suggestions=url_type_suggestions, - record_type_suggestions=record_type_suggestions, - agency_suggestions=agency_suggestions, - batch_info=await GetAnnotationBatchInfoQueryBuilder( - batch_id=self.batch_id, - models=[ - UserUrlAgencySuggestion, - ] - ).run(session), - location_suggestions=location_suggestions, - name_suggestions=name_suggestions - ) - ) \ No newline at end of file diff --git a/src/api/endpoints/annotate/all/post/query.py b/src/api/endpoints/annotate/all/post/query.py index 2cbcb420..4056de8e 100644 --- a/src/api/endpoints/annotate/all/post/query.py +++ b/src/api/endpoints/annotate/all/post/query.py @@ -3,10 +3,6 @@ from src.api.endpoints.annotate.all.post.models.request import AllAnnotationPostInfo from src.api.endpoints.annotate.all.post.requester import AddAllAnnotationsToURLRequester from src.db.models.impl.flag.url_validated.enums import URLType -from src.db.models.impl.url.suggestion.agency.user import UserUrlAgencySuggestion -from src.db.models.impl.url.suggestion.location.user.sqlalchemy import UserLocationSuggestion -from src.db.models.impl.url.suggestion.record_type.user import UserRecordTypeSuggestion -from src.db.models.impl.url.suggestion.relevant.user import UserURLTypeSuggestion from src.db.queries.base.builder import QueryBuilderBase diff --git a/src/api/endpoints/annotate/anonymous/__init__.py b/src/api/endpoints/annotate/anonymous/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/annotate/anonymous/get/__init__.py b/src/api/endpoints/annotate/anonymous/get/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/annotate/anonymous/get/query.py b/src/api/endpoints/annotate/anonymous/get/query.py new file mode 100644 index 00000000..7e5f2e53 --- /dev/null +++ b/src/api/endpoints/annotate/anonymous/get/query.py @@ -0,0 +1,61 @@ +from typing import Any + +from sqlalchemy import Select, func +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload + +from src.api.endpoints.annotate._shared.extract import extract_and_format_get_annotation_result +from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse +from src.collectors.enums import URLStatus +from src.db.helpers.query import not_exists_url +from src.db.models.impl.url.core.sqlalchemy import URL +from src.db.models.impl.url.suggestion.anonymous.url_type.sqlalchemy import AnonymousAnnotationURLType +from src.db.models.views.unvalidated_url import UnvalidatedURL +from src.db.models.views.url_anno_count import URLAnnotationCount +from src.db.models.views.url_annotations_flags import URLAnnotationFlagsView +from src.db.queries.base.builder import QueryBuilderBase + + +class GetNextURLForAnonymousAnnotationQueryBuilder(QueryBuilderBase): + + async def run(self, session: AsyncSession) -> GetNextURLForAllAnnotationResponse: + + query = ( + Select(URL) + # URL Must be unvalidated + .join( + UnvalidatedURL, + UnvalidatedURL.url_id == URL.id + ) + .join( + URLAnnotationFlagsView, + URLAnnotationFlagsView.url_id == URL.id + ) + .join( + URLAnnotationCount, + URLAnnotationCount.url_id == URL.id + ) + .where( + URL.status == URLStatus.OK.value, + not_exists_url(AnonymousAnnotationURLType) + ) + .options( + joinedload(URL.html_content), + joinedload(URL.user_relevant_suggestions), + joinedload(URL.user_record_type_suggestions), + joinedload(URL.name_suggestions), + ) + .order_by( + func.random() + ) + .limit(1) + ) + + raw_results = (await session.execute(query)).unique() + url: URL | None = raw_results.scalars().one_or_none() + if url is None: + return GetNextURLForAllAnnotationResponse( + next_annotation=None + ) + + return await extract_and_format_get_annotation_result(session, url=url) diff --git a/src/api/endpoints/annotate/anonymous/post/__init__.py b/src/api/endpoints/annotate/anonymous/post/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/annotate/anonymous/post/query.py b/src/api/endpoints/annotate/anonymous/post/query.py new file mode 100644 index 00000000..faa7aa1d --- /dev/null +++ b/src/api/endpoints/annotate/anonymous/post/query.py @@ -0,0 +1,56 @@ +from sqlalchemy.ext.asyncio import AsyncSession + +from src.api.endpoints.annotate.all.post.models.request import AllAnnotationPostInfo +from src.db.models.impl.url.suggestion.anonymous.agency.sqlalchemy import AnonymousAnnotationAgency +from src.db.models.impl.url.suggestion.anonymous.location.sqlalchemy import AnonymousAnnotationLocation +from src.db.models.impl.url.suggestion.anonymous.record_type.sqlalchemy import AnonymousAnnotationRecordType +from src.db.models.impl.url.suggestion.anonymous.url_type.sqlalchemy import AnonymousAnnotationURLType +from src.db.queries.base.builder import QueryBuilderBase + + +class AddAnonymousAnnotationsToURLQueryBuilder(QueryBuilderBase): + def __init__( + self, + url_id: int, + post_info: AllAnnotationPostInfo + ): + super().__init__() + self.url_id = url_id + self.post_info = post_info + + async def run(self, session: AsyncSession) -> None: + + url_type_suggestion = AnonymousAnnotationURLType( + url_id=self.url_id, + url_type=self.post_info.suggested_status + ) + session.add(url_type_suggestion) + + if self.post_info.record_type is not None: + record_type_suggestion = AnonymousAnnotationRecordType( + url_id=self.url_id, + record_type=self.post_info.record_type + ) + session.add(record_type_suggestion) + + if len(self.post_info.location_info.location_ids) != 0: + location_suggestions = [ + AnonymousAnnotationLocation( + url_id=self.url_id, + location_id=location_id + ) + for location_id in self.post_info.location_info.location_ids + ] + session.add_all(location_suggestions) + + if len(self.post_info.agency_info.agency_ids) != 0: + agency_suggestions = [ + AnonymousAnnotationAgency( + url_id=self.url_id, + agency_id=agency_id + ) + for agency_id in self.post_info.agency_info.agency_ids + ] + session.add_all(agency_suggestions) + + # Ignore Name suggestions \ No newline at end of file diff --git a/src/api/endpoints/annotate/routes.py b/src/api/endpoints/annotate/routes.py index 6972314d..a09ee1ec 100644 --- a/src/api/endpoints/annotate/routes.py +++ b/src/api/endpoints/annotate/routes.py @@ -5,6 +5,9 @@ from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse from src.api.endpoints.annotate.all.get.queries.agency.core import GetAgencySuggestionsQueryBuilder from src.api.endpoints.annotate.all.post.models.request import AllAnnotationPostInfo +from src.api.endpoints.annotate.all.post.query import AddAllAnnotationsToURLQueryBuilder +from src.api.endpoints.annotate.anonymous.get.query import GetNextURLForAnonymousAnnotationQueryBuilder +from src.api.endpoints.annotate.anonymous.post.query import AddAnonymousAnnotationsToURLQueryBuilder from src.core.core import AsyncCore from src.security.dtos.access_info import AccessInfo from src.security.manager import get_access_info @@ -27,6 +30,33 @@ ) +@annotate_router.get("/anonymous") +async def get_next_url_for_all_annotations_anonymous( + async_core: AsyncCore = Depends(get_async_core), +) -> GetNextURLForAllAnnotationResponse: + return await async_core.adb_client.run_query_builder( + GetNextURLForAnonymousAnnotationQueryBuilder() + ) + +@annotate_router.post("/anonymous/{url_id}") +async def annotate_url_for_all_annotations_and_get_next_url_anonymous( + url_id: int, + all_annotation_post_info: AllAnnotationPostInfo, + async_core: AsyncCore = Depends(get_async_core), +) -> GetNextURLForAllAnnotationResponse: + await async_core.adb_client.run_query_builder( + AddAnonymousAnnotationsToURLQueryBuilder( + url_id=url_id, + post_info=all_annotation_post_info + ) + ) + + return await async_core.adb_client.run_query_builder( + GetNextURLForAnonymousAnnotationQueryBuilder() + ) + + + @annotate_router.get("/all") async def get_next_url_for_all_annotations( access_info: AccessInfo = Depends(get_access_info), @@ -34,7 +64,7 @@ async def get_next_url_for_all_annotations( batch_id: int | None = batch_query, anno_url_id: int | None = url_id_query ) -> GetNextURLForAllAnnotationResponse: - return await async_core.get_next_url_for_all_annotations( + return await async_core.adb_client.get_next_url_for_all_annotations( batch_id=batch_id, user_id=access_info.user_id, url_id=anno_url_id @@ -52,12 +82,15 @@ async def annotate_url_for_all_annotations_and_get_next_url( """ Post URL annotation and get next URL to annotate """ - await async_core.submit_url_for_all_annotations( - user_id=access_info.user_id, - url_id=url_id, - post_info=all_annotation_post_info - ) - return await async_core.get_next_url_for_all_annotations( + await async_core.adb_client.run_query_builder( + AddAllAnnotationsToURLQueryBuilder( + user_id=access_info.user_id, + url_id=url_id, + post_info=all_annotation_post_info + ) + ) + + return await async_core.adb_client.get_next_url_for_all_annotations( batch_id=batch_id, user_id=access_info.user_id, url_id=anno_url_id diff --git a/src/core/core.py b/src/core/core.py index 1bc4fe6f..7d4ac083 100644 --- a/src/core/core.py +++ b/src/core/core.py @@ -154,39 +154,9 @@ async def get_tasks( task_status=task_status ) - async def get_task_info(self, task_id: int) -> TaskInfo: return await self.adb_client.get_task_info(task_id=task_id) - - #region Annotations and Review - - async def get_next_url_for_all_annotations( - self, - user_id: int, - batch_id: int | None, - url_id: int | None - ) -> GetNextURLForAllAnnotationResponse: - return await self.adb_client.get_next_url_for_all_annotations( - batch_id=batch_id, - user_id=user_id, - url_id=url_id - ) - - async def submit_url_for_all_annotations( - self, - user_id: int, - url_id: int, - post_info: AllAnnotationPostInfo - ): - await self.adb_client.run_query_builder( - AddAllAnnotationsToURLQueryBuilder( - user_id=user_id, - url_id=url_id, - post_info=post_info - ) - ) - async def upload_manual_batch( self, dto: ManualBatchInputDTO, diff --git a/src/db/models/impl/url/suggestion/anonymous/__init__.py b/src/db/models/impl/url/suggestion/anonymous/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/db/models/impl/url/suggestion/anonymous/agency/__init__.py b/src/db/models/impl/url/suggestion/anonymous/agency/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/db/models/impl/url/suggestion/anonymous/agency/sqlalchemy.py b/src/db/models/impl/url/suggestion/anonymous/agency/sqlalchemy.py new file mode 100644 index 00000000..afea2f23 --- /dev/null +++ b/src/db/models/impl/url/suggestion/anonymous/agency/sqlalchemy.py @@ -0,0 +1,16 @@ +from sqlalchemy import PrimaryKeyConstraint + +from src.db.models.mixins import URLDependentMixin, AgencyDependentMixin, CreatedAtMixin +from src.db.models.templates_.base import Base + + +class AnonymousAnnotationAgency( + Base, + URLDependentMixin, + AgencyDependentMixin, + CreatedAtMixin +): + __tablename__ = "anonymous_annotation_agency" + __table_args__ = ( + PrimaryKeyConstraint("url_id", "agency_id"), + ) \ No newline at end of file diff --git a/src/db/models/impl/url/suggestion/anonymous/location/__init__.py b/src/db/models/impl/url/suggestion/anonymous/location/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/db/models/impl/url/suggestion/anonymous/location/sqlalchemy.py b/src/db/models/impl/url/suggestion/anonymous/location/sqlalchemy.py new file mode 100644 index 00000000..f02cb7ba --- /dev/null +++ b/src/db/models/impl/url/suggestion/anonymous/location/sqlalchemy.py @@ -0,0 +1,17 @@ +from sqlalchemy import PrimaryKeyConstraint + +from src.db.models.mixins import LocationDependentMixin, URLDependentMixin, CreatedAtMixin +from src.db.models.templates_.base import Base + + +class AnonymousAnnotationLocation( + Base, + URLDependentMixin, + LocationDependentMixin, + CreatedAtMixin +): + + __tablename__ = "anonymous_annotation_location" + __table_args__ = ( + PrimaryKeyConstraint("url_id", "location_id"), + ) \ No newline at end of file diff --git a/src/db/models/impl/url/suggestion/anonymous/record_type/__init__.py b/src/db/models/impl/url/suggestion/anonymous/record_type/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/db/models/impl/url/suggestion/anonymous/record_type/sqlalchemy.py b/src/db/models/impl/url/suggestion/anonymous/record_type/sqlalchemy.py new file mode 100644 index 00000000..25a9ddec --- /dev/null +++ b/src/db/models/impl/url/suggestion/anonymous/record_type/sqlalchemy.py @@ -0,0 +1,23 @@ +from sqlalchemy import PrimaryKeyConstraint +from sqlalchemy.orm import Mapped + +from src.core.enums import RecordType +from src.db.models.helpers import enum_column +from src.db.models.mixins import URLDependentMixin, CreatedAtMixin +from src.db.models.templates_.base import Base + + +class AnonymousAnnotationRecordType( + Base, + URLDependentMixin, + CreatedAtMixin +): + __tablename__ = "anonymous_annotation_record_type" + __table_args__ = ( + PrimaryKeyConstraint("url_id", "record_type"), + ) + + record_type: Mapped[RecordType] = enum_column( + name="record_type", + enum_type=RecordType, + ) \ No newline at end of file diff --git a/src/db/models/impl/url/suggestion/anonymous/url_type/__init__.py b/src/db/models/impl/url/suggestion/anonymous/url_type/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/db/models/impl/url/suggestion/anonymous/url_type/sqlalchemy.py b/src/db/models/impl/url/suggestion/anonymous/url_type/sqlalchemy.py new file mode 100644 index 00000000..f9033ffa --- /dev/null +++ b/src/db/models/impl/url/suggestion/anonymous/url_type/sqlalchemy.py @@ -0,0 +1,23 @@ +from sqlalchemy import PrimaryKeyConstraint +from sqlalchemy.orm import Mapped + +from src.db.models.helpers import enum_column +from src.db.models.impl.flag.url_validated.enums import URLType +from src.db.models.mixins import URLDependentMixin, CreatedAtMixin +from src.db.models.templates_.base import Base + + +class AnonymousAnnotationURLType( + Base, + URLDependentMixin, + CreatedAtMixin +): + __tablename__ = "anonymous_annotation_url_type" + __table_args__ = ( + PrimaryKeyConstraint("url_id", "url_type"), + ) + + url_type: Mapped[URLType] = enum_column( + name="url_type", + enum_type=URLType, + ) \ No newline at end of file diff --git a/src/util/alembic_helpers.py b/src/util/alembic_helpers.py index 85621ca4..cb9d8d67 100644 --- a/src/util/alembic_helpers.py +++ b/src/util/alembic_helpers.py @@ -3,6 +3,7 @@ from alembic import op import sqlalchemy as sa from sqlalchemy import text +from sqlalchemy.dialects.postgresql import ENUM def switch_enum_type( @@ -96,6 +97,17 @@ def created_at_column() -> sa.Column: comment='The time the row was created.' ) +def enum_column( + column_name, + enum_name +) -> sa.Column: + return sa.Column( + column_name, + ENUM(name=enum_name, create_type=False), + nullable=False, + comment=f'The {column_name} of the row.' + ) + def updated_at_column() -> sa.Column: """Returns a standard `updated_at` column.""" return sa.Column( diff --git a/tests/automated/integration/api/annotate/anonymous/__init__.py b/tests/automated/integration/api/annotate/anonymous/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/automated/integration/api/annotate/anonymous/helper.py b/tests/automated/integration/api/annotate/anonymous/helper.py new file mode 100644 index 00000000..ccfe518f --- /dev/null +++ b/tests/automated/integration/api/annotate/anonymous/helper.py @@ -0,0 +1,23 @@ +from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse +from src.api.endpoints.annotate.all.post.models.request import AllAnnotationPostInfo +from tests.automated.integration.api._helpers.RequestValidator import RequestValidator + + +async def get_next_url_for_anonymous_annotation( + request_validator: RequestValidator, +): + data = request_validator.get( + url=f"/annotate/anonymous" + ) + return GetNextURLForAllAnnotationResponse(**data) + +async def post_and_get_next_url_for_anonymous_annotation( + request_validator: RequestValidator, + url_id: int, + all_annotation_post_info: AllAnnotationPostInfo, +): + data = request_validator.post( + url=f"/annotate/anonymous/{url_id}", + json=all_annotation_post_info.model_dump(mode='json') + ) + return GetNextURLForAllAnnotationResponse(**data) \ No newline at end of file diff --git a/tests/automated/integration/api/annotate/anonymous/test_core.py b/tests/automated/integration/api/annotate/anonymous/test_core.py new file mode 100644 index 00000000..4b747363 --- /dev/null +++ b/tests/automated/integration/api/annotate/anonymous/test_core.py @@ -0,0 +1,83 @@ +import pytest + +from src.api.endpoints.annotate.all.get.models.name import NameAnnotationSuggestion +from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse +from src.api.endpoints.annotate.all.post.models.agency import AnnotationPostAgencyInfo +from src.api.endpoints.annotate.all.post.models.location import AnnotationPostLocationInfo +from src.api.endpoints.annotate.all.post.models.name import AnnotationPostNameInfo +from src.api.endpoints.annotate.all.post.models.request import AllAnnotationPostInfo +from src.core.enums import RecordType +from src.db.dtos.url.mapping import URLMapping +from src.db.models.impl.flag.url_validated.enums import URLType +from src.db.models.impl.url.suggestion.anonymous.agency.sqlalchemy import AnonymousAnnotationAgency +from src.db.models.impl.url.suggestion.anonymous.location.sqlalchemy import AnonymousAnnotationLocation +from src.db.models.impl.url.suggestion.anonymous.record_type.sqlalchemy import AnonymousAnnotationRecordType +from src.db.models.impl.url.suggestion.anonymous.url_type.sqlalchemy import AnonymousAnnotationURLType +from src.db.models.mixins import URLDependentMixin +from tests.automated.integration.api.annotate.anonymous.helper import get_next_url_for_anonymous_annotation, \ + post_and_get_next_url_for_anonymous_annotation +from tests.helpers.data_creator.models.creation_info.us_state import USStateCreationInfo +from tests.helpers.setup.final_review.core import setup_for_get_next_url_for_final_review +from tests.helpers.setup.final_review.model import FinalReviewSetupInfo + + +@pytest.mark.asyncio +async def test_annotate_anonymous( + api_test_helper, + pennsylvania: USStateCreationInfo, +): + ath = api_test_helper + ddc = ath.db_data_creator + rv = ath.request_validator + + # Set up URLs + setup_info_1 = await setup_for_get_next_url_for_final_review( + db_data_creator=ath.db_data_creator, include_user_annotations=True + ) + url_mapping_1: URLMapping = setup_info_1.url_mapping + setup_info_2: FinalReviewSetupInfo = await setup_for_get_next_url_for_final_review( + db_data_creator=ath.db_data_creator, include_user_annotations=True + ) + url_mapping_2: URLMapping = setup_info_2.url_mapping + + get_response_1: GetNextURLForAllAnnotationResponse = await get_next_url_for_anonymous_annotation(rv) + assert get_response_1.next_annotation is not None + assert len(get_response_1.next_annotation.name_suggestions) == 1 + name_suggestion: NameAnnotationSuggestion = get_response_1.next_annotation.name_suggestions[0] + assert name_suggestion.name is not None + assert name_suggestion.endorsement_count == 0 + + agency_id: int = await ddc.agency() + + post_response_1: GetNextURLForAllAnnotationResponse = await post_and_get_next_url_for_anonymous_annotation( + rv, + get_response_1.next_annotation.url_info.url_id, + AllAnnotationPostInfo( + suggested_status=URLType.DATA_SOURCE, + record_type=RecordType.ACCIDENT_REPORTS, + agency_info=AnnotationPostAgencyInfo(agency_ids=[agency_id]), + location_info=AnnotationPostLocationInfo( + location_ids=[ + pennsylvania.location_id, + ] + ), + name_info=AnnotationPostNameInfo( + new_name="New Name" + ) + ) + ) + + assert post_response_1.next_annotation is not None + assert post_response_1.next_annotation.url_info.url_id != get_response_1.next_annotation.url_info.url_id + + for model in [ + AnonymousAnnotationAgency, + AnonymousAnnotationLocation, + AnonymousAnnotationRecordType, + AnonymousAnnotationURLType + ]: + instances: list[URLDependentMixin] = await ddc.adb_client.get_all(model) + assert len(instances) == 1 + instance: model = instances[0] + assert instance.url_id == get_response_1.next_annotation.url_info.url_id +