Skip to content

Commit

Permalink
[SDESK-7465] - Create new async Desk resource and service (#2812)
Browse files Browse the repository at this point in the history
* Create new resource model for desks resource

* Create new async resource service for desks

* Create resource config for desks

* Create desks module and register resource config

* Added desks to default MODULES config

* Fixed mypy errors

* Suggested fixes
  • Loading branch information
BrianMwangi21 authored Jan 14, 2025
1 parent ed2eccb commit fd224f5
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 10 deletions.
7 changes: 7 additions & 0 deletions apps/desks_async/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from superdesk.core.module import Module
from .desks_async_service import DesksAsyncService
from .module import desks_resource_config

__all__ = ["DesksAsyncService"]

module = Module(name="apps.desks_async", resources=[desks_resource_config])
186 changes: 186 additions & 0 deletions apps/desks_async/desks_async_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
from typing import Any, Sequence
from quart_babel import gettext as _

from superdesk import get_resource_service
from superdesk.activity import add_activity, ACTIVITY_UPDATE
from superdesk.core import get_app_config
from superdesk.core.resources import AsyncResourceService
from superdesk.errors import SuperdeskApiError
from superdesk.notification import push_notification
from superdesk.resource_fields import ID_FIELD
from superdesk.types import DesksResourceModel
from superdesk.types.enums import DeskTypeEnum


class DesksAsyncService(AsyncResourceService[DesksResourceModel]):
notification_key = "desk"

async def create(self, docs: Sequence[DesksResourceModel | dict[str, Any]]) -> list[str]:
"""Creates new desk.
Overriding to check if the desk being created has Working and Incoming Stages. If not then Working and Incoming
Stages would be created and associates them with the desk and desk with the Working and Incoming Stages.
Also sets desk_type.
:return: list of desk id's
"""
docs = await self._convert_dicts_to_model(docs)
stage_service = get_resource_service("stages")

for desk in docs:
stages_to_be_linked_with_desk = []
self._ensure_unique_members(desk.to_dict())

if desk.content_expiry == 0:
desk.content_expiry = get_app_config("CONTENT_EXPIRY_MINUTES") or 0

if desk.working_stage is None:
stages_to_be_linked_with_desk.append("working_stage")
stage_id = stage_service.create_working_stage()
desk.working_stage = stage_id[0]

if desk.incoming_stage is None:
stages_to_be_linked_with_desk.append("incoming_stage")
stage_id = stage_service.create_incoming_stage()
desk.incoming_stage = stage_id[0]

desk.desk_type = DeskTypeEnum.AUTHORING
await super().create([desk])
for stage_type in stages_to_be_linked_with_desk:
stage_service.patch(desk.to_dict()[stage_type], {"desk": desk.id})

# make the desk available in default content template
content_templates = get_resource_service("content_templates")
template = content_templates.find_one(req=None, _id=desk.default_content_template)
if template:
template.setdefault("template_desks", []).append(desk.id)
content_templates.patch(desk.default_content_template, template)

return [str(doc.id) for doc in docs]

async def on_created(self, docs: list[DesksResourceModel]) -> None:
users_service = get_resource_service("users")
for doc in docs:
push_notification(self.notification_key, created=1, desk_id=str(doc.id))
users_service.update_stage_visibility_for_users()

async def on_update(self, updates: dict[str, Any], original: DesksResourceModel) -> None:
if updates.get("content_expiry") == 0:
updates["content_expiry"] = None

self._ensure_unique_members(updates)

if updates.get("desk_type") and updates.get("desk_type") != original.desk_type:
archive_versions_query = {
"$or": [
{"task.last_authoring_desk": str(original.id)},
{"task.last_production_desk": str(original.id)},
]
}

items = get_resource_service("archive_versions").get(req=None, lookup=archive_versions_query)
if items and items.count():
raise SuperdeskApiError.badRequestError(
message=_("Cannot update Desk Type as there are article(s) referenced by the Desk.")
)

async def on_updated(self, updates: dict[str, Any], original: DesksResourceModel) -> None:
await self.__send_notification(updates, original)

async def on_delete(self, doc: DesksResourceModel):
"""Runs on desk delete.
Overriding to prevent deletion of a desk if the desk meets one of the below conditions:
1. The desk isn't assigned as a default desk to user(s)
2. The desk has no content
3. The desk is associated with routing rule(s)
"""

as_default_desk = get_resource_service("users").get(req=None, lookup={"desk": doc.id})
if as_default_desk and as_default_desk.count():
raise SuperdeskApiError.preconditionFailedError(
message=_("Cannot delete desk as it is assigned as default desk to user(s).")
)

routing_rules_query = {
"$or": [
{"rules.actions.fetch.desk": doc.id},
{"rules.actions.publish.desk": doc.id},
]
}
routing_rules = get_resource_service("routing_schemes").get(req=None, lookup=routing_rules_query)
if routing_rules and routing_rules.count():
raise SuperdeskApiError.preconditionFailedError(
message=_("Cannot delete desk as routing scheme(s) are associated with the desk")
)

archive_versions_query = {
"$or": [
{"task.desk": str(doc.id)},
{"task.last_authoring_desk": str(doc.id)},
{"task.last_production_desk": str(doc.id)},
]
}

items = get_resource_service("archive_versions").get(req=None, lookup=archive_versions_query)
if items and items.count():
raise SuperdeskApiError.preconditionFailedError(
message=_("Cannot delete desk as it has article(s) or referenced by versions of the article(s).")
)

async def delete_many(self, lookup: dict[str, Any]) -> list[str]:
"""
Overriding to delete stages before deleting a desk
"""

get_resource_service("stages").delete(lookup={"desk": lookup.get(ID_FIELD)})
return await super().delete_many(lookup)

async def on_deleted(self, doc: DesksResourceModel):
desk_user_ids = [str(member["user"]) for member in doc.members]
push_notification(self.notification_key, deleted=1, user_ids=desk_user_ids, desk_id=str(doc.id))

async def __send_notification(self, updates: dict[str, Any], desk: DesksResourceModel):
desk_id = desk.id
users_service = get_resource_service("users")

if "members" in updates:
added, removed = self.__compare_members(desk.members, updates["members"])
if len(removed) > 0:
push_notification(
"desk_membership_revoked", updated=1, user_ids=[str(item) for item in removed], desk_id=str(desk_id)
)

for added_user in added:
user = users_service.find_one(req=None, _id=added_user)
activity = add_activity(
ACTIVITY_UPDATE,
"user {{user}} has been added to desk {{desk}}: Please re-login.",
self.resource_name,
notify=added,
can_push_notification=False,
user=user.get("username"),
desk=desk.name,
)
push_notification("activity", _dest=activity["recipients"])
users_service.update_stage_visibility_for_user(user)

for removed_user in removed:
user = users_service.find_one(req=None, _id=removed_user)
users_service.update_stage_visibility_for_user(user)

else:
push_notification(self.notification_key, updated=1, desk_id=str(desk_id))

def __compare_members(self, original, updates):
original_members = set([member["user"] for member in original])
updates_members = set([member["user"] for member in updates])
added = updates_members - original_members
removed = original_members - updates_members
return added, removed

def _ensure_unique_members(self, doc: dict[str, Any]):
"""Ensure the members are unique"""
if doc.get("members"):
# ensuring that members list is unique
doc["members"] = [{"user": user} for user in {member.get("user") for member in doc.get("members", [])}]
19 changes: 19 additions & 0 deletions apps/desks_async/module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from superdesk.core.resources import ResourceConfig, MongoResourceConfig, MongoIndexOptions
from superdesk.types import DesksResourceModel
from .desks_async_service import DesksAsyncService

desks_resource_config = ResourceConfig(
name="desks",
data_class=DesksResourceModel,
service=DesksAsyncService,
default_sort=[("name", 1)],
mongo=MongoResourceConfig(
indexes=[
MongoIndexOptions(
name="name_1",
keys=[("name", 1)],
unique=True,
),
],
),
)
2 changes: 1 addition & 1 deletion superdesk/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ def local_to_utc_hour(hour):
#:
#: ..versionadded: 3.0.0
#:
MODULES = ["superdesk.users"]
MODULES = ["superdesk.users", "apps.desks_async"]

ASYNC_AUTH_CLASS = "superdesk.core.auth.token_auth:TokenAuthorization"

Expand Down
3 changes: 2 additions & 1 deletion superdesk/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from typing import TypedDict, Dict, Any, List
from .desks import DesksResourceModel
from .users import UsersResourceModel

__all__ = ["UsersResourceModel"]
__all__ = ["UsersResourceModel", "DesksResourceModel"]


class WebsocketMessageFilterConditions(TypedDict, total=False):
Expand Down
43 changes: 43 additions & 0 deletions superdesk/types/desks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import Annotated, Any
from pydantic import Field

from .enums import MonitoringTypeEnum, MonitoringViewEnum, DeskTypeEnum
from superdesk.core.resources import ResourceModel, fields, dataclass
from superdesk.core.resources.fields import ObjectId
from superdesk.core.resources.validators import validate_unique_value_async, validate_data_relation_async


@dataclass
class MonitoringSetting:
_id: str
type: MonitoringTypeEnum
max_items: int


class DesksResourceModel(ResourceModel):
name: Annotated[fields.Keyword, validate_unique_value_async("desks", "name")]
description: str
members: list[dict[str, Annotated[ObjectId, validate_data_relation_async("users")]]] = Field(default_factory=list)
incoming_stage: Annotated[ObjectId, validate_data_relation_async("stages")] | None = None
working_stage: Annotated[ObjectId, validate_data_relation_async("stages")] | None = None
content_expiry: int
source: str
send_to_desk_not_allowed: bool = Field(default=False)
monitoring_settings: list[MonitoringSetting] = Field(default_factory=list)
desk_type: DeskTypeEnum = Field(default=DeskTypeEnum.AUTHORING)
desk_metadata: dict[str, Any] = Field(default_factory=dict)
content_profiles: dict[str, Any] = Field(default_factory=dict)
desk_language: str
monitoring_default_view: MonitoringViewEnum | None = None
default_content_profile: Annotated[ObjectId, validate_data_relation_async("content_types")] | None = None
default_content_template: Annotated[ObjectId, validate_data_relation_async("content_templates")] | None = None
slack_channel_name: str = Field(description="Name of a Slack channel that may be associated with the desk")
preferred_cv_items: dict[str, Any] = Field(default_factory=dict, description="Desk prefered vocabulary items")
preserve_published_content: bool = Field(
default=False,
description="If the preserve_published_content is set to true then the content on this won't be expired",
)
sams_settings: dict[str, Any] = Field(
default_factory=dict, description="Store SAMS's Desk settings on the Desk items"
)
email: str | None = None
32 changes: 32 additions & 0 deletions superdesk/types/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from enum import Enum, unique


@unique
class DeskTypeEnum(str, Enum):
AUTHORING = "authoring"
PRODUCTION = "production"


@unique
class MonitoringTypeEnum(str, Enum):
SEARCH = "search"
STAGE = "stage"
SCHEDULED_DESK_OUTPUT = "scheduled_desk_output"
DESK_OUTPUT = "desk_output"
PERSONAL = "personal"
SENT_DESK_OUTPUT = "sent_desk_output"


@unique
class MonitoringViewEnum(str, Enum):
BLANK = ""
LIST = "list"
SWIMLANE = "swimlane"
PHOTOGRID = "photogrid"


@unique
class UserTypeEnum(str, Enum):
USER = "user"
ADMINISTRATOR = "administrator"
EXTERNAL = "external"
9 changes: 1 addition & 8 deletions superdesk/types/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,13 @@


from datetime import datetime
from enum import Enum, unique
from typing import Annotated, Any

from pydantic import Field
from superdesk.core.resources import ResourceModel, fields
from superdesk.core.resources.fields import ObjectId
from superdesk.core.resources.validators import validate_unique_value_async, validate_data_relation_async


@unique
class UserTypeEnum(str, Enum):
USER = "user"
ADMINISTRATOR = "administrator"
EXTERNAL = "external"
from superdesk.types.enums import UserTypeEnum


class UsersResourceModel(ResourceModel):
Expand Down

0 comments on commit fd224f5

Please sign in to comment.