From 7ab4307b2a86477435ffe38409f3f48514b9b49d Mon Sep 17 00:00:00 2001 From: Paulo Viadanna Date: Sun, 17 Mar 2019 21:41:55 -0300 Subject: [PATCH] Add API to purge notifications This commit introduces API endpoints to request the removal of notifications for specific users and to remove notifications containing specific payloads. Version bumped to 0.9.0 --- edx_notifications/lib/admin.py | 22 ++++++++ edx_notifications/server/api/admin.py | 25 +++++++++ .../server/api/tests/test_admin.py | 51 +++++++++++++++++++ edx_notifications/server/api/url_regex.py | 2 + edx_notifications/server/api/urls.py | 14 +++-- edx_notifications/server/api/urls_mock.py | 6 +++ .../stores/sql/store_provider.py | 22 +++++++- setup.py | 2 +- 8 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 edx_notifications/lib/admin.py create mode 100644 edx_notifications/server/api/admin.py create mode 100644 edx_notifications/server/api/tests/test_admin.py diff --git a/edx_notifications/lib/admin.py b/edx_notifications/lib/admin.py new file mode 100644 index 00000000..f6854ce0 --- /dev/null +++ b/edx_notifications/lib/admin.py @@ -0,0 +1,22 @@ +""" +All in-proc API endpoints for acting as a Notifications Admin +""" +from edx_notifications.stores.store import notification_store + + +def purge_user_data(user_ids): + """ + This will purge the notifications and preferences for the given user IDs + :param user_ids: and iterable of user IDs + """ + store = notification_store() + store.purge_notifications_for_users(user_ids) + + +def purge_notifications_with_payload(payload): + """ + This will purge the notifications and containing the given payload + :payload: string content contained in notifications payload + """ + store = notification_store() + store.purge_notifications_containing(payload) diff --git a/edx_notifications/server/api/admin.py b/edx_notifications/server/api/admin.py new file mode 100644 index 00000000..e632108e --- /dev/null +++ b/edx_notifications/server/api/admin.py @@ -0,0 +1,25 @@ +""" +Administration endpoints +""" +from django.http import HttpResponseBadRequest, HttpResponse, HttpResponseForbidden + +from edx_notifications.lib.admin import purge_user_data +from edx_notifications.server.api.api_utils import AuthenticatedAPIView + + +class DeleteUsersData(AuthenticatedAPIView): + """ + POST removes all data for given user IDs + """ + + def post(self, request): + """ + HTTP POST Handler + """ + if 'user_ids' not in request.data: + return HttpResponseBadRequest('Missing user ids') + if not request.user.is_staff: + return HttpResponseForbidden() + user_ids = request.data.get('user_ids') + purge_user_data(user_ids) + return HttpResponse(status=204) diff --git a/edx_notifications/server/api/tests/test_admin.py b/edx_notifications/server/api/tests/test_admin.py new file mode 100644 index 00000000..f03e5cf0 --- /dev/null +++ b/edx_notifications/server/api/tests/test_admin.py @@ -0,0 +1,51 @@ +""" +Tests for the administration endpoints +""" +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.test import TestCase + +from edx_notifications.server.api.tests.utils import TestClient + + +class AdminAPITests(TestCase): + """ + Tests for the admin.py + """ + + def setUp(self): + """ + Create clients + """ + + self.admin_client = TestClient() + self.admin_user = User(username='admin', is_staff=True) + self.admin_user.save() + self.admin_client.login_user(self.admin_user) + + self.client = TestClient() + self.user = User(username='user', is_staff=False) + self.user.save() + self.client.login_user(self.user) + + def test_purge_user_data(self): + """ + Make sure purging user data works + """ + + response = self.admin_client.post( + reverse('edx_notifications.admin.delete_users_data'), + {'user_ids': [self.admin_user.id]} + ) + self.assertEqual(response.status_code, 204) + + def test_admin_required_to_purge(self): + """ + Make sure purging is only available to staff users + """ + + response = self.client.post( + reverse('edx_notifications.admin.delete_users_data'), + {'user_ids': [self.admin_user.id]} + ) + self.assertEqual(response.status_code, 403) diff --git a/edx_notifications/server/api/url_regex.py b/edx_notifications/server/api/url_regex.py index 6551ed6a..ae494877 100644 --- a/edx_notifications/server/api/url_regex.py +++ b/edx_notifications/server/api/url_regex.py @@ -12,3 +12,5 @@ CONSUMER_USER_PREFERENCES_DETAIL_REGEX = r'edx_notifications/v1/consumer/user_preferences/(?P[0-9A-Za-z-]+)$' CONSUMER_USER_PREFERENCES_DETAIL_NO_PARAM_REGEX = r'edx_notifications/v1/consumer/user_preferences/$' CONSUMER_RENDERERS_TEMPLATES_REGEX = r'edx_notifications/v1/consumer/renderers/templates$' + +ADMIN_USERS_DELETE = r'edx_notifications/v1/admin/user/delete$' diff --git a/edx_notifications/server/api/urls.py b/edx_notifications/server/api/urls.py index 009a7757..f50fe6b0 100644 --- a/edx_notifications/server/api/urls.py +++ b/edx_notifications/server/api/urls.py @@ -2,11 +2,9 @@ All URL mappings for HTTP-based APIs """ from django.conf.urls import patterns, url - from rest_framework.urlpatterns import format_suffix_patterns -from edx_notifications.server.api import consumer as consumer_views - +from edx_notifications.server.api import consumer as consumer_views, admin as admin_views from .url_regex import ( CONSUMER_NOTIFICATIONS_COUNT_REGEX, CONSUMER_NOTIFICATION_DETAIL_REGEX, @@ -17,8 +15,9 @@ CONSUMER_USER_PREFERENCES_DETAIL_REGEX, CONSUMER_NOTIFICATIONS_PREFERENCES_REGEX, CONSUMER_USER_PREFERENCES_REGEX, - CONSUMER_USER_PREFERENCES_DETAIL_NO_PARAM_REGEX) - + CONSUMER_USER_PREFERENCES_DETAIL_NO_PARAM_REGEX, + ADMIN_USERS_DELETE +) urlpatterns = patterns( # pylint: disable=invalid-name '', @@ -72,6 +71,11 @@ consumer_views.UserPreferenceDetail.as_view(), name='edx_notifications.consumer.user_preferences.detail.no_param' ), + url( + ADMIN_USERS_DELETE, + admin_views.DeleteUsersData.as_view(), + name='edx_notifications.admin.delete_users_data' + ), ) diff --git a/edx_notifications/server/api/urls_mock.py b/edx_notifications/server/api/urls_mock.py index e12d8155..5c4ea261 100644 --- a/edx_notifications/server/api/urls_mock.py +++ b/edx_notifications/server/api/urls_mock.py @@ -17,6 +17,7 @@ CONSUMER_NOTIFICATIONS_PREFERENCES_REGEX, CONSUMER_USER_PREFERENCES_REGEX, CONSUMER_USER_PREFERENCES_DETAIL_NO_PARAM_REGEX, + ADMIN_USERS_DELETE ) @@ -79,4 +80,9 @@ def mock_handler(request): # pylint: disable=unused-argument mock_handler, name='edx_notifications.consumer.user_preferences.detail.no_param' ), + url( + ADMIN_USERS_DELETE, + mock_handler, + name='edx_notifications.admin.delete_user_notifications' + ), ) diff --git a/edx_notifications/stores/sql/store_provider.py b/edx_notifications/stores/sql/store_provider.py index 5d2b17d4..6fc0ce7c 100644 --- a/edx_notifications/stores/sql/store_provider.py +++ b/edx_notifications/stores/sql/store_provider.py @@ -20,7 +20,10 @@ SQLNotificationType, SQLUserNotification, SQLNotificationCallbackTimer, - SQLNotificationPreference, SQLUserNotificationPreferences) + SQLNotificationPreference, + SQLUserNotificationPreferences, + SQLUserNotificationArchive +) class SQLNotificationStoreProvider(BaseNotificationStoreProvider): @@ -554,3 +557,20 @@ def get_all_namespaces(self, start_datetime=None, end_datetime=None): result_set = result_set.values_list('namespace', flat=True).order_by('namespace').distinct() return result_set + + def purge_notifications_for_users(self, user_ids): + """ + This will remove all notifications and preferences for given user IDs + """ + user_notifications = SQLUserNotification.objects.filter(user_id__in=user_ids) + archived_notifications = SQLUserNotificationArchive.objects.filter(user_id__in=user_ids) + SQLNotificationMessage.objects.filter(id__in=user_notifications.values('msg_id')).delete() + SQLNotificationMessage.objects.filter(id__in=archived_notifications.values('msg_id')).delete() + SQLUserNotificationArchive.objects.filter(user_id__in=user_ids).delete() + SQLUserNotificationPreferences.objects.filter(user_id__in=user_ids).delete() + + def purge_notifications_containing(self, payload): + """ + This will remove all notifications which contains the given payload + """ + SQLNotificationMessage.objects.filter(payload__contains=payload).delete() diff --git a/setup.py b/setup.py index 362f01de..130f3ad2 100755 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ def load_requirements(*requirements_paths): setup( name='edx-notifications', - version='0.8.3', + version='0.9.0', description='Notification subsystem for Open edX', long_description=open('README.md').read(), author='edX',