Skip to content

Commit 8aaaee7

Browse files
authored
Merge pull request #487 from Police-Data-Accessibility-Project/mc_434_anonymous_annotation_submissions
Add anonymous annotation endpoint
2 parents 1aa6603 + ef55307 commit 8aaaee7

24 files changed

Lines changed: 481 additions & 96 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Add anonymous annotation tables
2+
3+
Revision ID: 7aace6587d1a
4+
Revises: 43077d7e08c5
5+
Create Date: 2025-10-13 20:07:18.388899
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
from src.util.alembic_helpers import url_id_column, agency_id_column, created_at_column, location_id_column, enum_column
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = '7aace6587d1a'
17+
down_revision: Union[str, None] = '43077d7e08c5'
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
op.create_table(
24+
"anonymous_annotation_agency",
25+
url_id_column(),
26+
agency_id_column(),
27+
created_at_column(),
28+
sa.PrimaryKeyConstraint('url_id', 'agency_id')
29+
)
30+
op.create_table(
31+
"anonymous_annotation_location",
32+
url_id_column(),
33+
location_id_column(),
34+
created_at_column(),
35+
sa.PrimaryKeyConstraint('url_id', 'location_id')
36+
)
37+
op.create_table(
38+
"anonymous_annotation_record_type",
39+
url_id_column(),
40+
enum_column(
41+
column_name="record_type",
42+
enum_name="record_type"
43+
),
44+
created_at_column(),
45+
sa.PrimaryKeyConstraint('url_id', 'record_type')
46+
)
47+
op.create_table(
48+
"anonymous_annotation_url_type",
49+
url_id_column(),
50+
enum_column(
51+
column_name="url_type",
52+
enum_name="url_type"
53+
),
54+
created_at_column(),
55+
sa.PrimaryKeyConstraint('url_id', 'url_type')
56+
)
57+
58+
59+
def downgrade() -> None:
60+
pass
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from sqlalchemy.ext.asyncio import AsyncSession
2+
3+
from src.api.endpoints.annotate._shared.queries.get_annotation_batch_info import GetAnnotationBatchInfoQueryBuilder
4+
from src.api.endpoints.annotate.all.get.models.agency import AgencyAnnotationResponseOuterInfo
5+
from src.api.endpoints.annotate.all.get.models.location import LocationAnnotationResponseOuterInfo
6+
from src.api.endpoints.annotate.all.get.models.name import NameAnnotationSuggestion
7+
from src.api.endpoints.annotate.all.get.models.record_type import RecordTypeAnnotationSuggestion
8+
from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse, \
9+
GetNextURLForAllAnnotationInnerResponse
10+
from src.api.endpoints.annotate.all.get.models.url_type import URLTypeAnnotationSuggestion
11+
from src.api.endpoints.annotate.all.get.queries.agency.core import GetAgencySuggestionsQueryBuilder
12+
from src.api.endpoints.annotate.all.get.queries.convert import \
13+
convert_user_url_type_suggestion_to_url_type_annotation_suggestion, \
14+
convert_user_record_type_suggestion_to_record_type_annotation_suggestion
15+
from src.api.endpoints.annotate.all.get.queries.location_.core import GetLocationSuggestionsQueryBuilder
16+
from src.api.endpoints.annotate.all.get.queries.name.core import GetNameSuggestionsQueryBuilder
17+
from src.db.dto_converter import DTOConverter
18+
from src.db.dtos.url.mapping import URLMapping
19+
from src.db.models.impl.url.core.sqlalchemy import URL
20+
from src.db.models.impl.url.suggestion.agency.user import UserUrlAgencySuggestion
21+
22+
23+
async def extract_and_format_get_annotation_result(
24+
session: AsyncSession,
25+
url: URL,
26+
batch_id: int | None = None
27+
):
28+
html_response_info = DTOConverter.html_content_list_to_html_response_info(
29+
url.html_content
30+
)
31+
url_type_suggestions: list[URLTypeAnnotationSuggestion] = \
32+
convert_user_url_type_suggestion_to_url_type_annotation_suggestion(
33+
url.user_relevant_suggestions
34+
)
35+
record_type_suggestions: list[RecordTypeAnnotationSuggestion] = \
36+
convert_user_record_type_suggestion_to_record_type_annotation_suggestion(
37+
url.user_record_type_suggestions
38+
)
39+
agency_suggestions: AgencyAnnotationResponseOuterInfo = \
40+
await GetAgencySuggestionsQueryBuilder(url_id=url.id).run(session)
41+
location_suggestions: LocationAnnotationResponseOuterInfo = \
42+
await GetLocationSuggestionsQueryBuilder(url_id=url.id).run(session)
43+
name_suggestions: list[NameAnnotationSuggestion] = \
44+
await GetNameSuggestionsQueryBuilder(url_id=url.id).run(session)
45+
return GetNextURLForAllAnnotationResponse(
46+
next_annotation=GetNextURLForAllAnnotationInnerResponse(
47+
url_info=URLMapping(
48+
url_id=url.id,
49+
url=url.url
50+
),
51+
html_info=html_response_info,
52+
url_type_suggestions=url_type_suggestions,
53+
record_type_suggestions=record_type_suggestions,
54+
agency_suggestions=agency_suggestions,
55+
batch_info=await GetAnnotationBatchInfoQueryBuilder(
56+
batch_id=batch_id,
57+
models=[
58+
UserUrlAgencySuggestion,
59+
]
60+
).run(session),
61+
location_suggestions=location_suggestions,
62+
name_suggestions=name_suggestions
63+
)
64+
)

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

