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

[API] Added REST API for Notifications #48

Merged
merged 2 commits into from
Jun 26, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ before_install:
- pip install -U pip wheel
- pip install $DJANGO
- pip install -U -r requirements-test.txt
# temporary: remove when openwisp-utils 0.5.0 is released
- pip install -U https://github.com/openwisp/openwisp-utils/tarball/master#egg=openwisp_utils[qa,rest]
# temporary: remove when openwisp-users with API support is released
- pip install -U https://github.com/openwisp/openwisp-users/tarball/master#egg=openwisp_users[rest]

install:
- python setup.py -q develop
Expand Down
122 changes: 122 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,33 @@ Setup (integrate into an existing Django project)
'allauth',
'allauth.account',
'allauth.socialaccount',
# rest framework
'rest_framework',
'rest_framework.authtoken',
'drf_yasg',
'django_filters',
'openwisp_users',
'django.contrib.admin',
# notifications module
'openwisp_notifications',
]

``urls.py``:

.. code-block:: python

from django.contrib import admin
from django.urls import include, path
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include(('openwisp_users.api.urls', 'users'), namespace='users')),
path('', include('openwisp_notifications.urls', namespace='openwisp_notifications')),
]

urlpatterns += staticfiles_urlpatterns()

Configure caching (you may use a different cache storage if you want):

.. code-block:: python
Expand Down Expand Up @@ -379,6 +400,90 @@ This setting takes the URL of the logo to be displayed on email notification.
**Note**: Provide a URL which points to the logo on your own web server. Ensure that the URL provided is
publicly accessible from the internet. Otherwise, the logo may not be displayed in email.

REST API
--------

Live documentation
~~~~~~~~~~~~~~~~~~

.. image:: https://github.com/openwisp/openwisp-notifications/blob/master/docs/images/api-docs.png

A general live API documentation (following the OpenAPI specification) is available at ``/api/v1/docs/``.

Browsable web interface
~~~~~~~~~~~~~~~~~~~~~~~

.. image:: https://github.com/openwisp/openwisp-notifications/blob/master/docs/images/api-ui.png

Additionally, opening any of the endpoints `listed below <#list-of-endpoints>`_
directly in the browser will show the `browsable API interface of Django-REST-Framework
<https://www.django-rest-framework.org/topics/browsable-api/>`_,
which makes it even easier to find out the details of each endpoint.

Authentication
~~~~~~~~~~~~~~

See openwisp-users: `authenticating with the user token
<https://github.com/openwisp/openwisp-users#authenticating-with-the-user-token>`_.

When browsing the API via the `Live documentation <#live-documentation>`_
or the `Browsable web interface <#browsable-web-interface>`_, you can use
the session authentication by logging in the django admin.

Pagination
~~~~~~~~~~

The *list* endpoint support the ``page_size`` parameter that allows paginating
the results in conjunction with the ``page`` parameter.

.. code-block:: text

GET /api/v1/notifications/?page_size=10
GET api/v1/notifications/?page_size=10&page=2

List of endpoints
~~~~~~~~~~~~~~~~~

Since the detailed explanation is contained in the `Live documentation <#live-documentation>`_
and in the `Browsable web page <#browsable-web-interface>`_ of each endpoint,
here we'll provide just a list of the available endpoints,
for further information please open the URL of the endpoint in your browser.

List user's notifications
#########################

.. code-block:: text

GET /api/v1/notifications/

Mark all user's notifications read
##################################

.. code-block:: text

POST /api/v1/notifications/read/

Get notification details
########################

.. code-block:: text

GET /api/v1/notifications/{pk}/

Mark a notification read
########################

.. code-block:: text

PATCH /api/v1/notifications/{pk}/

Delete a notification
#####################

.. code-block:: text

DELETE /api/v1/notifications/{pk}/

Installing for development
--------------------------

Expand Down Expand Up @@ -731,6 +836,23 @@ to find out how to do this.
**Note**: Some tests will fail if ``templatetags`` and ``admin/base.html`` are not configured properly.
See preceeding sections to configure them properly.

