Skip to content

Commit 7e5b806

Browse files
committed
Django REST Framework v3 support.
1 parent 09e3fb2 commit 7e5b806

File tree

7 files changed

+211
-2
lines changed

7 files changed

+211
-2
lines changed

README.rst

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ Django 1.8 is required. Support for older versions is available in the release 1
2424

2525
Tastypie support should work on Tastypie 0.11.0 and newer.
2626

27+
Django REST Framework support should work on DRF version 3.0 and newer.
28+
2729
Setup
2830
-----
2931
You can install the library directly from pypi using pip:
@@ -142,7 +144,7 @@ Exceptions
142144
Tastypie support
143145
----------------
144146

145-
The app includes tastypie-compatible resources in push_notifications.api. These can be used as-is, or as base classes
147+
The app includes tastypie-compatible resources in push_notifications.api.tastypie. These can be used as-is, or as base classes
146148
for more involved APIs.
147149
The following resources are available:
148150

@@ -157,6 +159,57 @@ Subclassing the authenticated resources in order to add a ``SameUserAuthenticati
157159

158160
When registered, the APIs will show up at ``<api_root>/device/apns`` and ``<api_root>/device/gcm``, respectively.
159161

162+
Django REST Framework (DRF) support
163+
-----------------------------------
164+
165+
ViewSets are available for both APNS and GCM devices in two permission flavors:
166+
167+
- ``APNSDeviceViewSet`` and ``GCMDeviceViewSet``
168+
169+
- Permissions as specified in settings (``AllowAny`` by default, which is not recommended)
170+
- A device may be registered without associating it with a user
171+
172+
- ``APNSDeviceAuthorizedViewSet`` and ``GCMDeviceAuthorizedViewSet``
173+
174+
- Permissions are ``IsAuthenticated`` and custom permission ``IsOwner``, which will only allow the ``request.user`` to get and update devices that belong to that user
175+
- Requires a user to be authenticated, so all devices will be associated with a user
176+
177+
When creating an ``APNSDevice``, the ``registration_id`` is validated to be a 64-character hexadecimal string.
178+
179+
Routes can be added one of two ways:
180+
181+
- Routers_ (include all views)
182+
.. _Routers: http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers#using-routers
183+
184+
::
185+
186+
from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet, GCMDeviceAuthorizedViewSet
187+
from rest_framework.routers import DefaultRouter
188+
189+
router = DefaultRouter()
190+
router.register(r'device/apns', APNSDeviceAuthorizedViewSet)
191+
router.register(r'device/gcm', GCMDeviceAuthorizedViewSet)
192+
193+
urlpatterns = patterns('',
194+
# URLs will show up at <api_root>/device/apns
195+
url(r'^', include(router.urls)),
196+
# ...
197+
)
198+
199+
- Using as_view_ (specify which views to include)
200+
.. _as_view: http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers#binding-viewsets-to-urls-explicitly
201+
202+
::
203+
204+
from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet
205+
206+
urlpatterns = patterns('',
207+
# Only allow creation of devices by authenticated users
208+
url(r'^device/apns/?$', APNSDeviceAuthorizedViewSet.as_view({'post': 'create'}), name='create_apns_device'),
209+
# ...
210+
)
211+
212+
160213
Python 3 support
161214
----------------
162215