Lines changed: 3 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,9 @@
22
from sqlalchemy.ext.asyncio import AsyncSession
33
from sqlalchemy.orm import joinedload
44

5-
from src.api.endpoints.annotate._shared.queries.get_annotation_batch_info import GetAnnotationBatchInfoQueryBuilder
6-
from src.api.endpoints.annotate.all.get.models.agency import AgencyAnnotationResponseOuterInfo
7-
from src.api.endpoints.annotate.all.get.models.location import LocationAnnotationResponseOuterInfo
8-
from src.api.endpoints.annotate.all.get.models.name import NameAnnotationSuggestion
9-
from src.api.endpoints.annotate.all.get.models.record_type import RecordTypeAnnotationSuggestion
10-
from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse, \
11-
GetNextURLForAllAnnotationInnerResponse
12-
from src.api.endpoints.annotate.all.get.models.url_type import URLTypeAnnotationSuggestion
13-
from src.api.endpoints.annotate.all.get.queries.agency.core import GetAgencySuggestionsQueryBuilder
14-
from src.api.endpoints.annotate.all.get.queries.convert import \
15-
convert_user_url_type_suggestion_to_url_type_annotation_suggestion, \
16-
convert_user_record_type_suggestion_to_record_type_annotation_suggestion
17-
from src.api.endpoints.annotate.all.get.queries.location_.core import GetLocationSuggestionsQueryBuilder
18-
from src.api.endpoints.annotate.all.get.queries.name.core import GetNameSuggestionsQueryBuilder
5+
from src.api.endpoints.annotate._shared.extract import extract_and_format_get_annotation_result
6+
from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse
197
from src.collectors.enums import URLStatus
20-
from src.db.dto_converter import DTOConverter
21-
from src.db.dtos.url.mapping import URLMapping
228
from src.db.models.impl.flag.url_suspended.sqlalchemy import FlagURLSuspended
239
from src.db.models.impl.link.batch_url.sqlalchemy import LinkBatchURL
2410
from src.db.models.impl.url.core.sqlalchemy import URL
@@ -135,43 +121,5 @@ async def run(
135121
next_annotation=None
136122
)
137123

