Skip to content

Commit 8eadc84

Browse files
[SDESK-7516] - Upgrade Locations resource & service to async (#2219)
* Create location resource model * Create location async service * Create resource config for Location async service, register it in module, set old service to internal resource * Added Locations rest endpoints to cover on_fetched and on_fetched_item * Code refactor * Simplify find count with just count, add generate_guid in model, remove on_create * Added auth config * Added fields.Keywords and validate unique where needed * Added validate on guid
1 parent c63bb41 commit 8eadc84

8 files changed

+171
-1
lines changed

server/planning/locations/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
import superdesk
1313
from .locations_service import LocationsResource, LocationsService
1414

15+
from .locations_service_async import LocationsAsyncService
16+
from .module import locations_resource_config
17+
18+
__all__ = ["LocationsAsyncService", "locations_resource_config"]
19+
1520

1621
def init_app(app):
1722
locations_search_service = LocationsService("locations", backend=superdesk.get_backend())

server/planning/locations/locations_service.py

+1
Original file line numberDiff line numberDiff line change
@@ -173,3 +173,4 @@ class LocationsResource(Resource):
173173
}
174174

175175
merge_nested_documents = True
176+
internal_resource = True
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from typing import Any
2+
3+
from planning.core.service import BasePlanningAsyncService
4+
from planning.events.events_service import EventsAsyncService
5+
from planning.types import LocationResourceModel
6+
7+
from superdesk.resource_fields import ID_FIELD
8+
9+
10+
class LocationsAsyncService(BasePlanningAsyncService[LocationResourceModel]):
11+
async def delete_many(self, lookup: dict[str, Any]) -> list[str]:
12+
"""
13+
If the document to be deleted is referenced in an event, flag it as inactive otherwise just delete it.
14+
"""
15+
if lookup:
16+
location = await self.find_by_id(lookup[ID_FIELD])
17+
if location:
18+
events_count = await EventsAsyncService().count({"location.qcode": str(location.guid)})
19+
if events_count > 0:
20+
# Update the unique name in case the location get recreated
21+
await self.update(
22+
location.id,
23+
{
24+
"is_active": False,
25+
"unique_name": str(location.id),
26+
},
27+
)
28+
return []
29+
30+
return await super().delete_many(lookup)

server/planning/locations/module.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from superdesk.core.auth.privilege_rules import http_method_privilege_based_rules
2+
from superdesk.core.resources import (
3+
ResourceConfig,
4+
ElasticResourceConfig,
5+
RestEndpointConfig,
6+
)
7+
8+
from planning.types import LocationResourceModel
9+
from .locations_service_async import LocationsAsyncService
10+
from .rest_api import LocationsRestEndpoints
11+
12+
locations_resource_config: ResourceConfig = ResourceConfig(
13+
name="locations",
14+
data_class=LocationResourceModel,
15+
service=LocationsAsyncService,
16+
elastic=ElasticResourceConfig(),
17+
rest_endpoints=RestEndpointConfig(
18+
item_methods=["GET", "PATCH", "PUT", "DELETE"],
19+
resource_methods=["GET", "POST"],
20+
endpoints_class=LocationsRestEndpoints,
21+
auth=http_method_privilege_based_rules(
22+
{
23+
"POST": "planning",
24+
"PATCH": "planning_locations_management",
25+
"DELETE": "planning_locations_management",
26+
}
27+
),
28+
),
29+
)

server/planning/locations/rest_api.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from planning.common import format_address
2+
3+
from superdesk.core.types import Request, RestGetResponse
4+
from superdesk.core.resources import ResourceRestEndpoints
5+
6+
7+
class LocationsRestEndpoints(ResourceRestEndpoints):
8+
async def on_fetched(self, request: Request, doc: RestGetResponse) -> None:
9+
"""
10+
Overriding to format location address for multi-item response
11+
"""
12+
for item in doc.get("_items", []):
13+
format_address(item)
14+
15+
async def on_fetched_item(self, request: Request, doc: dict) -> None:
16+
"""
17+
Overriding to format location address for single item response
18+
"""
19+
format_address(doc)

server/planning/module.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from planning.assignments import assignments_resource_config, delivery_resource_config
2323
from planning.published import published_resource_config
2424
from planning.content_profiles import planning_types_resource_config
25-
25+
from planning.locations import locations_resource_config
2626
from .planning_locks import planning_locks as planning_locks_endpoint
2727

