Skip to content

Commit

Permalink
Enable Notifications feature for WRI (#146)
Browse files Browse the repository at this point in the history
* Initialize DB for WRI Activity Plugin

* Initialize DB for WRI Activity Plugin

* Add a basic function to query the entire table for the user_id

* Add a action to read all and only for specific activity

* Add update and create function for the notification feature

* update readem

* Add auth and validators for notification action endpoints

* [README] Add instr. for notification feature

* [README] Fix headings

* [README] Fix headings

* [README] Minor typo fixes

* Add state column for the notifications

* Fix parameters for state

* [README] Typo fix
  • Loading branch information
MuhammadIsmailShahzad authored Dec 1, 2023
1 parent 6d7f8a1 commit eb0cc2b
Show file tree
Hide file tree
Showing 10 changed files with 574 additions and 6 deletions.
50 changes: 50 additions & 0 deletions ckan-backend-dev/src/ckanext-wri/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,56 @@

This is the WRI Open Data Portal extension for CKAN. It contains CKAN backend customizations for this project.

## Notifications Feature

This extension includes a notification feature that utilizes its own database, action endpoints, and custom validators.
The feature adds three actions
* `notification_create`
* `notification_update`
* `notification_get_all`

### Database Setup

This extension adds a table named `notifications` to the database for the API notification feature. Initialization is required on the initial boot-up of the extension.

To initiate the database setup, use the following command:

```console
ckan -c <path-to-ini-file> notificationdb
```
### API Endpoints

#### POST /api/action/notification_create

**Parameters:**
- **recipient_id** (string) – The user ID of the recipient of the notification (required).
- **sender_id** (string) – The user ID of the sender of the notification (required).
- **activity_type** (string) – The type of activity that triggers the notification, such as `dataset_create`, etc. (required).
- **object_type** (string) – The type of the object on which the action is being performed (e.g., dataset, resource, etc.) (required).
- **object_id** (string) – The ID of the object on which the action is being performed (required).

The parameters `time_sent` (set to the current timestamp), `state` (set as `active`) and `is_unread` (set to `false`) are automatically configured during creation.

#### POST /api/action/notification_update

**Parameters:**
- **recipient_id** (string) – The user ID of the recipient of the notification (required).
- **sender_id** (string) – The user ID of the sender of the notification (required).
- **activity_type** (string) – The type of activity that triggers the notification, such as `dataset_create`, etc. (required).
- **object_type** (string) – The type of the object on which the action is being performed (e.g., dataset, resource, etc.) (required).
- **object_id** (string) – The ID of the object on which the action is being performed (required).
- **time_sent** (datetime withut timezone) – The timestamp of the sent time (required).
- **is_unread** (string) – Indicates whether the notification is read or not (required).
- **state** (string) – `active` or `deleted` (required).

#### GET /api/action/notification_get_all

Returns a list of notifications for a sender or recipient.

**Parameters:**
- **recipient_id** (string) – The user ID of the recipient of the notification (optional, but either `recipient_id` or `sender_id` is required).
- **sender_id** (string) – The user ID of the sender of the notification (optional, but either `recipient_id` or `sender_id` is required).

## Development

See the [CKAN Backend Development README](ckan-backend-dev/README.md) for instructions on how to set up a local Docker CKAN backend development environment.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from ckan.types import ActionResult, Context, DataDict
from typing_extensions import TypeAlias
import logging
from ckanext.wri.model.notification import Notification, notification_dictize
import ckan.plugins.toolkit as tk
from ckanext.wri.logic.auth import schema

NotificationGetUserViewedActivity: TypeAlias = None
log = logging.getLogger(__name__)

def notification_create(context: Context, data_dict: DataDict) -> NotificationGetUserViewedActivity:
"""Create a Notification by providing Sender and Recipient"""

model = context["model"]
session = context['session']
user_obj = model.User.get(context["user"])

tk.check_access("notification_create", context, data_dict)
sch = context.get("schema") or schema.default_create_notification_schema()
data, errors = tk.navl_validate(data_dict, sch, context)
if errors:
raise tk.ValidationError(errors)

recipient_id = data_dict.get('recipient_id')
sender_id = data_dict.get('sender_id')
activity_type = data_dict.get('activity_type')
object_type = data_dict.get('object_type')
object_id = data_dict.get('object_id')

user_notifications = Notification(
recipient_id=recipient_id,
sender_id=sender_id,
activity_type=activity_type,
object_type=object_type,
object_id=object_id
)

session.add(user_notifications)

if not context.get('defer_commit'):
model.repo.commit()

notification_dicts = notification_dictize(user_notifications, context)
return notification_dicts
46 changes: 45 additions & 1 deletion ckan-backend-dev/src/ckanext-wri/ckanext/wri/logic/action/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@

from ckan.common import _
from ckan.types import ActionResult, Context, DataDict
from typing_extensions import TypeAlias
from ckanext.wri.model.notification import Notification,notification_dictize, notification_list_dictize
import datetime
import ckan.plugins.toolkit as tk
from ckanext.wri.logic.auth import schema

log = logging.getLogger('ckan.logic')

Expand All @@ -38,6 +43,7 @@
ValidationError = logic.ValidationError


NotificationGetUserViewedActivity: TypeAlias = None
log = logging.getLogger(__name__)


Expand Down Expand Up @@ -381,4 +387,42 @@ def package_search(context: Context, data_dict: DataDict) -> ActionResult.Packag
search_results['search_facets'][facet]['items'],
key=lambda facet: facet['display_name'], reverse=True)