138-
html_response_info = DTOConverter.html_content_list_to_html_response_info(
139-
url.html_content
140-
)
141-
142-
url_type_suggestions: list[URLTypeAnnotationSuggestion] = \
143-
convert_user_url_type_suggestion_to_url_type_annotation_suggestion(
144-
url.user_relevant_suggestions
145-
)
146-
record_type_suggestions: list[RecordTypeAnnotationSuggestion] = \
147-
convert_user_record_type_suggestion_to_record_type_annotation_suggestion(
148-
url.user_record_type_suggestions
149-
)
150-
agency_suggestions: AgencyAnnotationResponseOuterInfo = \
151-
await GetAgencySuggestionsQueryBuilder(url_id=url.id).run(session)
152-
location_suggestions: LocationAnnotationResponseOuterInfo = \
153-
await GetLocationSuggestionsQueryBuilder(url_id=url.id).run(session)
154-
name_suggestions: list[NameAnnotationSuggestion] = \
155-
await GetNameSuggestionsQueryBuilder(url_id=url.id).run(session)
156-
124+
return await extract_and_format_get_annotation_result(session, url=url, batch_id=self.batch_id)
157125

158-
return GetNextURLForAllAnnotationResponse(
159-
next_annotation=GetNextURLForAllAnnotationInnerResponse(
160-
url_info=URLMapping(
161-
url_id=url.id,
162-
url=url.url
163-
),
164-
html_info=html_response_info,
165-
url_type_suggestions=url_type_suggestions,
166-
record_type_suggestions=record_type_suggestions,
167-
agency_suggestions=agency_suggestions,
168-
batch_info=await GetAnnotationBatchInfoQueryBuilder(
169-
batch_id=self.batch_id,
170-
models=[
171-
UserUrlAgencySuggestion,
172-
]
173-
).run(session),
174-
location_suggestions=location_suggestions,
175-
name_suggestions=name_suggestions
176-
)
177-
)

src/api/endpoints/annotate/all/post/query.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@
33
from src.api.endpoints.annotate.all.post.models.request import AllAnnotationPostInfo
44
from src.api.endpoints.annotate.all.post.requester import AddAllAnnotationsToURLRequester
55
from src.db.models.impl.flag.url_validated.enums import URLType
6-
from src.db.models.impl.url.suggestion.agency.user import UserUrlAgencySuggestion
7-
from src.db.models.impl.url.suggestion.location.user.sqlalchemy import UserLocationSuggestion
8-
from src.db.models.impl.url.suggestion.record_type.user import UserRecordTypeSuggestion
9-
from src.db.models.impl.url.suggestion.relevant.user import UserURLTypeSuggestion
106
from src.db.queries.base.builder import QueryBuilderBase
117

128

src/api/endpoints/annotate/anonymous/__init__.py

Whitespace-only changes.

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