Other base classes that can be inherited and extended
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The following steps are not required and are intended for more advanced customization.

API views
#########

The API view classes can be extended into other django applications as well. Note
that it is not required for extending openwisp-notifications to your app and this change
is required only if you plan to make changes to the API views.

Create a view file as done in `sample_notifications/views.py <https://github.com/openwisp/openwisp-notifications/blob/master/tests/openwisp2/sample_notifications/views.py>`_

For more information regarding Django REST Framework API views, please refer to the
`"Generic views" section in the Django REST Framework documentation <https://www.django-rest-framework.org/api-guide/generic-views/>`_.

Contributing
------------

Expand Down
Binary file added docs/images/api-docs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/api-ui.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
40 changes: 40 additions & 0 deletions openwisp_notifications/api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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 ContentTypeField(serializers.Field):
def to_representation(self, obj):
return obj.model


class NotificationSerializer(serializers.ModelSerializer):
target_object_url = serializers.SerializerMethodField()
actor_content_type = ContentTypeField(read_only=True)
target_content_type = ContentTypeField(read_only=True)
action_object_content_type = ContentTypeField(read_only=True)

class Meta:
model = Notification
exclude = ['description', 'deleted', 'public']
extra_fields = ['message', 'email_subject', 'target_object_url']

def get_field_names(self, declared_fields, info):
model_fields = super().get_field_names(declared_fields, info)
return model_fields + self.Meta.extra_fields

def get_target_object_url(self, obj):
url = reverse(
f'admin:{Notification._meta.app_label}_{Notification._meta.model_name}_change',
args=(obj.id,),
)
return _get_absolute_url(url)


class NotificationListSerializer(NotificationSerializer):
class Meta(NotificationSerializer.Meta):
fields = ['id', 'message', 'unread', 'target_object_url', 'email_subject']
exclude = None
14 changes: 14 additions & 0 deletions openwisp_notifications/api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from django.urls import path
from openwisp_notifications.api import views

app_name = 'openwisp_notifications'


def get_api_urls(api_views=None):
if not api_views:
api_views = views
return [
path('', views.notifications_list, name='notifications_list'),
path('read/', views.notifications_read_all, name='notifications_read_all'),
path('<uuid:pk>/', views.notification_detail, name='notification_detail'),
]
69 changes: 69 additions & 0 deletions openwisp_notifications/api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from django_filters.rest_framework import DjangoFilterBackend
from openwisp_notifications.api.serializers import (
NotificationListSerializer,
NotificationSerializer,
)
from openwisp_notifications.swapper import load_model
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
from rest_framework.generics import GenericAPIView, RetrieveDestroyAPIView
from rest_framework.mixins import ListModelMixin
from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from openwisp_users.api.authentication import BearerAuthentication

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 = [BearerAuthentication, SessionAuthentication]
permission_classes = [IsAuthenticated]
queryset = Notification.objects.all()

def get_queryset(self):
return self.queryset.filter(recipient=self.request.user)


class NotificationListView(BaseNotificationView, ListModelMixin):
serializer_class = NotificationListSerializer
pagination_class = NotificationPaginator
filter_backends = [DjangoFilterBackend]
filterset_fields = ['unread']

def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)


class NotificationDetailView(BaseNotificationView, RetrieveDestroyAPIView):
serializer_class = NotificationSerializer
lookup_field = 'pk'

def patch(self, request, pk):
return self._mark_notification_read(pk)

def _mark_notification_read(self, notification_id):
notification = self.get_object()
notification.mark_as_read()
return Response(status=status.HTTP_200_OK,)


class NotificationReadAllView(BaseNotificationView):
def post(self, request, *args, **kwargs):
queryset = self.get_queryset()
queryset.update(unread=False)
Notification.invalidate_cache(request.user)
return Response(status=status.HTTP_200_OK)


notifications_list = NotificationListView.as_view()
notification_detail = NotificationDetailView.as_view()
notifications_read_all = NotificationReadAllView.as_view()
Loading