return search_results
return search_results

@logic.side_effect_free
def notification_get_all(
context: Context, data_dict: DataDict
) -> NotificationGetUserViewedActivity:
"""Get the notifications for a user by sender_id or receiver_id
"""
model = context["model"]
session = context['session']

tk.check_access("notification_get_all", context, data_dict)
sender_id = data_dict.get('sender_id')
recipient_id = data_dict.get('recipient_id')

user_obj = model.User.get(context["user"])
user_id = user_obj.id

sch = context.get("schema") or schema.default_get_notification_schema()
data, errors = tk.navl_validate(data_dict, sch, context)
if errors:
raise tk.ValidationError(errors)

notification_objects = Notification.get(recipient_id=recipient_id,sender_id=sender_id)

try:
iter(notification_objects)
notification_objecst_result = notification_list_dictize(
notification_objects, context
)
except TypeError:
notification_objecst_result = notification_dictize(
notification_objects, context
)

if not notification_objecst_result:
raise logic.NotFound(_('Notification not found'))

return notification_objecst_result
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from ckan.types import ActionResult, Context, DataDict
from typing_extensions import TypeAlias
import logging
from ckanext.wri.model.notification import Notification, notification_list_dictize
from ckanext.wri.logic.auth import schema
import ckan.plugins.toolkit as tk
import ckan.logic as logic
from ckan.common import _

NotificationGetUserViewedActivity: TypeAlias = None
log = logging.getLogger(__name__)

def notification_update(context: Context, data_dict: DataDict) -> NotificationGetUserViewedActivity:
"""Update notification status for a user"""

tk.check_access("notification_create", context, data_dict)
sch = context.get("schema") or schema.default_update_notification_schema()
data, errors = tk.navl_validate(data_dict, sch, context)
if errors:
raise tk.ValidationError(errors)

model = context["model"]
session = context['session']
user_obj = model.User.get(context["user"])

if not data_dict.get("id"):
return

notification_id = data_dict.get("id")
recipient_id = data_dict.get('recipient_id')
sender_id = data_dict.get('sender_id')
activity_type = data_dict.get('activity_type')
object_type = data_dict.get('object_type')
object_id = data_dict.get('object_id')
time_sent = data_dict.get('time_sent')
is_unread = data_dict.get('is_unread')
state = data_dict.get('state')

