Skip to content

Commit

Permalink
[SDESK-7463] - Create new async PlanningTypes resource and service (#…
Browse files Browse the repository at this point in the history
…2169)

* Create new ResourceModel for the planning_types resource

* Create new async resource service for planning_types

* Create resource config with rest endpoints

* Register the resource config with the planning module

* Set internal_resource to True for old planning_types resource

* Fix type error on mypy

* Suggested fixes

* Suggested fixes
  • Loading branch information
BrianMwangi21 authored Jan 8, 2025
1 parent a011e9b commit 45668fb
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 0 deletions.
5 changes: 5 additions & 0 deletions server/planning/content_profiles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
from .service import PlanningTypesService
from planning.common import get_config_event_related_item_search_provider_name

from .planning_types_async_service import PlanningTypesAsyncService
from .module import planning_types_resource_config

__all__ = ["planning_types_resource_config", "PlanningTypesAsyncService"]


def init_app(app: Eve):
superdesk.privilege(
Expand Down
12 changes: 12 additions & 0 deletions server/planning/content_profiles/module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from superdesk.core.resources import ResourceConfig, RestEndpointConfig

from planning.types import PlanningTypesResourceModel
from .planning_types_async_service import PlanningTypesAsyncService


planning_types_resource_config = ResourceConfig(
name="planning_types",
data_class=PlanningTypesResourceModel,
service=PlanningTypesAsyncService,
rest_endpoints=RestEndpointConfig(resource_methods=["GET", "POST"], item_methods=["GET", "PATCH"]),
)
155 changes: 155 additions & 0 deletions server/planning/content_profiles/planning_types_async_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from typing import Any
from copy import deepcopy

from superdesk.core.resources import AsyncResourceService
from superdesk.core.resources.cursor import ElasticsearchResourceCursorAsync
from superdesk.core.types import SearchRequest, ProjectedFieldArg, SortParam

from planning.common import planning_link_updates_to_coverage, get_config_event_related_item_search_provider_name
from planning.types import PlanningTypesResourceModel
from .profiles import DEFAULT_PROFILES


class PlanningTypesAsyncService(AsyncResourceService[PlanningTypesResourceModel]):
"""Planning types async service
Provide a service that returns what fields should be shown in the edit forms in planning, in the edit dictionary.
Also provide a schema to allow the client to validate the values entered in the forms.
Entries can be overridden by providing alternates in the planning_types mongo collection.
"""

async def find_one(
self,
req: SearchRequest | None = None,
projection: ProjectedFieldArg | None = None,
use_mongo: bool = False,
version: int | None = None,
**lookup,
) -> PlanningTypesResourceModel | None:
"""
Overrides the `find_one` method to merge default planning type configurations
with database entries. If no entry exists in the database, it returns a default
planning type configuration.
"""
try:
search_request = (
req
if req is not None
else SearchRequest(
where=lookup,
page=1,
max_results=1,
projection=projection,
use_mongo=use_mongo,
version=version,
)
)

planning_type = await super().find_one(search_request)

# lookup name from either **lookup of planning_item(if lookup has only '_id')
lookup_name = lookup.get("name")
if not lookup_name and planning_type:
lookup_name = planning_type.name

default_planning_type = deepcopy(
next(
(ptype for ptype in DEFAULT_PROFILES if ptype.get("name") == lookup_name),
{},
)
)
if not planning_type:
self._remove_unsupported_fields(default_planning_type)
return PlanningTypesResourceModel(**default_planning_type)

self.merge_planning_type(planning_type.to_dict(), default_planning_type)
return planning_type
except IndexError:
return None

async def find(
self,
req: SearchRequest | dict,
page: int = 1,
max_results: int = 25,
sort: SortParam | None = None,
projection: ProjectedFieldArg | None = None,
use_mongo: bool = False,
) -> ElasticsearchResourceCursorAsync[PlanningTypesResourceModel]:
"""
Overrides the base `find` to return a cursor containing planning types
with default configurations merged into the results from the database. If a planning
type is not present in the database, a default configuration is added.
"""

if isinstance(req, SearchRequest):
req = {
"where": req.where,
"page": req.page,
"max_results": req.max_results,
"sort": req.sort,
"projection": req.projection,
}

cursor = await super().find(req, page, max_results, sort, projection, use_mongo)
planning_types = await cursor.to_list_raw()
merged_planning_types = []

for default_planning_type in deepcopy(DEFAULT_PROFILES):
planning_type = next(
(p for p in planning_types if p.get("name") == default_planning_type.get("name")),
None,
)

# If nothing is defined in database for this planning_type, use default
if planning_type is None:
self._remove_unsupported_fields(default_planning_type)
merged_planning_types.append(default_planning_type)
else:
self.merge_planning_type(planning_type, default_planning_type)
merged_planning_types.append(planning_type)

return ElasticsearchResourceCursorAsync(data_class=PlanningTypesResourceModel, hits=merged_planning_types)

def merge_planning_type(self, planning_type: dict[str, Any], default_planning_type: dict[str, Any]):
# Update schema fields with database schema fields
default_type: dict[str, Any] = {"schema": {}, "editor": {}}
updated_planning_type = deepcopy(default_planning_type or default_type)

updated_planning_type.setdefault("groups", {})
updated_planning_type["groups"].update(planning_type.get("groups", {}))

if planning_type["name"] == "advanced_search":
updated_planning_type["schema"].update(planning_type.get("schema", {}))
updated_planning_type["editor"]["event"].update((planning_type.get("editor") or {}).get("event"))
updated_planning_type["editor"]["planning"].update((planning_type.get("editor") or {}).get("planning"))
updated_planning_type["editor"]["combined"].update((planning_type.get("editor") or {}).get("combined"))
elif planning_type["name"] in ["event", "planning", "coverage"]:
for config_type in ["editor", "schema"]:
planning_type.setdefault(config_type, {})
for field, options in updated_planning_type[config_type].items():
# If this field is none, then it is of type `schema.NoneField()`
# no need to copy any schema
if updated_planning_type[config_type][field]:
updated_planning_type[config_type][field].update(planning_type[config_type].get(field) or {})
else:
updated_planning_type["editor"].update(planning_type.get("editor", {}))
updated_planning_type["schema"].update(planning_type.get("schema", {}))

planning_type["schema"] = updated_planning_type["schema"]
planning_type["editor"] = updated_planning_type["editor"]
planning_type["groups"] = updated_planning_type["groups"]
self._remove_unsupported_fields(planning_type)

def _remove_unsupported_fields(self, planning_type: dict[str, Any]):
# Disable Event ``related_items`` field
# if ``EVENT_RELATED_ITEM_SEARCH_PROVIDER_NAME`` config is not set
if planning_type.get("name") == "event" and not get_config_event_related_item_search_provider_name():
planning_type["editor"].pop("related_items", None)
planning_type["schema"].pop("related_items", None)

# Disable Coverage ``no_content_linking`` field
# if ``PLANNING_LINK_UPDATES_TO_COVERAGES`` config is not ``True``
if planning_type.get("name") == "coverage" and not planning_link_updates_to_coverage():
planning_type["editor"].pop("no_content_linking", None)
planning_type["schema"].pop("no_content_linking", None)
1 change: 1 addition & 0 deletions server/planning/content_profiles/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,4 @@ class PlanningTypesResource(superdesk.Resource):
"POST": "planning_manage_content_profiles",
"PATCH": "planning_manage_content_profiles",
}
internal_resource = True
2 changes: 2 additions & 0 deletions server/planning/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from planning.planning import planning_resource_config, planning_history_resource_config
from planning.assignments import assignments_resource_config, delivery_resource_config
from planning.published import published_resource_config
from planning.content_profiles import planning_types_resource_config


