Skip to content

Commit faaa616

Browse files
authored
Merge pull request #451 from Police-Data-Accessibility-Project/mc_425_agency_search_endpoint
Update Auto Validate to also require a settled name
2 parents d5971d7 + 72e5261 commit faaa616

File tree

13 files changed

+173
-3
lines changed

13 files changed

+173
-3
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from sqlalchemy import Column
2+
3+
from src.core.tasks.url.operators.validate.queries.ctes.consensus.base import ValidationCTEContainer
4+
from src.core.tasks.url.operators.validate.queries.ctes.consensus.helper import build_validation_query
5+
from src.core.tasks.url.operators.validate.queries.ctes.counts.impl.name import NAME_VALIDATION_COUNTS_CTE
6+
from src.core.tasks.url.operators.validate.queries.ctes.scored import ScoredCTEContainer
7+
8+
9+
class NameValidationCTEContainer(ValidationCTEContainer):
10+
11+
def __init__(self):
12+
_scored = ScoredCTEContainer(
13+
NAME_VALIDATION_COUNTS_CTE
14+
)
15+
16+
self._query = build_validation_query(
17+
_scored,
18+
"name"
19+
)
20+
21+
@property
22+
def name(self) -> Column[int]:
23+
return self._query.c.name
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from sqlalchemy import select, func
2+
3+
from src.core.tasks.url.operators.validate.queries.ctes.counts.core import ValidatedCountsCTEContainer
4+
from src.db.models.impl.link.user_name_suggestion.sqlalchemy import LinkUserNameSuggestion
5+
from src.db.models.impl.url.suggestion.name.sqlalchemy import URLNameSuggestion
6+
from src.db.models.views.unvalidated_url import UnvalidatedURL
7+
8+
NAME_VALIDATION_COUNTS_CTE = ValidatedCountsCTEContainer(
9+
(
10+
select(
11+
URLNameSuggestion.url_id,
12+
URLNameSuggestion.suggestion.label("entity"),
13+
func.count().label("votes")
14+
)
15+
.join(
16+
UnvalidatedURL,
17+
URLNameSuggestion.url_id == UnvalidatedURL.url_id
18+
)
19+
.join(
20+
LinkUserNameSuggestion,
21+
LinkUserNameSuggestion.suggestion_id == URLNameSuggestion.id
22+
)
23+
.group_by(
24+
URLNameSuggestion.url_id,
25+
URLNameSuggestion.suggestion
26+
)
27+
).cte("counts_name")
28+
)