Whitespace-only changes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from typing import Any
2+
3+
from sqlalchemy import Select, func
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
from sqlalchemy.orm import joinedload
6+
7+
from src.api.endpoints.annotate._shared.extract import extract_and_format_get_annotation_result
8+
from src.api.endpoints.annotate.all.get.models.response import GetNextURLForAllAnnotationResponse
9+
from src.collectors.enums import URLStatus
10+
from src.db.helpers.query import not_exists_url
11+
from src.db.models.impl.url.core.sqlalchemy import URL
12+
from src.db.models.impl.url.suggestion.anonymous.url_type.sqlalchemy import AnonymousAnnotationURLType
13+
from src.db.models.views.unvalidated_url import UnvalidatedURL
14+
from src.db.models.views.url_anno_count import URLAnnotationCount
15+
from src.db.models.views.url_annotations_flags import URLAnnotationFlagsView
16+
from src.db.queries.base.builder import QueryBuilderBase
17+
18+
19+
class GetNextURLForAnonymousAnnotationQueryBuilder(QueryBuilderBase):
20+
21+
async def run(self, session: AsyncSession) -> GetNextURLForAllAnnotationResponse:
22+
23+
query = (
24+
Select(URL)
25+
# URL Must be unvalidated
26+
.join(
27+
UnvalidatedURL,
28+
UnvalidatedURL.url_id == URL.id
29+
)
30+
.join(
31+
URLAnnotationFlagsView,
32+
URLAnnotationFlagsView.url_id == URL.id
33+
)
34+
.join(
35+
URLAnnotationCount,
36+
URLAnnotationCount.url_id == URL.id
37+
)
38+
.where(
39+
URL.status == URLStatus.OK.value,
40+
not_exists_url(AnonymousAnnotationURLType)
41+
)
42+
.options(
43+
joinedload(URL.html_content),
44+
joinedload(URL.user_relevant_suggestions),
45+
joinedload(URL.user_record_type_suggestions),
46+
joinedload(URL.name_suggestions),
47+
)
48+
.order_by(
49+
func.random()
50+
)
51+
.limit(1)
52+
)
53+
54+
raw_results = (await session.execute(query)).unique()
55+
url: URL | None = raw_results.scalars().one_or_none()
56+
if url is None:
57+
return GetNextURLForAllAnnotationResponse(
58+
next_annotation=None
59+
)
60+
61+
return await extract_and_format_get_annotation_result(session, url=url)

src/api/endpoints/annotate/anonymous/post/__init__.py

Whitespace-only changes.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from sqlalchemy.ext.asyncio import AsyncSession
2+
3+
from src.api.endpoints.annotate.all.post.models.request import AllAnnotationPostInfo
4+
from src.db.models.impl.url.suggestion.anonymous.agency.sqlalchemy import AnonymousAnnotationAgency
5+
from src.db.models.impl.url.suggestion.anonymous.location.sqlalchemy import AnonymousAnnotationLocation
6+
from src.db.models.impl.url.suggestion.anonymous.record_type.sqlalchemy import AnonymousAnnotationRecordType
7+
from src.db.models.impl.url.suggestion.anonymous.url_type.sqlalchemy import AnonymousAnnotationURLType
8+
from src.db.queries.base.builder import QueryBuilderBase
9+
10+
11+
class AddAnonymousAnnotationsToURLQueryBuilder(QueryBuilderBase):
12+
def __init__(
13+
self,
14+
url_id: int,
15+
post_info: AllAnnotationPostInfo
16+
):
17+
super().__init__()
18+
self.url_id = url_id
19+
self.post_info = post_info
20+
21+
async def run(self, session: AsyncSession) -> None:
22+
23+
url_type_suggestion = AnonymousAnnotationURLType(
24+
url_id=self.url_id,
25+
url_type=self.post_info.suggested_status
26+
)
27+
session.add(url_type_suggestion)
28+
29+
if self.post_info.record_type is not None:
30+
record_type_suggestion = AnonymousAnnotationRecordType(
31+
url_id=self.url_id,
32+
record_type=self.post_info.record_type
33+
)
34+
session.add(record_type_suggestion)
35+
36+
if len(self.post_info.location_info.location_ids) != 0:
37+
location_suggestions = [
38+
AnonymousAnnotationLocation(
39+
url_id=self.url_id,
40+
location_id=location_id
41+
)
42+
for location_id in self.post_info.location_info.location_ids
43+
]
44+
session.add_all(location_suggestions)
45+
46+
if len(self.post_info.agency_info.agency_ids) != 0:
47+
agency_suggestions = [
48+
AnonymousAnnotationAgency(
49+
url_id=self.url_id,
50+
agency_id=agency_id
51+
)
52+
for agency_id in self.post_info.agency_info.agency_ids
53+
]
54+
session.add_all(agency_suggestions)
55+
56+
# Ignore Name suggestions

0 commit comments

Comments
 (0)