user_notifications = Notification.update(
notification_id=notification_id,
recipient_id=recipient_id,
sender_id=sender_id,
activity_type=activity_type,
object_type=object_type,
object_id=object_id,
time_sent=time_sent,
is_unread=is_unread,
state=state
)

notification_dicts = notification_list_dictize(user_notifications, context)
if not notification_dicts:
raise logic.NotFound(_('Notification not found'))
return notification_dicts
23 changes: 23 additions & 0 deletions ckan-backend-dev/src/ckanext-wri/ckanext/wri/logic/auth/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import ckan.authz as authz
import ckan.plugins.toolkit as tk
from ckan.types import Context, DataDict, AuthResult


@tk.auth_allow_anonymous_access
def notification_get_all(
context: Context, data_dict: DataDict
) -> AuthResult:
if context.get("user"):
return {"success": True}
else:
return {
"success": False,
"msg": tk._("You must be logged in to access the notifications."),
}


@tk.auth_allow_anonymous_access
def notification_create(
context: Context, data_dict: DataDict
) -> AuthResult:
return tk.check_access("notification_get_all", context, data_dict)
134 changes: 134 additions & 0 deletions ckan-backend-dev/src/ckanext-wri/ckanext/wri/logic/auth/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-

from __future__ import annotations

from ckan.logic.schema import validator_args, default_pagination_schema
from ckan.types import Schema, Validator, ValidatorFactory
from ckanext.wri.logic.validators import mutually_exclusive


@validator_args
def default_create_notification_schema(
ignore: Validator,
not_missing: Validator,
not_empty: Validator,
unicode_safe: Validator,
ignore_empty: Validator,
ignore_missing: Validator,
) -> Schema:
return {
"recipient_id": [
not_missing,
not_empty,
unicode_safe,
],
"sender_id": [
not_missing,
not_empty,
unicode_safe,
],
"activity_type": [
not_missing,
not_empty,
unicode_safe,
],
"object_type": [
not_missing,
not_empty,
unicode_safe,
],
"object_id": [
not_missing,
not_empty,
unicode_safe,
],
"time_sent": [ignore_empty, ignore_missing],
"is_unread": [ignore_empty, ignore_missing],
"state": [ignore_empty, ignore_missing]
}

@validator_args
def default_update_notification_schema(
ignore: Validator,
not_missing: Validator,
not_empty: Validator,
unicode_safe: Validator,
ignore_empty: Validator,
ignore_missing: Validator,
) -> Schema:
return {
"id": [
not_missing,
not_empty,
unicode_safe,
],
"recipient_id": [
not_missing,
not_empty,
unicode_safe,
],
"sender_id": [
not_missing,
not_empty,
unicode_safe,
],
"activity_type": [
not_missing,
not_empty,
unicode_safe,
],
"object_type": [
not_missing,
not_empty,
unicode_safe,
],
"object_id": [
not_missing,
not_empty,
unicode_safe,
],
"time_sent": [
not_missing,
not_empty,
unicode_safe,
],
"is_unread":[
not_missing,
not_empty,
unicode_safe,
],
"state":[
not_missing,
not_empty,
unicode_safe,
],
}


@validator_args
def default_get_notification_schema(
ignore: Validator,
not_missing: Validator,
not_empty: Validator,
unicode_safe: Validator,
ignore_empty: Validator,
ignore_missing: Validator,
) -> Schema:
return {
"recipient_id": [
unicode_safe,
mutually_exclusive('sender_id'),
ignore_missing
],
"sender_id": [
unicode_safe,
mutually_exclusive('recipient_id'),
ignore_missing
],
"activity_type": [ignore_empty, ignore_missing],
"object_type": [ignore_empty, ignore_missing],
"object_id": [ignore_empty, ignore_missing],
"time_sent": [ignore_empty, ignore_missing],
"is_unread":[ignore_empty, ignore_missing],
"state": [ignore_empty, ignore_missing]
}
Loading

0 comments on commit eb0cc2b

Please sign in to comment.