Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

notifications: add user preferences filter #65

46 changes: 46 additions & 0 deletions invenio_users_resources/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2023 Graz University of Technology.
#
# Invenio-Users-Resources is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.

"""User specific resources for notifications."""


from invenio_notifications.models import Recipient
rekt-hard marked this conversation as resolved.
Show resolved Hide resolved
from invenio_notifications.services.builders import RecipientGenerator
from invenio_notifications.services.filters import RecipientFilter
from invenio_records.dictutils import dict_lookup


class UserPreferencesRecipientFilter(RecipientFilter):
ntarocco marked this conversation as resolved.
Show resolved Hide resolved
"""Recipient filter for notifications being enabled at all."""

def __call__(self, notification, recipients):
"""Filter recipients."""
for key in list(recipients.keys()):
r = recipients[key]
if not (
r.data.get("preferences", {})
.get("notifications", {})
.get("enabled", False)
):
del recipients[key]

return recipients


class UserRecipient(RecipientGenerator):
"""User recipient generator for a notification."""

def __init__(self, key):
"""Ctor."""
self.key = key

def __call__(self, notification, recipients):
"""Update required recipient information and add backend id."""
user = dict_lookup(notification.context, self.key)
recipients[user["id"]] = Recipient(data=user)
return recipients
6 changes: 6 additions & 0 deletions invenio_users_resources/records/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ def parse_user_data(user):
default_locale = current_app.config.get("BABEL_DEFAULT_LOCALE", "en")
data["preferences"].setdefault("locale", default_locale)
data["preferences"].setdefault("timezone", "Europe/Zurich")
data["preferences"].setdefault(
"notifications",
{
"enabled": True,
},
)

return data

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@
"timezone": {
"type": "keyword",
"index":"false"
},
"notifications": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add this index name to the changelog inveniosoftware/docs-invenio-rdm#536 ? we need to remember to add this to the migration recipe

"properties": {
"enabled": {
"type": "boolean"
}
}
}
},
"dynamic": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@
"timezone": {
"type": "keyword",
"index":"false"
},
"notifications": {
"properties": {
"enabled": {
"type": "boolean"
}
}
}
},
"dynamic": true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,17 @@
"timezone": {
"type": "keyword",
"index": "false"
},
"notifications": {
"properties": {
"enabled": {
"type": "boolean"
}
}
}
},
"dynamic": true
}
}
}
}
}
6 changes: 6 additions & 0 deletions invenio_users_resources/services/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@ class UserGhostSchema(Schema):
dump_only=True,
)
is_ghost = fields.Boolean(dump_only=True)


class NotificationPreferences(Schema):
"""Schema for notification preferences."""

enabled = fields.Bool()
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ zip_safe = False
install_requires =
invenio-accounts>=2.2.0,<3.0.0
invenio-i18n>=2.0.0
invenio-notifications>=0.1.0,<1.0.0
invenio-oauthclient>=2.2.0,<3.0.0
invenio-records-resources>=4.0.0,<5.0.0

Expand Down
56 changes: 56 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Copyright (C) 2022 TU Wien.
# Copyright (C) 2022 European Union.
# Copyright (C) 2022 CERN.
# Copyright (C) 2023 Graz University of Technology.
#
# Invenio-Users-Resources is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
Expand All @@ -19,16 +20,27 @@
from invenio_access.permissions import any_user as any_user_need
from invenio_accounts.proxies import current_datastore
from invenio_app.factory import create_api
from marshmallow import fields

from invenio_users_resources.proxies import (
current_groups_service,
current_users_service,
)
from invenio_users_resources.records import GroupAggregate, UserAggregate
from invenio_users_resources.services.schemas import (
NotificationPreferences,
UserPreferencesSchema,
)

pytest_plugins = ("celery.contrib.pytest",)


class UserPreferencesNotificationsSchema(UserPreferencesSchema):
"""Schema extending preferences with notification preferences."""

