diff --git a/alembic/versions/2025_12_26_1527-42933d84aa52_revise_annotation_count_view.py b/alembic/versions/2025_12_26_1527-42933d84aa52_revise_annotation_count_view.py new file mode 100644 index 00000000..241c7845 --- /dev/null +++ b/alembic/versions/2025_12_26_1527-42933d84aa52_revise_annotation_count_view.py @@ -0,0 +1,210 @@ +"""Revise annotation count view + +Revision ID: 42933d84aa52 +Revises: e88e4e962dc7 +Create Date: 2025-12-26 15:27:30.368862 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '42933d84aa52' +down_revision: Union[str, None] = 'e88e4e962dc7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("""DROP VIEW IF EXISTS url_annotation_count_view""") + op.execute( + """ + CREATE VIEW url_annotation_count_view AS + WITH + auto_location_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__location__auto__subtasks anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , auto_agency_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__agency__auto__subtasks anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , auto_url_type_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__url_type__auto anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , auto_record_type_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__record_type__auto anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , user_location_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__location__user anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , user_agency_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__agency__user anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , user_url_type_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__url_type__user anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , user_record_type_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__record_type__user anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , anon_location_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__location__anon anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , anon_agency_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__agency__anon anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , anon_url_type_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__url_type__anon anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + , anon_record_type_count AS ( + SELECT + u_1.id, + count(anno.url_id) AS cnt + FROM + urls u_1 + JOIN annotation__record_type__anon anno + ON u_1.id = anno.url_id + GROUP BY + u_1.id + ) + SELECT + u.id AS url_id, + COALESCE(auto_ag.cnt, 0::bigint) AS auto_agency_count, + COALESCE(auto_loc.cnt, 0::bigint) AS auto_location_count, + COALESCE(auto_rec.cnt, 0::bigint) AS auto_record_type_count, + COALESCE(auto_typ.cnt, 0::bigint) AS auto_url_type_count, + COALESCE(user_ag.cnt, 0::bigint) AS user_agency_count, + COALESCE(user_loc.cnt, 0::bigint) AS user_location_count, + COALESCE(user_rec.cnt, 0::bigint) AS user_record_type_count, + COALESCE(user_typ.cnt, 0::bigint) AS user_url_type_count, + COALESCE(anon_ag.cnt, 0::bigint) AS anon_agency_count, + COALESCE(anon_loc.cnt, 0::bigint) AS anon_location_count, + COALESCE(anon_rec.cnt, 0::bigint) AS anon_record_type_count, + COALESCE(anon_typ.cnt, 0::bigint) AS anon_url_type_count, + COALESCE(auto_ag.cnt, 0::bigint) + COALESCE(auto_loc.cnt, 0::bigint) + COALESCE(auto_rec.cnt, 0::bigint) + + COALESCE(auto_typ.cnt, 0::bigint) + COALESCE(user_ag.cnt, 0::bigint) + COALESCE(user_loc.cnt, 0::bigint) + + COALESCE(user_rec.cnt, 0::bigint) + COALESCE(user_typ.cnt, 0::bigint) + COALESCE(anon_ag.cnt, 0::bigint) + + COALESCE(anon_loc.cnt, 0::bigint) + COALESCE(anon_rec.cnt, 0::bigint) + COALESCE(anon_typ.cnt, 0::bigint) AS total_anno_count + + FROM + urls u + LEFT JOIN auto_agency_count auto_ag + ON auto_ag.id = u.id + LEFT JOIN auto_location_count auto_loc + ON auto_loc.id = u.id + LEFT JOIN auto_record_type_count auto_rec + ON auto_rec.id = u.id + LEFT JOIN auto_url_type_count auto_typ + ON auto_typ.id = u.id + LEFT JOIN user_agency_count user_ag + ON user_ag.id = u.id + LEFT JOIN user_location_count user_loc + ON user_loc.id = u.id + LEFT JOIN user_record_type_count user_rec + ON user_rec.id = u.id + LEFT JOIN user_url_type_count user_typ + ON user_typ.id = u.id + LEFT JOIN anon_agency_count anon_ag + ON user_ag.id = u.id + LEFT JOIN anon_location_count anon_loc + ON user_loc.id = u.id + LEFT JOIN anon_record_type_count anon_rec + ON user_rec.id = u.id + LEFT JOIN anon_url_type_count anon_typ + ON user_typ.id = u.id + + """ + ) + + +def downgrade() -> None: + pass diff --git a/src/api/endpoints/annotate/_shared/queries/helper.py b/src/api/endpoints/annotate/_shared/queries/helper.py index f8bdf033..76def5c1 100644 --- a/src/api/endpoints/annotate/_shared/queries/helper.py +++ b/src/api/endpoints/annotate/_shared/queries/helper.py @@ -2,7 +2,7 @@ This module contains helper functions for the annotate GET queries """ -from sqlalchemy import Select, case, exists, select +from sqlalchemy import Select, case, CTE, ColumnElement from sqlalchemy.orm import joinedload from src.collectors.enums import URLStatus @@ -15,10 +15,9 @@ from src.db.models.views.url_annotations_flags import URLAnnotationFlagsView -def get_select() -> Select: - return ( - Select(URL) - +def add_joins(query: Select) -> Select: + query = ( + query .join( URLAnnotationFlagsView, URLAnnotationFlagsView.url_id == URL.id @@ -28,10 +27,12 @@ def get_select() -> Select: URLAnnotationCount.url_id == URL.id ) ) + return query -def conclude(query: Select) -> Select: - # Add common where conditions - query = query.where( +def add_common_where_conditions( + query: Select, +) -> Select: + return query.where( URL.status == URLStatus.OK.value, not_exists_url( FlagURLSuspended @@ -42,29 +43,41 @@ def conclude(query: Select) -> Select: ) ) - - query = ( - # Add load options - query.options( - joinedload(URL.html_content), - joinedload(URL.user_url_type_suggestions), - joinedload(URL.user_record_type_suggestions), - joinedload(URL.anon_record_type_suggestions), - joinedload(URL.anon_url_type_suggestions), - ) - # Sorting Priority - .order_by( - # Privilege manually submitted URLs first - case( - (URL.source == URLSource.MANUAL, 0), - else_=1 - ).asc(), - # Break ties by favoring URL with higher total annotations - URLAnnotationCount.total_anno_count.desc(), - # Break additional ties by favoring least recently created URLs - URL.id.asc() - ) - # Limit to 1 result - .limit(1) +def add_load_options( + query: Select +) -> Select: + return query.options( + joinedload(URL.html_content), + joinedload(URL.user_url_type_suggestions), + joinedload(URL.user_record_type_suggestions), + joinedload(URL.anon_record_type_suggestions), + joinedload(URL.anon_url_type_suggestions), ) - return query \ No newline at end of file + +def bool_sort( + condition: ColumnElement[bool] +) -> ColumnElement[int]: + return case( + (condition, 0), + else_=1 + ).asc() + +def common_sorts( + base_cte: CTE +) -> list[ColumnElement[int]]: + return [ + # Privilege URLs whose batches are associated with locations + # followed by ANY user + bool_sort(base_cte.c.followed_by_any_user), + # Privilege Manually Submitted URLs + bool_sort(URL.source == URLSource.MANUAL), + # Privilege based on total number of user annotations + URLAnnotationCount.user_url_type_count.desc(), + # Privilege based on total number of anon annotations + URLAnnotationCount.anon_url_type_count.desc(), + # Privilege based on total number of auto annotations + URLAnnotationCount.auto_url_type_count.desc(), + # Break additional ties by favoring least recently created URLs + URL.id.asc() + ] + diff --git a/src/api/endpoints/annotate/all/get/queries/core.py b/src/api/endpoints/annotate/all/get/queries/core.py index 852886c6..a382f0b4 100644 --- a/src/api/endpoints/annotate/all/get/queries/core.py +++ b/src/api/endpoints/annotate/all/get/queries/core.py @@ -1,9 +1,12 @@ -from sqlalchemy import exists, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src.api.endpoints.annotate._shared.extract import extract_and_format_get_annotation_result from src.api.endpoints.annotate._shared.queries import helper from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse +from src.api.endpoints.annotate.all.get.queries.features.followed_by_any_user import get_followed_by_any_user_feature +from src.api.endpoints.annotate.all.get.queries.features.followed_by_user import get_followed_by_user_feature +from src.api.endpoints.annotate.all.get.queries.helpers import not_exists_user_annotation from src.db.models.impl.annotation.agency.user.sqlalchemy import AnnotationAgencyUser from src.db.models.impl.annotation.location.user.sqlalchemy import AnnotationLocationUser from src.db.models.impl.annotation.record_type.user.user import AnnotationRecordTypeUser @@ -30,55 +33,63 @@ async def run( self, session: AsyncSession ) -> GetNextURLForAllAnnotationResponse: - query = helper.get_select() + base_cte = select( + URL.id, + get_followed_by_user_feature(self.user_id), + get_followed_by_any_user_feature() + ).cte("base") + + query = select( + URL, + base_cte.c.followed_by_user, + base_cte.c.followed_by_any_user, + ).join( + base_cte, + base_cte.c.id == URL.id + ) + query = helper.add_joins(query) # Add user annotation-specific joins and conditions if self.batch_id is not None: query = query.join(LinkBatchURL).where(LinkBatchURL.batch_id == self.batch_id) if self.url_id is not None: query = query.where(URL.id == self.url_id) + + user_models = [ + AnnotationURLTypeUser, + AnnotationAgencyUser, + AnnotationLocationUser, + AnnotationRecordTypeUser, + ] + query = ( query .where( # Must not have been previously annotated by user - ~exists( - select(AnnotationURLTypeUser.url_id) - .where( - AnnotationURLTypeUser.url_id == URL.id, - AnnotationURLTypeUser.user_id == self.user_id, - ) - ), - ~exists( - select(AnnotationAgencyUser.url_id) - .where( - AnnotationAgencyUser.url_id == URL.id, - AnnotationAgencyUser.user_id == self.user_id, - ) - ), - ~exists( - select( - AnnotationLocationUser.url_id - ) - .where( - AnnotationLocationUser.url_id == URL.id, - AnnotationLocationUser.user_id == self.user_id, - ) - ), - ~exists( - select( - AnnotationRecordTypeUser.url_id - ) - .where( - AnnotationRecordTypeUser.url_id == URL.id, - AnnotationRecordTypeUser.user_id == self.user_id, - ) + *[ + not_exists_user_annotation( + user_id=self.user_id, + user_model=user_model ) + for user_model in user_models + ] ) ) # Conclude query with limit and sorting - query = helper.conclude(query) + query = helper.add_common_where_conditions(query) + query = helper.add_load_options(query) + query = ( + # Sorting Priority + query.order_by( + # If the specific user follows *this* location, privilege it + helper.bool_sort(base_cte.c.followed_by_user), + *helper.common_sorts(base_cte) + ) + # Limit to 1 result + .limit(1) + ) raw_results = (await session.execute(query)).unique() url: URL | None = raw_results.scalars().one_or_none() diff --git a/src/api/endpoints/annotate/all/get/queries/features/README.md b/src/api/endpoints/annotate/all/get/queries/features/README.md new file mode 100644 index 00000000..e37fe6e5 --- /dev/null +++ b/src/api/endpoints/annotate/all/get/queries/features/README.md @@ -0,0 +1 @@ +"Features" in this case refers to EXISTs subqueries which are separately calculated and used for sorting. \ No newline at end of file diff --git a/src/api/endpoints/annotate/all/get/queries/features/__init__.py b/src/api/endpoints/annotate/all/get/queries/features/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/api/endpoints/annotate/all/get/queries/features/followed_by_any_user.py b/src/api/endpoints/annotate/all/get/queries/features/followed_by_any_user.py new file mode 100644 index 00000000..e14ddddd --- /dev/null +++ b/src/api/endpoints/annotate/all/get/queries/features/followed_by_any_user.py @@ -0,0 +1,27 @@ +from sqlalchemy import exists, select, literal, Exists + +from src.db.models.impl.link.batch_url.sqlalchemy import LinkBatchURL +from src.db.models.impl.link.location__user_follow import LinkLocationUserFollow +from src.db.models.impl.link.location_batch.sqlalchemy import LinkLocationBatch +from src.db.models.impl.url.core.sqlalchemy import URL + + +def get_followed_by_any_user_feature() -> Exists: + query = ( + exists( + select(literal(1)) + .select_from(LinkBatchURL) + .join( + LinkLocationBatch, + LinkLocationBatch.batch_id == LinkBatchURL.batch_id + ) + .join( + LinkLocationUserFollow, + LinkLocationUserFollow.location_id == LinkLocationBatch.location_id + ) + .where( + URL.id == LinkBatchURL.url_id, + ) + ).label("followed_by_any_user") + ) + return query \ No newline at end of file diff --git a/src/api/endpoints/annotate/all/get/queries/features/followed_by_user.py b/src/api/endpoints/annotate/all/get/queries/features/followed_by_user.py new file mode 100644 index 00000000..b73d4cd4 --- /dev/null +++ b/src/api/endpoints/annotate/all/get/queries/features/followed_by_user.py @@ -0,0 +1,30 @@ +from sqlalchemy import exists, select, literal, Exists + +from src.db.models.impl.link.batch_url.sqlalchemy import LinkBatchURL +from src.db.models.impl.link.location__user_follow import LinkLocationUserFollow +from src.db.models.impl.link.location_batch.sqlalchemy import LinkLocationBatch +from src.db.models.impl.url.core.sqlalchemy import URL + + +def get_followed_by_user_feature( + user_id: int +) -> Exists: + query = ( + exists( + select(literal(1)) + .select_from(LinkBatchURL) + .join( + LinkLocationBatch, + LinkLocationBatch.batch_id == LinkBatchURL.batch_id + ) + .join( + LinkLocationUserFollow, + LinkLocationUserFollow.location_id == LinkLocationBatch.location_id + ) + .where( + URL.id == LinkBatchURL.url_id, + LinkLocationUserFollow.user_id == user_id + ) + ).label("followed_by_user") + ) + return query \ No newline at end of file diff --git a/src/api/endpoints/annotate/all/get/queries/helpers.py b/src/api/endpoints/annotate/all/get/queries/helpers.py new file mode 100644 index 00000000..da112099 --- /dev/null +++ b/src/api/endpoints/annotate/all/get/queries/helpers.py @@ -0,0 +1,26 @@ +from typing import Protocol, TypeVar + +from sqlalchemy import ColumnElement, select, exists + +from src.db.models.impl.url.core.sqlalchemy import URL + + +class UserURLModelProtocol( + Protocol, +): + user_id: ColumnElement[int] + url_id: ColumnElement[int] + +UserModel = TypeVar("UserModel", bound=UserURLModelProtocol) + +def not_exists_user_annotation( + user_id: int, + user_model: UserModel +) -> ColumnElement[bool]: + return ~exists( + select(user_model.url_id) + .where( + user_model.url_id == URL.id, + user_model.user_id == user_id, + ) + ) \ No newline at end of file diff --git a/src/api/endpoints/annotate/anonymous/get/helpers.py b/src/api/endpoints/annotate/anonymous/get/helpers.py index 83a10845..96a15680 100644 --- a/src/api/endpoints/annotate/anonymous/get/helpers.py +++ b/src/api/endpoints/annotate/anonymous/get/helpers.py @@ -1,12 +1,9 @@ from typing import Protocol, TypeVar from uuid import UUID -from marshmallow.fields import Bool -from sqlalchemy import Exists, select, exists, ColumnElement, Boolean +from sqlalchemy import select, exists, ColumnElement from src.db.models.impl.url.core.sqlalchemy import URL -from src.db.models.mixins import AnonymousSessionMixin, URLDependentMixin -from src.db.models.templates_.base import Base class AnonymousURLModelProtocol( @@ -17,7 +14,10 @@ class AnonymousURLModelProtocol( AnonModel = TypeVar("AnonModel", bound=AnonymousURLModelProtocol) -def not_exists_anon_annotation(session_id: UUID, anon_model: AnonModel) -> ColumnElement[bool]: +def not_exists_anon_annotation( + session_id: UUID, + anon_model: AnonModel +) -> ColumnElement[bool]: return ~exists( select(anon_model.url_id) .where( diff --git a/src/api/endpoints/annotate/anonymous/get/query.py b/src/api/endpoints/annotate/anonymous/get/query.py index 684df2f5..c53726e1 100644 --- a/src/api/endpoints/annotate/anonymous/get/query.py +++ b/src/api/endpoints/annotate/anonymous/get/query.py @@ -1,10 +1,14 @@ from uuid import UUID +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from src.api.endpoints.annotate._shared.extract import extract_and_format_get_annotation_result from src.api.endpoints.annotate._shared.queries import helper +from src.api.endpoints.annotate._shared.queries.helper import add_common_where_conditions, add_load_options, \ + common_sorts from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse +from src.api.endpoints.annotate.all.get.queries.features.followed_by_any_user import get_followed_by_any_user_feature from src.api.endpoints.annotate.anonymous.get.helpers import not_exists_anon_annotation from src.api.endpoints.annotate.anonymous.get.response import GetNextURLForAnonymousAnnotationResponse from src.db.models.impl.annotation.agency.anon.sqlalchemy import AnnotationAgencyAnon @@ -25,32 +29,51 @@ def __init__( self.session_id = session_id async def run(self, session: AsyncSession) -> GetNextURLForAnonymousAnnotationResponse: - query = helper.get_select() + base_cte = select( + URL.id, + get_followed_by_any_user_feature() + ).cte("base") + + query = select( + URL, + base_cte.c.followed_by_any_user, + ).join( + base_cte, + base_cte.c.id == URL.id + ) + query = helper.add_joins(query) + + anon_models = [ + AnnotationURLTypeAnon, + AnnotationRecordTypeAnon, + AnnotationLocationAnon, + AnnotationAgencyAnon + ] # Add anonymous annotation-specific conditions. query = ( query .where( # Must not have been previously annotated by user - not_exists_anon_annotation( - session_id=self.session_id, - anon_model=AnnotationURLTypeAnon - ), - not_exists_anon_annotation( - session_id=self.session_id, - anon_model=AnnotationRecordTypeAnon - ), - not_exists_anon_annotation( - session_id=self.session_id, - anon_model=AnnotationLocationAnon - ), - not_exists_anon_annotation( - session_id=self.session_id, - anon_model=AnnotationAgencyAnon - ) + *[ + not_exists_anon_annotation( + session_id=self.session_id, + anon_model=anon_model + ) + for anon_model in anon_models + ] + ) + ) + query = add_common_where_conditions(query) + query = add_load_options(query) + query = ( + # Sorting Priority + query.order_by( + *common_sorts(base_cte) ) + # Limit to 1 result + .limit(1) ) - query = helper.conclude(query) raw_results = (await session.execute(query)).unique() url: URL | None = raw_results.scalars().one_or_none() diff --git a/src/db/models/impl/__init__.py b/src/db/models/impl/__init__.py index e69de29b..9e679b72 100644 --- a/src/db/models/impl/__init__.py +++ b/src/db/models/impl/__init__.py @@ -0,0 +1,3 @@ + +from .link.location_batch.sqlalchemy import LinkLocationBatch +from .link.batch_url.sqlalchemy import LinkBatchURL \ No newline at end of file diff --git a/src/db/models/views/url_anno_count.py b/src/db/models/views/url_anno_count.py index f3909b39..139b0bac 100644 --- a/src/db/models/views/url_anno_count.py +++ b/src/db/models/views/url_anno_count.py @@ -117,4 +117,8 @@ class URLAnnotationCount( user_location_count = Column(Integer, nullable=False) user_record_type_count = Column(Integer, nullable=False) user_url_type_count = Column(Integer, nullable=False) - total_anno_count = Column(Integer, nullable=False) \ No newline at end of file + anon_agency_count = Column(Integer, nullable=False) + anon_location_count = Column(Integer, nullable=False) + anon_record_type_count = Column(Integer, nullable=False) + anon_url_type_count = Column(Integer, nullable=False) + total_anno_count = Column(Integer, nullable=False) diff --git a/tests/automated/integration/api/annotate/all/test_sorting.py b/tests/automated/integration/api/annotate/all/test_sorting.py index a1c59813..1a81dc89 100644 --- a/tests/automated/integration/api/annotate/all/test_sorting.py +++ b/tests/automated/integration/api/annotate/all/test_sorting.py @@ -1,7 +1,14 @@ import pytest +from src.db.client.async_ import AsyncDatabaseClient +from src.db.models.impl.link.batch_url.sqlalchemy import LinkBatchURL +from src.db.models.impl.link.location__user_follow import LinkLocationUserFollow +from src.db.models.impl.link.location_batch.sqlalchemy import LinkLocationBatch from src.db.models.impl.url.core.enums import URLSource +from tests.automated.integration.conftest import MOCK_USER_ID from tests.helpers.api_test_helper import APITestHelper +from tests.helpers.data_creator.models.creation_info.county import CountyCreationInfo +from tests.helpers.data_creator.models.creation_info.locality import LocalityCreationInfo 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 @@ -9,7 +16,9 @@ @pytest.mark.asyncio async def test_annotate_sorting( api_test_helper: APITestHelper, - + test_batch_id: int, + pittsburgh_locality: LocalityCreationInfo, + allegheny_county: CountyCreationInfo, ): """ Test that annotations are prioritized in the following order: @@ -18,6 +27,7 @@ async def test_annotate_sorting( - Then prioritize by URL ID ascending (e.g. least recently created) """ ath = api_test_helper + dbc: AsyncDatabaseClient = ath.adb_client() # First URL created should be prioritized in absence of any other factors setup_info_first_annotation: FinalReviewSetupInfo = await setup_for_get_next_url_for_final_review( @@ -46,3 +56,58 @@ async def test_annotate_sorting( get_response_3 = await ath.request_validator.get_next_url_for_all_annotations() assert get_response_3.next_annotation is not None assert get_response_3.next_annotation.url_info.url_id == setup_info_manual_submission.url_mapping.url_id + + # URL with followed_by_any_user should take precedence over manual submissions + + ## Start by adding a new URL + setup_info_followed_by_any_user: FinalReviewSetupInfo = await setup_for_get_next_url_for_final_review( + db_data_creator=ath.db_data_creator, + include_user_annotations=False + ) + ## Add a link between that URL's batch and a location + link_batch_location = LinkLocationBatch( + batch_id=setup_info_followed_by_any_user.batch_id, + location_id=pittsburgh_locality.location_id + ) + await dbc.add(link_batch_location) + ## Add a link between that location and a user + link_location_user_follow = LinkLocationUserFollow( + location_id=pittsburgh_locality.location_id, + user_id=MOCK_USER_ID + 1 # To ensure it's not the same user we'll be using later on. + ) + await dbc.add(link_location_user_follow) + + # Run get_next_url_for_all_annotations + get_response_4 = await ath.request_validator.get_next_url_for_all_annotations() + # Assert that the URL with followed_by_any_user is returned + assert get_response_4.next_annotation is not None + assert get_response_4.next_annotation.url_info.url_id == setup_info_followed_by_any_user.url_mapping.url_id + + # URL whose associated location is followed by this specific user + # should take precedence over URL whose associated location + # is followed by any user + + ## Start by adding a new URL + setup_info_followed_by_annotating_user: FinalReviewSetupInfo = await setup_for_get_next_url_for_final_review( + db_data_creator=ath.db_data_creator, + include_user_annotations=False + ) + + ## Add a link between that URL's batch and a location + link_batch_location = LinkLocationBatch( + batch_id=setup_info_followed_by_annotating_user.batch_id, + location_id=allegheny_county.location_id + ) + await dbc.add(link_batch_location) + ## Add a link between that location and the mock user + link_location_user_follow = LinkLocationUserFollow( + location_id=allegheny_county.location_id, + user_id=MOCK_USER_ID + ) + await dbc.add(link_location_user_follow) + + get_response_5 = await ath.request_validator.get_next_url_for_all_annotations() + # Assert that the URL with followed_by_any_user is returned + assert get_response_5.next_annotation is not None + assert get_response_5.next_annotation.url_info.url_id == setup_info_followed_by_annotating_user.url_mapping.url_id +