module = Module(
Expand All @@ -13,6 +14,7 @@
assignments_resource_config,
published_resource_config,
delivery_resource_config,
planning_types_resource_config,
events_history_resource_config,
planning_history_resource_config,
],
Expand Down
2 changes: 2 additions & 0 deletions server/planning/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from .assignment import AssignmentResourceModel
from .published import PublishedPlanningModel
from .enums import PostStates, UpdateMethods, WorkflowState
from .planning_types import PlanningTypesResourceModel


__all__ = [
Expand All @@ -34,6 +35,7 @@
"PlanningHistoryResourceModel",
"AssignmentResourceModel",
"PublishedPlanningModel",
"PlanningTypesResourceModel",
"PlanningSchedule",
"PostStates",
"UpdateMethods",
Expand Down
48 changes: 48 additions & 0 deletions server/planning/types/planning_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from datetime import datetime
from pydantic import Field
from typing import Annotated, Any

from superdesk.core.resources import ResourceModel, fields
from superdesk.utc import utcnow
from superdesk.core.resources.fields import ObjectId
from superdesk.core.resources.validators import validate_iunique_value_async, validate_data_relation_async


class PlanningTypesResourceModel(ResourceModel):
# The name identifies the form in the UI to which the type relates
name: Annotated[fields.Keyword, validate_iunique_value_async("planning_types", "name")]

editor: dict[str, Any] = Field(
default_factory=dict,
description="Editor controls which fields are visible in the UI",
)
schema_config: dict[str, Any] = Field(
alias="schema",
default_factory=dict,
description="Schema controls the validation of fields at the front end",
)
groups: dict[str, Any] = Field(
default_factory=dict,
description="List of groups (and their translations) for grouping of fields in the editor",
)
post_schema: dict[str, Any] = Field(
alias="postSchema",
default_factory=dict,
description="Controls the validation of fields when posting",
)
list_fields_config: dict[str, Any] = Field(
alias="list",
default_factory=dict,
description="List fields when seeing events/planning during export/download",
)
export_list: list[str] = Field(
default_factory=list,
description="Fields visible in exports or downloads for events/planning",
)

# Audit information
created_by: Annotated[ObjectId, validate_data_relation_async("users")] | None = None
updated_by: Annotated[ObjectId, validate_data_relation_async("users")] | None = None
firstcreated: datetime = Field(default_factory=utcnow)
versioncreated: datetime = Field(default_factory=utcnow)
init_version: int | None = None

0 comments on commit 45668fb

Please sign in to comment.