From bdbb779dbc8e4899056fdde3ebdc9720503cc9f2 Mon Sep 17 00:00:00 2001 From: Chathan Driehuys Date: Fri, 11 May 2018 14:29:54 -0400 Subject: [PATCH 1/3] Add django-push-notifications. --- km_api/km_api/settings.py | 1 + requirements/base.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/km_api/km_api/settings.py b/km_api/km_api/settings.py index a25327fa..224e0d98 100644 --- a/km_api/km_api/settings.py +++ b/km_api/km_api/settings.py @@ -44,6 +44,7 @@ 'corsheaders', 'django_filters', 'dry_rest_permissions', + 'push_notifications', 'raven.contrib.django.raven_compat', 'rest_email_auth', 'rest_framework', diff --git a/requirements/base.txt b/requirements/base.txt index 653f9b5b..93e22146 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,6 +6,7 @@ boto3 == 1.6.8 coreapi == 2.3.3 django-cors-headers == 2.2.0 django-filter == 1.1.0 +django-push-notifications == 1.6.0 django-rest-email-auth == 1.0.0 django-ses == 0.8.5 django-solo == 1.1.3 From 082e281873ecb30b78390c3cf7e2ce30c64c0145 Mon Sep 17 00:00:00 2001 From: Chathan Driehuys Date: Fri, 11 May 2018 17:19:21 -0400 Subject: [PATCH 2/3] Add view for listing/creating APNS devices. The view allows users to register new APNS devices and list existing devices. --- km_api/conftest.py | 8 +++ km_api/factories.py | 11 +++ km_api/km_api/settings.py | 1 + km_api/km_api/urls.py | 1 + km_api/notifications/__init__.py | 0 km_api/notifications/tests/__init__.py | 0 .../tests/integration/__init__.py | 0 .../integration/test_apns_device_list_view.py | 68 +++++++++++++++++++ km_api/notifications/tests/views/__init__.py | 0 .../tests/views/test_apns_device_list_view.py | 66 ++++++++++++++++++ km_api/notifications/urls.py | 16 +++++ km_api/notifications/views.py | 30 ++++++++ 12 files changed, 201 insertions(+) create mode 100644 km_api/notifications/__init__.py create mode 100644 km_api/notifications/tests/__init__.py create mode 100644 km_api/notifications/tests/integration/__init__.py create mode 100644 km_api/notifications/tests/integration/test_apns_device_list_view.py create mode 100644 km_api/notifications/tests/views/__init__.py create mode 100644 km_api/notifications/tests/views/test_apns_device_list_view.py create mode 100644 km_api/notifications/urls.py create mode 100644 km_api/notifications/views.py diff --git a/km_api/conftest.py b/km_api/conftest.py index c7d3c0b4..b5aaeac6 100644 --- a/km_api/conftest.py +++ b/km_api/conftest.py @@ -85,6 +85,14 @@ def api_rf(): return UserAPIRequestFactory(user=AnonymousUser()) +@pytest.fixture +def apns_device_factory(db): + """ + Fixture to get the factory used to create APNS devices. + """ + return factories.APNSDeviceFactory + + @pytest.fixture def email_factory(db): """ diff --git a/km_api/factories.py b/km_api/factories.py index 220799ab..bb91a117 100644 --- a/km_api/factories.py +++ b/km_api/factories.py @@ -10,6 +10,17 @@ import factory +class APNSDeviceFactory(factory.django.DjangoModelFactory): + """ + Factory for generating APNS devices. + """ + user = factory.SubFactory('factories.UserFactory') + registration_id = factory.Sequence(lambda n: str(n)) + + class Meta: + model = 'push_notifications.APNSDevice' + + class EmailFactory(factory.django.DjangoModelFactory): """ Factory for generating ``EmailAddress`` instances. diff --git a/km_api/km_api/settings.py b/km_api/km_api/settings.py index 224e0d98..90c2169c 100644 --- a/km_api/km_api/settings.py +++ b/km_api/km_api/settings.py @@ -60,6 +60,7 @@ 'know_me.journal', 'know_me.profile', 'mailing_list', + 'notifications', ] MIDDLEWARE = [ diff --git a/km_api/km_api/urls.py b/km_api/km_api/urls.py index f8219a21..7ec82972 100644 --- a/km_api/km_api/urls.py +++ b/km_api/km_api/urls.py @@ -27,4 +27,5 @@ url(r'^auth/', include('km_auth.urls')), url(r'^docs/', include_docs_urls(title='Know Me API')), url(r'^know-me/', include('know_me.urls')), + url(r'^notifications/', include('notifications.urls')), ] diff --git a/km_api/notifications/__init__.py b/km_api/notifications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/km_api/notifications/tests/__init__.py b/km_api/notifications/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/km_api/notifications/tests/integration/__init__.py b/km_api/notifications/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/km_api/notifications/tests/integration/test_apns_device_list_view.py b/km_api/notifications/tests/integration/test_apns_device_list_view.py new file mode 100644 index 00000000..d0860f95 --- /dev/null +++ b/km_api/notifications/tests/integration/test_apns_device_list_view.py @@ -0,0 +1,68 @@ +import pytest + +from push_notifications.api.rest_framework import APNSDeviceSerializer + +from rest_framework import status +from rest_framework.reverse import reverse + + +url = reverse('notifications:apns-device-list') + + +@pytest.mark.integration +def test_get_apns_device_list( + api_client, + api_rf, + apns_device_factory, + user_factory): + """ + Sending a GET request to the view should return a list of the user's + registered APNS devices. + """ + user = user_factory() + api_client.force_authenticate(user=user) + api_rf.user = user + + apns_device_factory(user=user) + apns_device_factory(user=user) + + request = api_rf.get(url) + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + + serializer = APNSDeviceSerializer( + user.apnsdevice_set.all(), + context={'request': request}, + many=True, + ) + + assert response.data == serializer.data + + +@pytest.mark.integration +def test_post_new_apns_device(api_client, api_rf, user_factory): + """ + Sending a POST request to the view should create a new APNS device + attached to the requesting user. + """ + user = user_factory() + api_client.force_authenticate(user=user) + api_rf.user = user + + data = { + # The registration ID must be 64 or 200 characters + 'registration_id': '1' * 64, + } + + request = api_rf.post(url, data) + response = api_client.post(url, data) + + assert response.status_code == status.HTTP_201_CREATED + + serializer = APNSDeviceSerializer( + user.apnsdevice_set.get(), + context={'request': request}, + ) + + assert response.data == serializer.data diff --git a/km_api/notifications/tests/views/__init__.py b/km_api/notifications/tests/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/km_api/notifications/tests/views/test_apns_device_list_view.py b/km_api/notifications/tests/views/test_apns_device_list_view.py new file mode 100644 index 00000000..be14777c --- /dev/null +++ b/km_api/notifications/tests/views/test_apns_device_list_view.py @@ -0,0 +1,66 @@ +from unittest import mock + +from push_notifications.api.rest_framework import APNSDeviceSerializer + +from notifications import views + + +@mock.patch( + 'notifications.views.IsAuthenticated.has_permission', + autospec=True, +) +def test_check_permissions(mock_auth_perm): + """ + The view should require an authenticated user. + """ + view = views.APNSDeviceListView() + view.check_permissions(None) + + assert mock_auth_perm.call_count == 1 + + +def test_get_queryset(api_rf, apns_device_factory, user_factory): + """ + The view should operate on the devices owned by the user specified + in the view's keyword args. + """ + user = user_factory() + api_rf.user = user + + apns_device_factory(user=user) + apns_device_factory() + + view = views.APNSDeviceListView() + view.kwargs = {'pk': user.pk} + view.request = api_rf.get('/') + + assert list(view.get_queryset()) == list(user.apnsdevice_set.all()) + + +def test_get_serializer_class(): + """ + The view should use the APNS serializer provided by + django-push-notifications. + """ + view = views.APNSDeviceListView() + + assert view.get_serializer_class() == APNSDeviceSerializer + + +def test_perform_create(api_rf, user_factory): + """ + When creating a new APNS device, the view should attach the + requesting user to the device being created. + """ + user = user_factory() + api_rf.user = user + + view = views.APNSDeviceListView() + view.request = api_rf.post('/') + + serializer = mock.Mock(name='Mock Serializer') + + view.perform_create(serializer) + + assert serializer.save.call_count == 1 + assert serializer.save.call_args[1] == {'user': user} diff --git a/km_api/notifications/urls.py b/km_api/notifications/urls.py new file mode 100644 index 00000000..1e52f601 --- /dev/null +++ b/km_api/notifications/urls.py @@ -0,0 +1,16 @@ +from django.conf.urls import url + + +from notifications import views + + +app_name = 'notifications' + + +urlpatterns = [ + url( + r'^devices/apns/$', + views.APNSDeviceListView.as_view(), + name='apns-device-list', + ), +] diff --git a/km_api/notifications/views.py b/km_api/notifications/views.py new file mode 100644 index 00000000..ced4385d --- /dev/null +++ b/km_api/notifications/views.py @@ -0,0 +1,30 @@ +from push_notifications.api.rest_framework import APNSDeviceSerializer + +from rest_framework import generics +from rest_framework.permissions import IsAuthenticated + + +class APNSDeviceListView(generics.ListCreateAPIView): + """ + get: + List the specified user's registered APNS devices. + """ + permission_classes = (IsAuthenticated,) + serializer_class = APNSDeviceSerializer + + def get_queryset(self): + """ + Get a list of the requesting user's APNS devices. + """ + return self.request.user.apnsdevice_set.all() + + def perform_create(self, serializer): + """ + Create a new APNS device for the requesting user. + + Args: + serializer: + An instance of the view's serializer class containing + the data from the request. + """ + serializer.save(user=self.request.user) From ff3e344411929eb676a8313040dbdf23821a8d99 Mon Sep 17 00:00:00 2001 From: Chathan Driehuys Date: Fri, 11 May 2018 17:33:48 -0400 Subject: [PATCH 3/3] Use provided APNS view set. The view set provided by django-push-notifications does exactly what we need, so we can ditch the custom views. --- .../tests/views/test_apns_device_list_view.py | 66 ------------------- km_api/notifications/urls.py | 15 +++-- km_api/notifications/views.py | 30 --------- 3 files changed, 8 insertions(+), 103 deletions(-) delete mode 100644 km_api/notifications/tests/views/test_apns_device_list_view.py delete mode 100644 km_api/notifications/views.py diff --git a/km_api/notifications/tests/views/test_apns_device_list_view.py b/km_api/notifications/tests/views/test_apns_device_list_view.py deleted file mode 100644 index be14777c..00000000 --- a/km_api/notifications/tests/views/test_apns_device_list_view.py +++ /dev/null @@ -1,66 +0,0 @@ -from unittest import mock - -from push_notifications.api.rest_framework import APNSDeviceSerializer - -from notifications import views - - -@mock.patch( - 'notifications.views.IsAuthenticated.has_permission', - autospec=True, -) -def test_check_permissions(mock_auth_perm): - """ - The view should require an authenticated user. - """ - view = views.APNSDeviceListView() - view.check_permissions(None) - - assert mock_auth_perm.call_count == 1 - - -def test_get_queryset(api_rf, apns_device_factory, user_factory): - """ - The view should operate on the devices owned by the user specified - in the view's keyword args. - """ - user = user_factory() - api_rf.user = user - - apns_device_factory(user=user) - apns_device_factory() - - view = views.APNSDeviceListView() - view.kwargs = {'pk': user.pk} - view.request = api_rf.get('/') - - assert list(view.get_queryset()) == list(user.apnsdevice_set.all()) - - -def test_get_serializer_class(): - """ - The view should use the APNS serializer provided by - django-push-notifications. - """ - view = views.APNSDeviceListView() - - assert view.get_serializer_class() == APNSDeviceSerializer - - -def test_perform_create(api_rf, user_factory): - """ - When creating a new APNS device, the view should attach the - requesting user to the device being created. - """ - user = user_factory() - api_rf.user = user - - view = views.APNSDeviceListView() - view.request = api_rf.post('/') - - serializer = mock.Mock(name='Mock Serializer') - - view.perform_create(serializer) - - assert serializer.save.call_count == 1 - assert serializer.save.call_args[1] == {'user': user} diff --git a/km_api/notifications/urls.py b/km_api/notifications/urls.py index 1e52f601..1c28cfe7 100644 --- a/km_api/notifications/urls.py +++ b/km_api/notifications/urls.py @@ -1,16 +1,17 @@ -from django.conf.urls import url +from django.conf.urls import include, url +from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet -from notifications import views +from rest_framework.routers import DefaultRouter app_name = 'notifications' +router = DefaultRouter() +router.register('apns', APNSDeviceAuthorizedViewSet, 'apns-device') + + urlpatterns = [ - url( - r'^devices/apns/$', - views.APNSDeviceListView.as_view(), - name='apns-device-list', - ), + url(r'^', include(router.urls)), ] diff --git a/km_api/notifications/views.py b/km_api/notifications/views.py deleted file mode 100644 index ced4385d..00000000 --- a/km_api/notifications/views.py +++ /dev/null @@ -1,30 +0,0 @@ -from push_notifications.api.rest_framework import APNSDeviceSerializer - -from rest_framework import generics -from rest_framework.permissions import IsAuthenticated - - -class APNSDeviceListView(generics.ListCreateAPIView): - """ - get: - List the specified user's registered APNS devices. - """ - permission_classes = (IsAuthenticated,) - serializer_class = APNSDeviceSerializer - - def get_queryset(self): - """ - Get a list of the requesting user's APNS devices. - """ - return self.request.user.apnsdevice_set.all() - - def perform_create(self, serializer): - """ - Create a new APNS device for the requesting user. - - Args: - serializer: - An instance of the view's serializer class containing - the data from the request. - """ - serializer.save(user=self.request.user)