2828

@@ -74,5 +74,6 @@ def init_planning(app: SuperdeskAsyncApp):
7474
events_autosave_resource_config,
7575
planning_featured_resource_config,
7676
planning_autosave_resource_config,
77+
locations_resource_config,
7778
],
7879
)

server/planning/types/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from .planning_types import PlanningTypesResourceModel
2727
from .planning_featured import PlanningFeaturedResourceModel
2828
from .autosave import EventAutosaveResourceModel, PlanningAutosaveResourceModel
29+
from .locations import LocationResourceModel
2930

3031

3132
__all__ = [
@@ -49,6 +50,7 @@
4950
"PlanningFeaturedResourceModel",
5051
"EventAutosaveResourceModel",
5152
"PlanningAutosaveResourceModel",
53+
"LocationResourceModel",
5254
]
5355

5456

server/planning/types/locations.py

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from datetime import datetime
2+
from typing import Any, Annotated
3+
from pydantic import Field
4+
5+
from planning.types.base import BasePlanningModel
6+
7+
from superdesk.utc import utcnow
8+
from superdesk.core.resources import fields, dataclass
9+
from superdesk.core.resources.validators import validate_data_relation_async, validate_iunique_value_async
10+
from superdesk.core.utils import generate_guid, GUID_NEWSML
11+
12+
13+
@dataclass
14+
class Position:
15+
latitude: float | None = None
16+
longitude: float | None = None
17+
altitude: int | None = None
18+
gps_datum: str | None = None
19+
20+
21+
@dataclass
22+
class Address:
23+
title: str | None = None
24+
line: list[str] | None = None
25+
suburb: str | None = None
26+
city: str | None = None
27+
state: str | None = None
28+
postal_code: str | None = None
29+
country: str | None = None
30+
locality: str | None = None # Aka city
31+
area: str | None = None
32+
external: dict | None = None
33+
boundingbox: list | None = None
34+
address_type: str | None = Field(alias="type", default=None)
35+
36+
37+
class LocationResourceModel(BasePlanningModel):
38+
guid: Annotated[
39+
fields.Keyword,
40+
validate_iunique_value_async("locations", "guid"),
41+
Field(default_factory=lambda: generate_guid(type=GUID_NEWSML)),
42+
]
43+
unique_id: Annotated[int, validate_iunique_value_async("locations", "unique_id")] | None = None
44+
unique_name: Annotated[fields.Keyword, validate_iunique_value_async("locations", "unique_name")] | None = None
45+
version: int | None = None
46+
ingest_id: fields.Keyword | None = None
47+
48+
# Audit Information
49+
firstcreated: datetime = Field(default_factory=utcnow)
50+
versioncreated: datetime = Field(default_factory=utcnow)
51+
52+
# Ingest Details
53+
ingest_provider: Annotated[fields.ObjectId, validate_data_relation_async("ingest_providers")] | None = None
54+
source: fields.Keyword | None = None
55+
original_source: fields.Keyword | None = None
56+
ingest_provider_sequence: fields.Keyword | None = None
57+
58+
# Location Details
59+
# NewsML-G2 Event properties See:
60+
# https://iptc.org/std/NewsML-G2/2.23/specification/XML-Schema-Doc-Core/ConceptItem.html#LinkC5
61+
name: str | None = None
62+
translations: dict[str, Any] | None = None
63+
location_type: str = Field(alias="type", default="Unclassified")
64+
65+
# Position Details
66+
# NewsML-G2 poiDetails properties See IPTC-G2-Implementation_Guide 12.6.3
67+
# or https://iptc.org/std/NewsML-G2/2.23/specification/XML-Schema-Doc-Power/ConceptItem.html#LinkAA
68+
position: Position | None = None
69+
70+
# Address Details
71+
address: Address | None = None
72+
73+
# Other Location Info
74+
access: list[str] | None = None
75+
details: list[str] | None = None
76+
created: datetime | None = None
77+
ceased_to_exist: datetime | None = None
78+
open_hours: str | None = None
79+
capacity: str | None = None
80+
contact_info: list[str] | None = None
81+
is_active: bool = Field(
82+
default=True, description="Flag indicates if the location is active and should be shown in the UI"
83+
)

0 commit comments

Comments
 (0)