Skip to content

Commit 7ab6638

Browse files
committed
Merge branch 'dev' into mc_551_suggestion_nomenclature
# Conflicts: # src/api/endpoints/annotate/all/get/queries/core.py
2 parents b3600e9 + 6b569bc commit 7ab6638

File tree

10 files changed

+131
-62
lines changed

10 files changed

+131
-62
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
This module contains helper functions for the annotate GET queries
3+
"""
4+
5+
from sqlalchemy import Select, case
6+
from sqlalchemy.orm import joinedload
7+
8+
from src.db.models.impl.url.core.enums import URLSource
9+
from src.db.models.impl.url.core.sqlalchemy import URL
10+
from src.db.models.views.unvalidated_url import UnvalidatedURL
11+
from src.db.models.views.url_anno_count import URLAnnotationCount
12+
from src.db.models.views.url_annotations_flags import URLAnnotationFlagsView
13+
14+
15+
def get_select() -> Select:
16+
return (
17+
Select(URL)
18+
# URL Must be unvalidated
19+
.join(
20+
UnvalidatedURL,
21+
UnvalidatedURL.url_id == URL.id
22+
)
23+
.join(
24+
URLAnnotationFlagsView,
25+
URLAnnotationFlagsView.url_id == URL.id
26+
)
27+
.join(
28+
URLAnnotationCount,
29+
URLAnnotationCount.url_id == URL.id
30+
)
31+
)
32+
33+
def conclude(query: Select) -> Select:
34+
query = (
35+
# Add load options
36+
query.options(
37+
joinedload(URL.html_content),
38+
joinedload(URL.user_relevant_suggestions),
39+
joinedload(URL.user_record_type_suggestions),
40+
joinedload(URL.name_suggestions),
41+
)
42+
# Sorting Priority
43+
.order_by(
44+
# Privilege manually submitted URLs first
45+
case(
46+
(URL.source == URLSource.MANUAL, 0),
47+
else_=1
48+
).asc(),
49+
# Break ties by favoring URL with higher total annotations
50+
URLAnnotationCount.total_anno_count.desc(),
51+
# Break additional ties by favoring least recently created URLs
52+
URL.id.asc()
53+
)
54+
# Limit to 1 result
55+
.limit(1)
56+
)
57+
return query

src/api/endpoints/annotate/all/get/queries/core.py

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from sqlalchemy import Select, exists, select
1+
from sqlalchemy import exists, select
22
from sqlalchemy.ext.asyncio import AsyncSession
3-
from sqlalchemy.orm import joinedload
43

54
from src.api.endpoints.annotate._shared.extract import extract_and_format_get_annotation_result
5+
from src.api.endpoints.annotate._shared.queries import helper
66
from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse
77
from src.collectors.enums import URLStatus
88
from src.db.models.impl.annotation.agency.user.sqlalchemy import AnnotationAgencyUser
@@ -35,22 +35,9 @@ async def run(
3535
self,
3636
session: AsyncSession
3737
) -> GetNextURLForAllAnnotationResponse:
38-
query = (
39-
Select(URL)
40-
# URL Must be unvalidated
41-
.join(
42-
UnvalidatedURL,
43-
UnvalidatedURL.url_id == URL.id
44-
)
45-
.join(
46-
URLAnnotationFlagsView,
47-
URLAnnotationFlagsView.url_id == URL.id
48-
)
49-
.join(
50-
URLAnnotationCount,
51-
URLAnnotationCount.url_id == URL.id
52-
)
53-
)
38+
query = helper.get_select()
39+
40+
# Add user annotation-specific joins and conditions
5441
if self.batch_id is not None:
5542
query = query.join(LinkBatchURL).where(LinkBatchURL.batch_id == self.batch_id)
5643
if self.url_id is not None:
@@ -102,18 +89,11 @@ async def run(
10289
)
10390
)
10491
)
105-
# Add load options
106-
query = query.options(
107-
joinedload(URL.html_content),
108-
joinedload(URL.user_relevant_suggestions),
109-
joinedload(URL.user_record_type_suggestions),
110-
joinedload(URL.name_suggestions),
111-
)
11292

113-
query = query.order_by(
114-
URLAnnotationCount.total_anno_count.desc(),
115-
URL.id.asc()
116-
).limit(1)
93+
94+
# Conclude query with limit and sorting
95+
query = helper.conclude(query)
96+
11797
raw_results = (await session.execute(query)).unique()
11898
url: URL | None = raw_results.scalars().one_or_none()
11999
if url is None:

src/api/endpoints/annotate/anonymous/get/query.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from src.db.models.views.url_anno_count import URLAnnotationCount
2222
from src.db.models.views.url_annotations_flags import URLAnnotationFlagsView
2323
from src.db.queries.base.builder import QueryBuilderBase
24+
from src.api.endpoints.annotate._shared.queries import helper
2425

2526

2627
class GetNextURLForAnonymousAnnotationQueryBuilder(QueryBuilderBase):
@@ -33,22 +34,11 @@ def __init__(
3334
self.session_id = session_id
3435

3536
async def run(self, session: AsyncSession) -> GetNextURLForAnonymousAnnotationResponse:
37+
query = helper.get_select()
3638

39+
# Add anonymous annotation-specific conditions.
3740
query = (
38-
Select(URL)
39-
# URL Must be unvalidated
40-
.join(
41-
UnvalidatedURL,
42-
UnvalidatedURL.url_id == URL.id
43-
)
44-
.join(
45-
URLAnnotationFlagsView,
46-
URLAnnotationFlagsView.url_id == URL.id
47-
)
48-
.join(
49-
URLAnnotationCount,
50-
URLAnnotationCount.url_id == URL.id
51-
)
41+
query
5242
.where(
5343
URL.status == URLStatus.OK.value,
5444
# Must not have been previously annotated by user
@@ -77,18 +67,8 @@ async def run(self, session: AsyncSession) -> GetNextURLForAnonymousAnnotationRe
7767
)
7868
)
7969
)
80-
.options(
81-
joinedload(URL.html_content),
82-
joinedload(URL.user_relevant_suggestions),
83-
joinedload(URL.user_record_type_suggestions),
84-
joinedload(URL.name_suggestions),
85-
)
86-
.order_by(
87-
URLAnnotationCount.total_anno_count.desc(),
88-
URL.id.asc()
89-
)
90-
.limit(1)
9170
)
71+
query = helper.conclude(query)
9272

9373
raw_results = (await session.execute(query)).unique()
9474
url: URL | None = raw_results.scalars().one_or_none()

src/api/endpoints/contributions/routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from src.api.endpoints.contributions.user.response import ContributionsUserResponse
88
from src.core.core import AsyncCore
99
from src.security.dtos.access_info import AccessInfo
10-
from src.security.manager import get_access_info, get_standard_user_access_info
10+
from src.security.manager import get_standard_user_access_info
1111

1212
contributions_router = APIRouter(
1313
prefix="/contributions",
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import pytest
2+
3+
from src.db.models.impl.url.core.enums import URLSource
4+
from tests.helpers.api_test_helper import APITestHelper
5+
from tests.helpers.setup.final_review.core import setup_for_get_next_url_for_final_review
6+
from tests.helpers.setup.final_review.model import FinalReviewSetupInfo
7+
8+
9+
@pytest.mark.asyncio
10+
async def test_annotate_sorting(
11+
api_test_helper: APITestHelper,
12+
13+
):
14+
"""
15+
Test that annotations are prioritized in the following order:
16+
- Any manual submissions are prioritized first
17+
- Then prioritize by number of annotations descending
18+
- Then prioritize by URL ID ascending (e.g. least recently created)
19+
"""
20+
ath = api_test_helper
21+
22+
# First URL created should be prioritized in absence of any other factors
23+
setup_info_first_annotation: FinalReviewSetupInfo = await setup_for_get_next_url_for_final_review(
24+
db_data_creator=ath.db_data_creator,
25+
include_user_annotations=False
26+
)
27+
get_response_1 = await ath.request_validator.get_next_url_for_all_annotations()
28+
assert get_response_1.next_annotation is not None
29+
assert get_response_1.next_annotation.url_info.url_id == setup_info_first_annotation.url_mapping.url_id
30+
31+
# ...But higher annotation count should take precedence over least recently created
32+
setup_info_high_annotations: FinalReviewSetupInfo = await setup_for_get_next_url_for_final_review(
33+
db_data_creator=ath.db_data_creator,
34+
include_user_annotations=True
35+
)
36+
get_response_2 = await ath.request_validator.get_next_url_for_all_annotations()
37+
assert get_response_2.next_annotation is not None
38+
assert get_response_2.next_annotation.url_info.url_id == setup_info_high_annotations.url_mapping.url_id
39+
40+
# ...But manual submissions should take precedence over higher annotation count
41+
setup_info_manual_submission: FinalReviewSetupInfo = await setup_for_get_next_url_for_final_review(
42+
db_data_creator=ath.db_data_creator,
43+
source=URLSource.MANUAL,
44+
include_user_annotations=True
45+
)
46+
get_response_3 = await ath.request_validator.get_next_url_for_all_annotations()
47+
assert get_response_3.next_annotation is not None
48+
assert get_response_3.next_annotation.url_info.url_id == setup_info_manual_submission.url_mapping.url_id

tests/automated/integration/db/client/approve_url/test_basic.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
async def test_approve_url_basic(db_data_creator: DBDataCreator):
1818
setup_info = await setup_for_get_next_url_for_final_review(
1919
db_data_creator=db_data_creator,
20-
annotation_count=3,
2120
include_user_annotations=True
2221
)
2322
url_mapping = setup_info.url_mapping

tests/automated/integration/db/client/approve_url/test_error.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
async def test_approval_url_error(db_data_creator: DBDataCreator):
1212
setup_info = await setup_for_get_next_url_for_final_review(
1313
db_data_creator=db_data_creator,
14-
annotation_count=3,
1514
include_user_annotations=True,
1615
include_miscellaneous_metadata=False
1716
)

tests/helpers/data_creator/commands/impl/urls_/query.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@ def __init__(
1919
url_count: int,
2020
collector_metadata: dict | None = None,
2121
status: URLCreationEnum = URLCreationEnum.OK,
22-
created_at: datetime | None = None
22+
created_at: datetime | None = None,
23+
source: URLSource = URLSource.COLLECTOR
2324
):
2425
super().__init__()
2526
self.batch_id = batch_id
2627
self.url_count = url_count
2728
self.collector_metadata = collector_metadata
2829
self.status = status
2930
self.created_at = created_at
31+
self.source = source
3032

3133
async def run(self) -> InsertURLsInfo:
3234
raise NotImplementedError
@@ -45,7 +47,7 @@ def run_sync(self) -> InsertURLsInfo:
4547
) else None,
4648
collector_metadata=self.collector_metadata,
4749
created_at=self.created_at,
48-
source=URLSource.COLLECTOR
50+
source=self.source
4951
)
5052
)
5153

tests/helpers/data_creator/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,15 @@ def urls(
266266
url_count: int,
267267
collector_metadata: dict | None = None,
268268
outcome: URLCreationEnum = URLCreationEnum.OK,
269+
source: URLSource = URLSource.COLLECTOR,
269270
created_at: datetime | None = None
270271
) -> InsertURLsInfo:
271272
command = URLsDBDataCreatorCommand(
272273
batch_id=batch_id,
273274
url_count=url_count,
274275
collector_metadata=collector_metadata,
275276
status=outcome,
277+
source=source,
276278
created_at=created_at
277279
)
278280
return self.run_command_sync(command)

tests/helpers/setup/final_review/core.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
from src.api.endpoints.annotate.agency.post.dto import URLAgencyAnnotationPostInfo
44
from src.core.enums import RecordType
55
from src.db.models.impl.flag.url_validated.enums import URLType
6+
from src.db.models.impl.url.core.enums import URLSource
67
from tests.helpers.data_creator.core import DBDataCreator
78
from tests.helpers.setup.final_review.model import FinalReviewSetupInfo
89

910

1011
async def setup_for_get_next_url_for_final_review(
1112
db_data_creator: DBDataCreator,
12-
annotation_count: int | None = None,
1313
include_user_annotations: bool = True,
14-
include_miscellaneous_metadata: bool = True
14+
include_miscellaneous_metadata: bool = True,
15+
source: URLSource = URLSource.COLLECTOR
1516
) -> FinalReviewSetupInfo:
1617
"""
1718
Sets up the database to test the final_review functions
@@ -22,7 +23,8 @@ async def setup_for_get_next_url_for_final_review(
2223
batch_id = db_data_creator.batch()
2324
url_mapping = db_data_creator.urls(
2425
batch_id=batch_id,
25-
url_count=1
26+
url_count=1,
27+
source=source
2628
).url_mappings[0]
2729
if include_miscellaneous_metadata:
2830
await db_data_creator.url_miscellaneous_metadata(url_id=url_mapping.url_id)

0 commit comments

Comments
 (0)