src/core/tasks/url/operators/validate/queries/get/core.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from src.core.exceptions import FailedValidationException
77
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.agency import AgencyValidationCTEContainer
88
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.location import LocationValidationCTEContainer
9+
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.name import NameValidationCTEContainer
910
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.record_type import \
1011
RecordTypeValidationCTEContainer
1112
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.url_type import URLTypeValidationCTEContainer
@@ -24,6 +25,7 @@ async def run(self, session: AsyncSession) -> list[GetURLsForAutoValidationRespo
2425
location = LocationValidationCTEContainer()
2526
url_type = URLTypeValidationCTEContainer()
2627
record_type = RecordTypeValidationCTEContainer()
28+
name = NameValidationCTEContainer()
2729

2830
query = (
2931
select(
@@ -32,6 +34,7 @@ async def run(self, session: AsyncSession) -> list[GetURLsForAutoValidationRespo
3234
agency.agency_id,
3335
url_type.url_type,
3436
record_type.record_type,
37+
name.name,
3538
)
3639
.outerjoin(
3740
agency.query,
@@ -49,13 +52,18 @@ async def run(self, session: AsyncSession) -> list[GetURLsForAutoValidationRespo
4952
record_type.query,
5053
URL.id == record_type.url_id,
5154
)
55+
.outerjoin(
56+
name.query,
57+
URL.id == name.url_id,
58+
)
5259
)
5360
query = add_where_condition(
5461
query,
5562
agency=agency,
5663
location=location,
5764
url_type=url_type,
5865
record_type=record_type,
66+
name=name,
5967
)
6068

6169
mappings: Sequence[RowMapping] = await sh.mappings(session, query=query)

src/core/tasks/url/operators/validate/queries/get/models/response.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class GetURLsForAutoValidationResponse(BaseModel):
1111
agency_id: int | None
1212
url_type: URLType
1313
record_type: RecordType | None
14+
name: str | None
1415

1516
@model_validator(mode="after")
1617
def forbid_record_type_if_not_data_source(self):

src/core/tasks/url/operators/validate/queries/helper.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.agency import AgencyValidationCTEContainer
44
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.location import LocationValidationCTEContainer
5+
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.name import NameValidationCTEContainer
56
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.record_type import \
67
RecordTypeValidationCTEContainer
78
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.url_type import URLTypeValidationCTEContainer
@@ -13,7 +14,8 @@ def add_where_condition(
1314
agency: AgencyValidationCTEContainer,
1415
location: LocationValidationCTEContainer,
1516
url_type: URLTypeValidationCTEContainer,
16-
record_type: RecordTypeValidationCTEContainer
17+
record_type: RecordTypeValidationCTEContainer,
18+
name: NameValidationCTEContainer,
1719
) -> Select:
1820
return (
1921
query
@@ -25,13 +27,15 @@ def add_where_condition(
2527
agency.agency_id.isnot(None),
2628
location.location_id.isnot(None),
2729
record_type.record_type.isnot(None),
30+
name.name.isnot(None),
2831
),
2932
and_(
3033
url_type.url_type.in_(
3134
(URLType.META_URL.value, URLType.INDIVIDUAL_RECORD.value)
3235
),
3336
agency.agency_id.isnot(None),
3437
location.location_id.isnot(None),
38+
name.name.isnot(None),
3539
),
3640
url_type.url_type == URLType.NOT_RELEVANT.value
3741
),

src/core/tasks/url/operators/validate/queries/insert.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from typing import Any
22

3+
from sqlalchemy import update, case
34
from sqlalchemy.ext.asyncio import AsyncSession
45

56
from src.core.tasks.url.operators.validate.queries.get.models.response import GetURLsForAutoValidationResponse
67
from src.db.models.impl.flag.auto_validated.pydantic import FlagURLAutoValidatedPydantic
78
from src.db.models.impl.flag.url_validated.pydantic import FlagURLValidatedPydantic
89
from src.db.models.impl.link.url_agency.pydantic import LinkURLAgencyPydantic
10+
from src.db.models.impl.url.core.pydantic.upsert import URLUpsertModel
11+
from src.db.models.impl.url.core.sqlalchemy import URL
912
from src.db.models.impl.url.record_type.pydantic import URLRecordTypePydantic
1013
from src.db.queries.base.builder import QueryBuilderBase
1114
from src.db.helpers.session import session_helper as sh
@@ -56,4 +59,27 @@ async def run(self, session: AsyncSession) -> Any:
5659
]:
5760
await sh.bulk_insert(session, models=inserts)
5861

62+
await self.update_urls(session)
5963

64+
65+
async def update_urls(self, session: AsyncSession) -> Any:
66+
id_to_name: dict[int, str] = {}
67+
for response in self._responses:
68+
if response.name is not None:
69+
id_to_name[response.url_id] = response.name
70+
71+
if len(id_to_name) == 0:
72+
return
73+
74+
stmt = (
75+
update(URL)
76+
.where(URL.id.in_(id_to_name.keys()))
77+
.values(
78+
name=case(
79+
{id_: val for id_, val in id_to_name.items()},
80+
value=URL.id
81+
)
82+
)
83+
)
84+
85+
await session.execute(stmt)

src/core/tasks/url/operators/validate/queries/prereq/core.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.agency import AgencyValidationCTEContainer
55
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.location import LocationValidationCTEContainer
6+
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.name import NameValidationCTEContainer
67
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.record_type import \
78
RecordTypeValidationCTEContainer
89
from src.core.tasks.url.operators.validate.queries.ctes.consensus.impl.url_type import URLTypeValidationCTEContainer
@@ -25,6 +26,7 @@ async def run(self, session: AsyncSession) -> bool:
2526
location = LocationValidationCTEContainer()
2627
url_type = URLTypeValidationCTEContainer()
2728
record_type = RecordTypeValidationCTEContainer()
29+
name = NameValidationCTEContainer()
2830

2931

3032
query = (
@@ -50,13 +52,18 @@ async def run(self, session: AsyncSession) -> bool:
5052
record_type.query,
5153
UnvalidatedURL.url_id == record_type.url_id,
5254
)
55+
.outerjoin(
56+
name.query,
57+
UnvalidatedURL.url_id == name.url_id,
58+
)
5359
)
5460
query = add_where_condition(
5561
query,
5662
agency=agency,
5763
location=location,
5864
url_type=url_type,
5965
record_type=record_type,
66+
name=name,
6067
).limit(1)
6168

6269
return await sh.results_exist(session, query=query)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from src.db.models.impl.url.core.sqlalchemy import URL
2+
from src.db.models.templates_.base import Base
3+
from src.db.templates.markers.bulk.upsert import BulkUpsertableModel
4+
5+
6+
class URLUpsertModel(BulkUpsertableModel):
7+
8+
@classmethod
9+
def id_field(cls) -> str:
10+
return "id"
11+
12+
@classmethod
13+
def sa_model(cls) -> type[Base]:
14+
"""Defines the SQLAlchemy model."""
15+
return URL
16+
17+
id: int
18+
name: str | None

tests/automated/integration/tasks/url/impl/validate/helper.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
from src.db.models.impl.flag.url_validated.enums import URLType
66
from src.db.models.impl.flag.url_validated.sqlalchemy import FlagURLValidated
77
from src.db.models.impl.link.url_agency.sqlalchemy import LinkURLAgency
8+
from src.db.models.impl.url.core.sqlalchemy import URL
89
from src.db.models.impl.url.record_type.sqlalchemy import URLRecordType
10+
from src.db.models.impl.url.suggestion.name.enums import NameSuggestionSource
911
from tests.conftest import db_data_creator
1012
from tests.helpers.counter import next_int
1113
from tests.helpers.data_creator.core import DBDataCreator
@@ -117,4 +119,27 @@ async def add_record_type_suggestions(
117119
url_id=self.url_id,
118120
record_type=record_type,
119121
user_id=next_int()
120-
)
122+
)
123+
124+
async def add_name_suggestion(
125+
self,
126+
count: int = 1,
127+
) -> str:
128+
name = f"Test Validate Task Name"
129+
suggestion_id: int = await self.db_data_creator.name_suggestion(
130+
url_id=self.url_id,
131+
source=NameSuggestionSource.USER,
132+
name=name,
133+
)
134+
for i in range(count):
135+
await self.db_data_creator.user_name_endorsement(
136+
suggestion_id=suggestion_id,
137+
user_id=next_int(),
138+
)
139+
return name
140+
141+
async def check_name(self) -> None:
142+
urls: list[URL] = await self.adb_client.get_all(URL)
143+
assert len(urls) == 1
144+
url: URL = urls[0]
145+
assert url.name == "Test Validate Task Name"

tests/automated/integration/tasks/url/impl/validate/test_data_source.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ async def test_data_source(
3737

3838
await helper.add_record_type_suggestions(count=2)
3939

40+
assert not await operator.meets_task_prerequisites()
41+
42+
await helper.add_name_suggestion(count=2)
43+
4044
assert await operator.meets_task_prerequisites()
4145

4246
# Add different record type suggestion
@@ -59,4 +63,5 @@ async def test_data_source(
5963
await helper.check_auto_validated()
6064
await helper.check_agency_linked()
6165
await helper.check_record_type()
66+
await helper.check_name()
6267

0 commit comments

Comments
 (0)