notifications = fields.Nested(NotificationPreferences)


#
# Application
#
Expand All @@ -43,6 +55,8 @@ def app_config(app_config):
] = "invenio_jsonschemas.proxies.current_refresolver_store"
# Variable not used. We set it to silent warnings
app_config["JSONSCHEMAS_HOST"] = "not-used"
# setting preferences schema to test notifications
app_config["ACCOUNTS_USER_PREFERENCES_SCHEMA"] = UserPreferencesNotificationsSchema

return app_config

Expand Down Expand Up @@ -146,6 +160,36 @@ def users_data():
},
"active": False,
},
{
"username": "notification_enabled",
"email": "[email protected]",
"profile": {
"full_name": "Mr. Worldwide",
"affiliations": "World",
},
"preferences": {
"visibility": "restricted",
"email_visibility": "public",
"notifications": {
"enabled": True,
},
},
},
{
"username": "notification_disabled",
"email": "[email protected]",
"profile": {
"full_name": "Loner",
"affiliations": "Home",
},
"preferences": {
"visibility": "restricted",
"email_visibility": "public",
"notifications": {
"enabled": False,
},
},
},
]


Expand Down Expand Up @@ -226,3 +270,15 @@ def user_inactive(users):
def user_unconfirmed(users):
"""Unconfirmed user."""
return users["unconfirmed"]


@pytest.fixture(scope="module")
def user_notification_enabled(users):
"""User with notfications enabled."""
return users["notification_enabled"]


@pytest.fixture(scope="module")
def user_notification_disabled(users):
"""User with notfications disabled."""
return users["notification_disabled"]
83 changes: 83 additions & 0 deletions tests/test_notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2023 Graz University of Technology.
#
# Invenio-Users-Resources is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.

"""Notification related tests."""

from invenio_notifications.models import Notification, Recipient

from invenio_users_resources.notifications import (
UserPreferencesRecipientFilter,
UserRecipient,
)
from invenio_users_resources.records.api import UserAggregate


def test_user_recipient_generator(user_notification_disabled, user_notification_enabled):
generator_disabled = UserRecipient(key="disabled")
generator_enabled = UserRecipient(key="enabled")

user_notifications_disabled = UserAggregate.from_user(user_notification_disabled.user).dumps()
user_notifications_enabled = UserAggregate.from_user(
user_notification_enabled.user
).dumps()

n = Notification(
type="",
context={
"disabled": user_notifications_disabled,
"enabled": user_notifications_enabled,
},
)

recipients = {}

generator_disabled(n, recipients=recipients)
assert 1 == len(recipients)
assert user_notifications_disabled["id"] in recipients.keys()

generator_enabled(n, recipients=recipients)
assert 2 == len(recipients)
assert [
user_notifications_disabled["id"],
user_notifications_enabled["id"],
] == list(recipients)

# checking that user does not get added twice
generator_disabled(n, recipients=recipients)
assert 2 == len(recipients)
assert [
user_notifications_disabled["id"],
user_notifications_enabled["id"],
] == list(recipients)


def test_user_recipient_filter(user_notification_disabled, user_notification_enabled):
"""Test user recipient filter for notifications."""

user_notifications_disabled = UserAggregate.from_user(user_notification_disabled.user).dumps()
user_notifications_enabled = UserAggregate.from_user(
user_notification_enabled.user
).dumps()

n = Notification(type="", context={})
recipient_enabled = Recipient(data=user_notifications_disabled)
recipient_disabled = Recipient(data=user_notifications_enabled)

recipients = {
user_notifications_disabled["id"]: recipient_disabled,
user_notifications_enabled["id"]: recipient_enabled,
}

preferences_filter = UserPreferencesRecipientFilter()
filtered_recipients = preferences_filter(
notification=n,
recipients=recipients,
)

assert 1 == len(filtered_recipients)
assert recipient_enabled == filtered_recipients[user_notifications_enabled["id"]]