push_notifications/api/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.conf import settings
2+
3+
if "tastypie" in settings.INSTALLED_APPS:
4+
# Tastypie resources are importable from the api package level (backwards compatibility)
5+
from .tastypie import APNSDeviceResource, GCMDeviceResource, APNSDeviceAuthenticatedResource, GCMDeviceAuthenticatedResource
6+
7+
__all__ = [
8+
"APNSDeviceResource",
9+
"GCMDeviceResource",
10+
"APNSDeviceAuthenticatedResource",
11+
"GCMDeviceAuthenticatedResource"
12+
]
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from __future__ import absolute_import
2+
3+
from rest_framework import permissions
4+
from rest_framework.serializers import ModelSerializer, ValidationError
5+
from rest_framework.viewsets import ModelViewSet
6+
from rest_framework.fields import IntegerField
7+
8+
from push_notifications.models import APNSDevice, GCMDevice
9+
from push_notifications.fields import hex_re
10+
11+
12+
# Fields
13+
14+
class HexIntegerField(IntegerField):
15+
"""
16+
Store an integer represented as a hex string of form "0x01".
17+
"""
18+
19+
def to_internal_value(self, data):
20+
data = int(data, 16)
21+
return super(HexIntegerField, self).to_internal_value(data)
22+
23+
def to_representation(self, value):
24+
return value
25+
26+
27+
# Serializers
28+
class DeviceSerializerMixin(ModelSerializer):
29+
class Meta:
30+
fields = ("name", "registration_id", "device_id", "active", "date_created")
31+
read_only_fields = ("date_created", )
32+
33+
34+
class APNSDeviceSerializer(ModelSerializer):
35+
36+
class Meta(DeviceSerializerMixin.Meta):
37+
model = APNSDevice
38+
39+
def validate_registration_id(self, value):
40+
# iOS device tokens are 256-bit hexadecimal (64 characters)
41+
42+
if hex_re.match(value) is None or len(value) != 64:
43+
raise ValidationError("Registration ID (device token) is invalid")
44+
45+
return value
46+
47+
48+
class GCMDeviceSerializer(ModelSerializer):
49+
device_id = HexIntegerField(
50+
help_text="ANDROID_ID / TelephonyManager.getDeviceId() (e.g: 0x01)",
51+
style={'input_type': 'text'},
52+
required=False
53+
)
54+
55+
class Meta(DeviceSerializerMixin.Meta):
56+
model = GCMDevice
57+
58+
59+
# Permissions
60+
class IsOwner(permissions.BasePermission):
61+
def has_object_permission(self, request, view, obj):
62+
# must be the owner to view the object
63+
return obj.user == request.user
64+
65+
66+
# Mixins
67+
class DeviceViewSetMixin(object):
68+
lookup_field = "registration_id"
69+
70+
def perform_create(self, serializer):
71+
if self.request.user.is_authenticated():
72+
serializer.save(user=self.request.user)
73+
74+
75+
class AuthorizedMixin(object):
76+
permission_classes = (permissions.IsAuthenticated, IsOwner)
77+
78+
def get_queryset(self):
79+
# filter all devices to only those belonging to the current user
80+
return self.queryset.filter(user=self.request.user)
81+
82+
83+
# ViewSets
84+
class APNSDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
85+
queryset = APNSDevice.objects.all()
86+
serializer_class = APNSDeviceSerializer
87+
88+
89+
class APNSDeviceAuthorizedViewSet(AuthorizedMixin, APNSDeviceViewSet):
90+
pass
91+
92+
93+
class GCMDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
94+
queryset = GCMDevice.objects.all()
95+
serializer_class = GCMDeviceSerializer
96+
97+
98+
class GCMDeviceAuthorizedViewSet(AuthorizedMixin, GCMDeviceViewSet):
99+
pass
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from tastypie.authorization import Authorization
22
from tastypie.authentication import BasicAuthentication
33
from tastypie.resources import ModelResource
4-
from .models import APNSDevice, GCMDevice
4+
from push_notifications.models import APNSDevice, GCMDevice
55

66

77
class APNSDeviceResource(ModelResource):

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
name="django-push-notifications",
2828
packages=[
2929
"push_notifications",
30+
"push_notifications/api",
3031
"push_notifications/migrations",
3132
"push_notifications/management",
3233
"push_notifications/management/commands",

tests/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,11 @@
22
from test_gcm_push_payload import *
33
from test_apns_push_payload import *
44
from test_management_commands import *
5+
6+
# conditionally test rest_framework api if the DRF package is installed
7+
try:
8+
import rest_framework
9+
except ImportError:
10+
pass
11+
else:
12+
from test_rest_framework import *

tests/test_rest_framework.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import json
2+
import mock
3+
from django.test import TestCase
4+
from django.utils import timezone
5+
from push_notifications.models import GCMDevice, APNSDevice
6+
from tests.mock_responses import GCM_PLAIN_RESPONSE, GCM_MULTIPLE_JSON_RESPONSE
7+
from push_notifications.api.rest_framework import APNSDeviceSerializer, GCMDeviceSerializer
8+
9+
class APNSDeviceSerializerTestCase(TestCase):
10+
def test_validation(self):
11+
# valid data - upper case
12+
serializer = APNSDeviceSerializer(data={
13+
"registration_id": "AEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAEAE",
14+
"name": "Apple iPhone 6+",
15+
"device_id": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
16+
})
17+
self.assertTrue(serializer.is_valid())
18+
19+
# valid data - lower case
20+
serializer = APNSDeviceSerializer(data={
21+
"registration_id": "aeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeaeae",
22+
"name": "Apple iPhone 6+",
23+
"device_id": "ffffffffffffffffffffffffffffffff",
24+
})
25+
self.assertTrue(serializer.is_valid())
26+
27+
# invalid data - device_id, registration_id
28+
serializer = APNSDeviceSerializer(data={
29+
"registration_id": "invalid device token contains no hex",
30+
"name": "Apple iPhone 6+",
31+
"device_id": "ffffffffffffffffffffffffffffake",
32+
})
33+
self.assertFalse(serializer.is_valid())
34+
self.assertEqual(serializer.errors["device_id"][0], '"ffffffffffffffffffffffffffffake" is not a valid UUID.')
35+
self.assertEqual(serializer.errors["registration_id"][0], "Registration ID (device token) is invalid")
36+

0 commit comments

Comments
 (0)