-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[api] Added API endpoints for notifications
Added API endpoint for listing and marking notifications as read.
- Loading branch information
Showing
15 changed files
with
421 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from django.urls import reverse | ||
from openwisp_notifications.swapper import load_model | ||
from openwisp_notifications.utils import _get_absolute_url | ||
from rest_framework import serializers | ||
|
||
Notification = load_model('Notification') | ||
|
||
|
||
class NotificationSerializer(serializers.ModelSerializer): | ||
notification_url = serializers.SerializerMethodField() | ||
|
||
class Meta: | ||
model = Notification | ||
fields = ['id', 'message', 'unread', 'notification_url', 'email_subject'] | ||
|
||
def get_notification_url(self, obj): | ||
url = reverse( | ||
f'admin:{Notification._meta.app_label}_{Notification._meta.model_name}_change', | ||
args=(obj.id,), | ||
) | ||
return _get_absolute_url(url) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
from django.conf.urls import url | ||
from openwisp_notifications.api import views | ||
|
||
app_name = 'openwisp_notifications' | ||
|
||
urlpatterns = [ | ||
url( | ||
r'^openwisp_notifications/notifications/$', | ||
views.list_notifications, | ||
name='api_list_notifications', | ||
), | ||
url( | ||
r'^openwisp_notifications/notifications/(?P<notification_id>[0-9A-Fa-f-]+)/$', | ||
views.read_notification, | ||
name='api_read_notifications', | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import uuid | ||
|
||
from django.utils.translation import gettext_lazy as _ | ||
from openwisp_notifications.api.serializers import NotificationSerializer | ||
from openwisp_notifications.swapper import load_model | ||
from rest_framework import status | ||
from rest_framework.authentication import BasicAuthentication, SessionAuthentication | ||
from rest_framework.generics import GenericAPIView | ||
from rest_framework.mixins import ListModelMixin | ||
from rest_framework.pagination import PageNumberPagination | ||
from rest_framework.permissions import DjangoModelPermissions | ||
from rest_framework.response import Response | ||
|
||
Notification = load_model('Notification') | ||
|
||
|
||
class NotificationPaginator(PageNumberPagination): | ||
page_size = 10 | ||
page_size_query_param = 'page_size' | ||
max_page_size = 100 | ||
|
||
|
||
class BaseNotificationView(GenericAPIView): | ||
model = Notification | ||
authentication_classes = [SessionAuthentication, BasicAuthentication] | ||
permission_classes = [DjangoModelPermissions] | ||
queryset = Notification.objects.all() | ||
serializer_class = NotificationSerializer | ||
|
||
def get_queryset(self): | ||
return self.queryset.filter(recipient=self.request.user) | ||
|
||
|
||
class NotificationListView(BaseNotificationView, ListModelMixin): | ||
pagination_class = NotificationPaginator | ||
|
||
def get(self, request, *args, **kwargs): | ||
return self.list(request, *args, **kwargs) | ||
|
||
|
||
class NotificationReadView(BaseNotificationView): | ||
def patch(self, request, notification_id): | ||
return self._mark_notification_read(notification_id) | ||
|
||
def _mark_notification_read(self, notification_id): | ||
queryset = self.get_queryset() | ||
try: | ||
notification_id = uuid.UUID(notification_id) | ||
notification = queryset.get(id=notification_id) | ||
except ValueError as e: | ||
data = {'error': _(str(e))} | ||
status_code = status.HTTP_400_BAD_REQUEST | ||
except Notification.DoesNotExist as e: | ||
data = {'error': _(str(e))} | ||
status_code = status.HTTP_404_NOT_FOUND | ||
else: | ||
notification.mark_as_read() | ||
data = {'detail': _('Notification marked as read.')} | ||
status_code = status.HTTP_200_OK | ||
return Response(data, status_code) | ||
|
||
|
||
list_notifications = NotificationListView.as_view() | ||
read_notification = NotificationReadView.as_view() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import uuid | ||
|
||
from django.test import TestCase | ||
from django.urls import reverse | ||
from openwisp_notifications.signals import notify | ||
from openwisp_notifications.swapper import load_model | ||
|
||
from openwisp_users.tests.utils import TestOrganizationMixin | ||
|
||
Notification = load_model('Notification') | ||
|
||
|
||
class TestNotificationApi(TestCase, TestOrganizationMixin): | ||
app_label = 'openwisp_notifications' | ||
|
||
def setUp(self): | ||
self.admin = self._get_admin(self) | ||
self.client.force_login(self.admin) | ||
|
||
def test_list_notification_api(self): | ||
number_of_notifications = 20 | ||
url = reverse(f'{self.app_label}:api_list_notifications') | ||
for _ in range(number_of_notifications): | ||
notify.send(sender=self.admin, type='default', target=self.admin) | ||
|
||
with self.subTest('Test page query in notification list view'): | ||
response = self.client.get(url) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertEqual(response.data['count'], number_of_notifications) | ||
self.assertEqual( | ||
response.data['next'], | ||
f'http://testserver/api/v1/{self.app_label}/notifications/?page=2', | ||
) | ||
self.assertEqual(response.data['previous'], None) | ||
self.assertEqual(len(response.data['results']), 10) | ||
|
||
next_response = self.client.get(response.data['next']) | ||
self.assertEqual(next_response.status_code, 200) | ||
self.assertEqual(next_response.data['count'], number_of_notifications) | ||
self.assertEqual( | ||
next_response.data['next'], None, | ||
) | ||
self.assertEqual( | ||
next_response.data['previous'], | ||
f'http://testserver/api/v1/{self.app_label}/notifications/', | ||
) | ||
self.assertEqual(len(next_response.data['results']), 10) | ||
|
||
with self.subTest('Test page_size query'): | ||
page_size = 5 | ||
url = f'{url}?page_size={page_size}' | ||
response = self.client.get(url) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertEqual(response.data['count'], number_of_notifications) | ||
self.assertEqual( | ||
response.data['next'], | ||
f'http://testserver/api/v1/{self.app_label}/notifications/' | ||
f'?page=2&page_size={page_size}', | ||
) | ||
self.assertEqual(response.data['previous'], None) | ||
self.assertEqual(len(response.data['results']), page_size) | ||
|
||
next_response = self.client.get(response.data['next']) | ||
self.assertEqual(next_response.status_code, 200) | ||
self.assertEqual(next_response.data['count'], number_of_notifications) | ||
self.assertEqual( | ||
next_response.data['next'], | ||
f'http://testserver/api/v1/{self.app_label}/notifications/' | ||
f'?page=3&page_size={page_size}', | ||
) | ||
self.assertEqual( | ||
next_response.data['previous'], | ||
f'http://testserver/api/v1/{self.app_label}/notifications/' | ||
f'?page_size={page_size}', | ||
) | ||
self.assertEqual(len(next_response.data['results']), page_size) | ||
|
||
with self.subTest('Test individual result object'): | ||
response = self.client.get(url) | ||
self.assertEqual(response.status_code, 200) | ||
n = response.data['results'][0] | ||
self.assertIn('id', n) | ||
self.assertIn('message', n) | ||
self.assertTrue(n['unread']) | ||
self.assertIn('notification_url', n) | ||
self.assertEqual( | ||
n['email_subject'], '[example.com] Default Notification Subject' | ||
) | ||
|
||
def test_read_notification_api(self): | ||
notify.send(sender=self.admin, type='default', target=self.admin) | ||
n = Notification.objects.first() | ||
self.assertEqual(n.unread, True) | ||
|
||
with self.subTest('Test for invalid UUID'): | ||
url = reverse(f'{self.app_label}:api_read_notifications', args=(0,)) | ||
response = self.client.patch(url) | ||
self.assertEqual(response.status_code, 400) | ||
self.assertDictEqual( | ||
response.data, {'error': 'badly formed hexadecimal UUID string'} | ||
) | ||
|
||
with self.subTest('Test for non-existent notification'): | ||
url = reverse( | ||
f'{self.app_label}:api_read_notifications', args=(uuid.uuid4(),) | ||
) | ||
response = self.client.patch(url) | ||
self.assertEqual(response.status_code, 404) | ||
self.assertDictEqual( | ||
response.data, {'error': 'Notification matching query does not exist.'} | ||
) | ||
|
||
with self.subTest('Test for valid notificaton'): | ||
url = reverse(f'{self.app_label}:api_read_notifications', args=(n.id,)) | ||
response = self.client.patch(url) | ||
self.assertEqual(response.status_code, 200) | ||
self.assertDictEqual( | ||
response.data, {'detail': 'Notification marked as read.'} | ||
) | ||
n = Notification.objects.first() | ||
self.assertEqual(n.unread, False) | ||
|
||
def test_annonymous_user(self): | ||
self.client.logout() | ||
|
||
with self.subTest('Test for list notifications api'): | ||
url = reverse(f'{self.app_label}:api_list_notifications') | ||
response = self.client.get(url) | ||
self.assertEqual(response.status_code, 403) | ||
self.assertIn('detail', response.data) | ||
|
||
with self.subTest('Test for read notifications api'): | ||
url = reverse(f'{self.app_label}:api_read_notifications', args=(0,)) | ||
response = self.client.get(url) | ||
self.assertEqual(response.status_code, 403) | ||
self.assertIn('detail', response.data) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from django.conf.urls import include, url | ||
|
||
urlpatterns = [ | ||
url( | ||
r'^api/v1/', | ||
include('openwisp_notifications.api.urls', namespace='openwisp_notifications'), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.