[Python-modules-commits] [python-django-push-notifications] 01/03: importing python-django-push-notifications_1.4.1.orig.tar.gz
Michael Fladischer
fladi at moszumanska.debian.org
Fri Mar 3 08:26:42 UTC 2017
This is an automated email from the git hooks/post-receive script.
fladi pushed a commit to branch master
in repository python-django-push-notifications.
commit 451eb3f965917f8aaa6f261c0131712b233ea0d7
Author: Michael Fladischer <FladischerMichael at fladi.at>
Date: Fri Mar 3 08:52:19 2017 +0100
importing python-django-push-notifications_1.4.1.orig.tar.gz
---
.gitignore | 17 ++
.travis.yml | 12 ++
AUTHORS | 44 ++++
CHANGELOG.rst | 101 +++++++++
CONTRIBUTING.md | 24 +++
LICENSE | 19 ++
MANIFEST.in | 3 +
README.rst | 219 +++++++++++++++++++
push_notifications/__init__.py | 8 +
push_notifications/admin.py | 81 +++++++
push_notifications/api/__init__.py | 12 ++
push_notifications/api/rest_framework.py | 127 +++++++++++
push_notifications/api/tastypie.py | 42 ++++
push_notifications/apns.py | 238 +++++++++++++++++++++
push_notifications/fields.py | 123 +++++++++++
push_notifications/gcm.py | 194 +++++++++++++++++
push_notifications/management/__init__.py | 0
push_notifications/management/commands/__init__.py | 0
.../management/commands/prune_devices.py | 16 ++
push_notifications/migrations/0001_initial.py | 48 +++++
.../migrations/0002_auto_20160106_0850.py | 20 ++
push_notifications/migrations/__init__.py | 0
push_notifications/models.py | 100 +++++++++
push_notifications/settings.py | 21 ++
requirements.txt | 1 +
setup.cfg | 2 +
setup.py | 40 ++++
tests/__init__.py | 12 ++
tests/mock_responses.py | 17 ++
tests/runtests.py | 57 +++++
tests/settings.py | 23 ++
tests/test_apns_push_payload.py | 31 +++
tests/test_gcm_push_payload.py | 28 +++
tests/test_management_commands.py | 25 +++
tests/test_models.py | 232 ++++++++++++++++++++
tests/test_rest_framework.py | 120 +++++++++++
tox.ini | 20 ++
37 files changed, 2077 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..38db1b0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,17 @@
+# python compiled
+__pycache__
+*.pyc
+
+# distutils
+MANIFEST
+build
+
+# IDE
+.idea
+*.iml
+
+# virtualenv
+.env
+
+# tox
+.tox
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..eb155b3
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,12 @@
+language: python
+addons:
+ apt:
+ sources:
+ - deadsnakes
+ packages:
+ - python3.5
+install:
+ - pip install tox
+script:
+ - tox
+sudo: false
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..037ef5e
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,44 @@
+This library was created by Jerome Leclanche <jerome at leclan.ch>, for use on the
+Anthill application (https://www.anthill.com).
+
+Special thanks to everyone who contributed:
+
+Adam "Cezar" Jenkins
+Alan Descoins
+Ales Dokhanin
+Alistair Broomhead
+Andrey Zevakin
+Antonin Lenfant
+Arthur Silva
+Avichal Pandey
+Brad Pitcher
+Daniel Kronovet
+David Pretty
+Dilvane Zanardine
+Florian Finke
+Florian Purchess
+Francois Lebel
+halak
+Innocenty Enikeew
+Jack Feng
+Jamaal Scarlett
+Jay Camp
+Jeremy Morgan
+Jerome Leclanche
+Julien Dubiel
+Lital Natan
+Luke Burden
+Marconi Moreto
+Matthew Hershberger
+Maxim Kamenkov
+Mohamad Nour Chawich
+Nicolas Delaby
+Remigiusz Dymecki
+Ruslan Kovtun
+Sander Heling
+Sergei Evdokimov
+Sujit Nair
+Thomas Iovine
+Valentin Hăloiu
+Wyan Jow
+ at hoongun
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
new file mode 100644
index 0000000..17a1580
--- /dev/null
+++ b/CHANGELOG.rst
@@ -0,0 +1,101 @@
+v1.4.1 (2016-01-11)
+===================
+* APNS: Increased max device token size to 100 bytes (WWDC 2015, iOS 9)
+* BUGFIX: Fix an index error in the admin
+
+v1.4.0 (2015-12-13)
+===================
+* BACKWARDS-INCOMPATIBLE: Drop support for Python<3.4
+* DJANGO: Support Django 1.9
+* GCM: Handle canonical IDs
+* GCM: Allow full range of GCMDevice.device_id values
+* GCM: Do not allow duplicate registration_ids
+* DRF: Work around empty boolean defaults issue (django-rest-framework#1101)
+* BUGFIX: Do not throw GCMError in bulk messages from the admin
+* BUGFIX: Avoid generating an extra migration on Python 3
+* BUGFIX: Only send in bulk to active devices
+* BUGFIX: Display models correctly in the admin on both Python 2 and 3
+
+
+v1.3.1 (2015-06-30)
+===================
+This is an errata release.
+
+v1.3.0 (2015-06-30)
+===================
+* BACKWARDS-INCOMPATIBLE: Drop support for Python<2.7
+* BACKWARDS-INCOMPATIBLE: Drop support for Django<1.8
+* NEW FEATURE: Added a Django Rest Framework API. Requires DRF>=3.0.
+* APNS: Add support for setting the ca_certs file with new APNS_CA_CERTIFICATES setting
+* GCM: Deactivate GCMDevices when their notifications cause NotRegistered or InvalidRegistration
+* GCM: Indiscriminately handle all keyword arguments in gcm_send_message and gcm_send_bulk_message
+* GCM: Never fall back to json in gcm_send_message
+* BUGFIX: Fixed migration issues from 1.2.0 upgrade.
+* BUGFIX: Better detection of SQLite/GIS MySQL in various checks
+* BUGFIX: Assorted Python 3 bugfixes
+* BUGFIX: Fix display of device_id in admin
+
+v1.2.1 (2015-04-11)
+===================
+* APNS, GCM: Add a db_index to the device_id field
+* APNS: Use the native UUIDField on Django 1.8
+* APNS: Fix timeout handling on Python 3
+* APNS: Restore error checking on apns_send_bulk_message
+* GCM: Expose the time_to_live argument in gcm_send_bulk_message
+* GCM: Fix return value when gcm bulk is split in batches
+* GCM: Improved error checking reliability
+* GCM: Properly pass kwargs in GCMDeviceQuerySet.send_message()
+* BUGFIX: Fix HexIntegerField for Django 1.3
+
+v1.2.0 (2014-10-07)
+===================
+* BACKWARDS-INCOMPATIBLE: Added support for Django 1.7 migrations. South users will have to upgrade to South 1.0 or Django 1.7.
+* APNS: APNS MAX_NOTIFICATION_SIZE is now a setting and its default has been increased to 2048
+* APNS: Always connect with TLSv1 instead of SSLv3
+* APNS: Implemented support for APNS Feedback Service
+* APNS: Support for optional "category" dict
+* GCM: Improved error handling in bulk mode
+* GCM: Added support for time_to_live parameter
+* BUGFIX: Fixed various issues relating HexIntegerField
+* BUGFIX: Fixed issues in the admin with custom user models
+
+v1.1.0 (2014-06-29)
+===================
+* BACKWARDS-INCOMPATIBLE: The arguments for device.send_message() have changed. See README.rst for details.
+* Added a date_created field to GCMDevice and APNSDevice. This field keeps track of when the Device was created.
+ This requires a `manage.py migrate`.
+* Updated APNS protocol support
+* Allow sending empty sounds on APNS
+* Several APNS bugfixes
+* Fixed BigIntegerField support on PostGIS
+* Assorted migrations bugfixes
+* Added a test suite
+
+v1.0.1 (2013-01-16)
+===================
+* Migrations have been reset. If you were using migrations pre-1.0 you should upgrade to 1.0 instead and only
+ upgrade to 1.0.1 when you are ready to reset your migrations.
+
+v1.0 (2013-01-15)
+=================
+* Full Python 3 support
+* GCM device_id is now a custom field based on BigIntegerField and always unsigned (it should be input as hex)
+* Django versions older than 1.5 now require 'six' to be installed
+* Drop uniqueness on gcm registration_id due to compatibility issues with MySQL
+* Fix some issues with migrations
+* Add some basic tests
+* Integrate with travis-ci
+* Add an AUTHORS file
+
+v0.9 (2013-12-17)
+=================
+
+* Enable installation with pip
+* Add wheel support
+* Add full documentation
+* Various bug fixes
+
+v0.8 (2013-03-15)
+=================
+
+* Initial release
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..d6adcc3
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,24 @@
+### Coding style
+
+Please adhere to the coding style throughout the project.
+
+1. Always use tabs. [Here](https://leclan.ch/tabs) is a short explanation why tabs are preferred.
+2. Always use double quotes for strings, unless single quotes avoid unnecessary escapes.
+3. When in doubt, [PEP8](https://www.python.org/dev/peps/pep-0008/). Follow its naming conventions.
+4. Know when to make exceptions.
+
+Also see: [How to name things in programming](http://www.slideshare.net/pirhilton/how-to-name-things-the-hardest-problem-in-programming)
+
+
+### Commits and Pull Requests
+
+Keep the commit log as healthy as the code. It is one of the first places new contributors will look at the project.
+
+1. No more than one change per commit. There should be no changes in a commit which are unrelated to its message.
+2. Every commit should pass all tests on its own.
+3. Follow [these conventions](http://chris.beams.io/posts/git-commit/) when writing the commit message
+
+When filing a Pull Request, make sure it is rebased on top of most recent master.
+If you need to modify it or amend it in some way, you should always appropriately [fixup](https://help.github.com/articles/about-git-rebase/) the issues in git and force-push your changes to your fork.
+
+Also see: [Github Help: Using Pull Requests](https://help.github.com/articles/using-pull-requests/)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a9733e0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) Jerome Leclanche <jerome at leclan.ch>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..fa3ae80
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include MANIFEST.in
+include README.rst
+include LICENSE
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..1e6ca7c
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,219 @@
+django-push-notifications
+=========================
+.. image:: https://api.travis-ci.org/jleclanche/django-push-notifications.png
+ :target: https://travis-ci.org/jleclanche/django-push-notifications
+
+A minimal Django app that implements Device models that can send messages through APNS and GCM.
+
+The app implements two models: ``GCMDevice`` and ``APNSDevice``. Those models share the same attributes:
+ - ``name`` (optional): A name for the device.
+ - ``active`` (default True): A boolean that determines whether the device will be sent notifications.
+ - ``user`` (optional): A foreign key to auth.User, if you wish to link the device to a specific user.
+ - ``device_id`` (optional): A UUID for the device obtained from Android/iOS APIs, if you wish to uniquely identify it.
+ - ``registration_id`` (required): The GCM registration id or the APNS token for the device.
+
+
+The app also implements an admin panel, through which you can test single and bulk notifications. Select one or more
+GCM or APNS devices and in the action dropdown, select "Send test message" or "Send test message in bulk", accordingly.
+Note that sending a non-bulk test message to more than one device will just iterate over the devices and send multiple
+single messages.
+
+Dependencies
+------------
+Django 1.8 is required. Support for older versions is available in the release 1.2.1.
+
+Tastypie support should work on Tastypie 0.11.0 and newer.
+
+Django REST Framework support should work on DRF version 3.0 and newer.
+
+Setup
+-----
+You can install the library directly from pypi using pip:
+
+.. code-block:: shell
+
+ $ pip install django-push-notifications
+
+
+Edit your settings.py file:
+
+.. code-block:: python
+
+ INSTALLED_APPS = (
+ ...
+ "push_notifications"
+ )
+
+ PUSH_NOTIFICATIONS_SETTINGS = {
+ "GCM_API_KEY": "<your api key>",
+ "APNS_CERTIFICATE": "/path/to/your/certificate.pem",
+ }
+
+.. note::
+ If you are planning on running your project with ``DEBUG=True``, then make sure you have set the
+ *development* certificate as your ``APNS_CERTIFICATE``. Otherwise the app will not be able to connect to the correct host. See settings_ for details.
+
+You can learn more about APNS certificates `here <https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ProvisioningDevelopment.html>`_.
+
+Native Django migrations are in use. ``manage.py migrate`` will install and migrate all models.
+
+.. _settings:
+
+Settings list
+-------------
+All settings are contained in a ``PUSH_NOTIFICATIONS_SETTINGS`` dict.
+
+In order to use GCM, you are required to include ``GCM_API_KEY``.
+For APNS, you are required to include ``APNS_CERTIFICATE``.
+
+- ``APNS_CERTIFICATE``: Absolute path to your APNS certificate file. Certificates with passphrases are not supported.
+- ``APNS_CA_CERTIFICATES``: Absolute path to a CA certificates file for APNS. Optional - do not set if not needed. Defaults to None.
+- ``GCM_API_KEY``: Your API key for GCM.
+- ``APNS_HOST``: The hostname used for the APNS sockets.
+ - When ``DEBUG=True``, this defaults to ``gateway.sandbox.push.apple.com``.
+ - When ``DEBUG=False``, this defaults to ``gateway.push.apple.com``.
+- ``APNS_PORT``: The port used along with APNS_HOST. Defaults to 2195.
+- ``GCM_POST_URL``: The full url that GCM notifications will be POSTed to. Defaults to https://android.googleapis.com/gcm/send.
+- ``GCM_MAX_RECIPIENTS``: The maximum amount of recipients that can be contained per bulk message. If the ``registration_ids`` list is larger than that number, multiple bulk messages will be sent. Defaults to 1000 (the maximum amount supported by GCM).
+
+Sending messages
+----------------
+GCM and APNS services have slightly different semantics. The app tries to offer a common interface for both when using the models.
+
+.. code-block:: python
+
+ from push_notifications.models import APNSDevice, GCMDevice
+
+ device = GCMDevice.objects.get(registration_id=gcm_reg_id)
+ # The first argument will be sent as "message" to the intent extras Bundle
+ # Retrieve it with intent.getExtras().getString("message")
+ device.send_message("You've got mail")
+ # If you want to customize, send an extra dict and a None message.
+ # the extras dict will be mapped into the intent extras Bundle.
+ # For dicts where all values are keys this will be sent as url parameters,
+ # but for more complex nested collections the extras dict will be sent via
+ # the bulk message api.
+ device.send_message(None, extra={"foo": "bar"})
+
+ device = APNSDevice.objects.get(registration_id=apns_token)
+ device.send_message("You've got mail") # Alert message may only be sent as text.
+ device.send_message(None, badge=5) # No alerts but with badge.
+ device.send_message(None, badge=1, extra={"foo": "bar"}) # Silent message with badge and added custom data.
+
+.. note::
+ APNS does not support sending payloads that exceed 2048 bytes (increased from 256 in 2014).
+ The message is only one part of the payload, if
+ once constructed the payload exceeds the maximum size, an ``APNSDataOverflow`` exception will be raised before anything is sent.
+
+Sending messages in bulk
+------------------------
+.. code-block:: python
+
+ from push_notifications.models import APNSDevice, GCMDevice
+
+ devices = GCMDevice.objects.filter(user__first_name="James")
+ devices.send_message("Happy name day!")
+
+Sending messages in bulk makes use of the bulk mechanics offered by GCM and APNS. It is almost always preferable to send
+bulk notifications instead of single ones.
+
+Administration
+--------------
+
+APNS devices which are not receiving push notifications can be set to inactive by two methods. The web admin interface for
+APNS devices has a "prune devices" option. Any selected devices which are not receiving notifications will be set to inactive [1]_.
+There is also a management command to prune all devices failing to receive notifications:
+
+.. code-block:: shell
+
+ $ python manage.py prune_devices
+
+This removes all devices which are not receiving notifications.
+
+For more information, please refer to the APNS feedback service_.
+
+.. _service: https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html
+
+Exceptions
+----------
+
+- ``NotificationError(Exception)``: Base exception for all notification-related errors.
+- ``gcm.GCMError(NotificationError)``: An error was returned by GCM. This is never raised when using bulk notifications.
+- ``apns.APNSError(NotificationError)``: Something went wrong upon sending APNS notifications.
+- ``apns.APNSDataOverflow(APNSError)``: The APNS payload exceeds its maximum size and cannot be sent.
+
+Tastypie support
+----------------
+
+The app includes tastypie-compatible resources in push_notifications.api.tastypie. These can be used as-is, or as base classes
+for more involved APIs.
+The following resources are available:
+
+- ``APNSDeviceResource``
+- ``GCMDeviceResource``
+- ``APNSDeviceAuthenticatedResource``
+- ``GCMDeviceAuthenticatedResource``
+
+The base device resources will not ask for authentication, while the authenticated ones will link the logged in user to
+the device they register.
+Subclassing the authenticated resources in order to add a ``SameUserAuthentication`` and a user ``ForeignKey`` is recommended.
+
+When registered, the APIs will show up at ``<api_root>/device/apns`` and ``<api_root>/device/gcm``, respectively.
+
+Django REST Framework (DRF) support
+-----------------------------------
+
+ViewSets are available for both APNS and GCM devices in two permission flavors:
+
+- ``APNSDeviceViewSet`` and ``GCMDeviceViewSet``
+
+ - Permissions as specified in settings (``AllowAny`` by default, which is not recommended)
+ - A device may be registered without associating it with a user
+
+- ``APNSDeviceAuthorizedViewSet`` and ``GCMDeviceAuthorizedViewSet``
+
+ - Permissions are ``IsAuthenticated`` and custom permission ``IsOwner``, which will only allow the ``request.user`` to get and update devices that belong to that user
+ - Requires a user to be authenticated, so all devices will be associated with a user
+
+When creating an ``APNSDevice``, the ``registration_id`` is validated to be a 64-character or 200-character hexadecimal string. Since 2016, device tokens are to be increased from 32 bytes to 100 bytes.
+
+Routes can be added one of two ways:
+
+- Routers_ (include all views)
+.. _Routers: http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers#using-routers
+
+::
+
+ from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet, GCMDeviceAuthorizedViewSet
+ from rest_framework.routers import DefaultRouter
+
+ router = DefaultRouter()
+ router.register(r'device/apns', APNSDeviceAuthorizedViewSet)
+ router.register(r'device/gcm', GCMDeviceAuthorizedViewSet)
+
+ urlpatterns = patterns('',
+ # URLs will show up at <api_root>/device/apns
+ url(r'^', include(router.urls)),
+ # ...
+ )
+
+- Using as_view_ (specify which views to include)
+.. _as_view: http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers#binding-viewsets-to-urls-explicitly
+
+::
+
+ from push_notifications.api.rest_framework import APNSDeviceAuthorizedViewSet
+
+ urlpatterns = patterns('',
+ # Only allow creation of devices by authenticated users
+ url(r'^device/apns/?$', APNSDeviceAuthorizedViewSet.as_view({'post': 'create'}), name='create_apns_device'),
+ # ...
+ )
+
+
+Python 3 support
+----------------
+
+``django-push-notifications`` is fully compatible with Python 3.4 & 3.5
+
+.. [1] Any devices which are not selected, but are not receiving notifications will not be deactivated on a subsequent call to "prune devices" unless another attempt to send a message to the device fails after the call to the feedback service.
diff --git a/push_notifications/__init__.py b/push_notifications/__init__.py
new file mode 100644
index 0000000..22eb89f
--- /dev/null
+++ b/push_notifications/__init__.py
@@ -0,0 +1,8 @@
+
+__author__ = "Jerome Leclanche"
+__email__ = "jerome at leclan.ch"
+__version__ = "1.4.1"
+
+
+class NotificationError(Exception):
+ pass
diff --git a/push_notifications/admin.py b/push_notifications/admin.py
new file mode 100644
index 0000000..fd4666b
--- /dev/null
+++ b/push_notifications/admin.py
@@ -0,0 +1,81 @@
+from django.contrib import admin, messages
+from django.contrib.auth import get_user_model
+from django.utils.translation import ugettext_lazy as _
+from .gcm import GCMError
+from .models import APNSDevice, GCMDevice, get_expired_tokens
+
+
+User = get_user_model()
+
+
+class DeviceAdmin(admin.ModelAdmin):
+ list_display = ("__str__", "device_id", "user", "active", "date_created")
+ search_fields = ("name", "device_id", "user__%s" % (User.USERNAME_FIELD))
+ list_filter = ("active", )
+ actions = ("send_message", "send_bulk_message", "prune_devices", "enable", "disable")
+
+ def send_messages(self, request, queryset, bulk=False):
+ """
+ Provides error handling for DeviceAdmin send_message and send_bulk_message methods.
+ """
+ ret = []
+ errors = []
+ r = ""
+
+ for device in queryset:
+ try:
+ if bulk:
+ r = queryset.send_message("Test bulk notification")
+ else:
+ r = device.send_message("Test single notification")
+ if r:
+ ret.append(r)
+ except GCMError as e:
+ errors.append(str(e))
+
+ if bulk:
+ break
+
+ if errors:
+ self.message_user(request, _("Some messages could not be processed: %r" % (", ".join(errors))), level=messages.ERROR)
+ if ret:
+ if not bulk:
+ ret = ", ".join(ret)
+ if errors:
+ msg = _("Some messages were sent: %s" % (ret))
+ else:
+ msg = _("All messages were sent: %s" % (ret))
+ self.message_user(request, msg)
+
+ def send_message(self, request, queryset):
+ self.send_messages(request, queryset)
+ send_message.short_description = _("Send test message")
+
+ def send_bulk_message(self, request, queryset):
+ self.send_messages(request, queryset, True)
+ send_bulk_message.short_description = _("Send test message in bulk")
+
+ def enable(self, request, queryset):
+ queryset.update(active=True)
+ enable.short_description = _("Enable selected devices")
+
+ def disable(self, request, queryset):
+ queryset.update(active=False)
+ disable.short_description = _("Disable selected devices")
+
+ def prune_devices(self, request, queryset):
+ # Note that when get_expired_tokens() is called, Apple's
+ # feedback service resets, so, calling it again won't return
+ # the device again (unless a message is sent to it again). So,
+ # if the user doesn't select all the devices for pruning, we
+ # could very easily leave an expired device as active. Maybe
+ # this is just a bad API.
+ expired = get_expired_tokens()
+ devices = queryset.filter(registration_id__in=expired)
+ for d in devices:
+ d.active = False
+ d.save()
+
+
+admin.site.register(APNSDevice, DeviceAdmin)
+admin.site.register(GCMDevice, DeviceAdmin)
diff --git a/push_notifications/api/__init__.py b/push_notifications/api/__init__.py
new file mode 100644
index 0000000..1f8fbd1
--- /dev/null
+++ b/push_notifications/api/__init__.py
@@ -0,0 +1,12 @@
+from django.conf import settings
+
+if "tastypie" in settings.INSTALLED_APPS:
+ # Tastypie resources are importable from the api package level (backwards compatibility)
+ from .tastypie import APNSDeviceResource, GCMDeviceResource, APNSDeviceAuthenticatedResource, GCMDeviceAuthenticatedResource
+
+ __all__ = [
+ "APNSDeviceResource",
+ "GCMDeviceResource",
+ "APNSDeviceAuthenticatedResource",
+ "GCMDeviceAuthenticatedResource"
+ ]
diff --git a/push_notifications/api/rest_framework.py b/push_notifications/api/rest_framework.py
new file mode 100644
index 0000000..7f0b826
--- /dev/null
+++ b/push_notifications/api/rest_framework.py
@@ -0,0 +1,127 @@
+from __future__ import absolute_import
+
+from rest_framework import permissions
+from rest_framework.serializers import ModelSerializer, ValidationError
+from rest_framework.validators import UniqueValidator
+from rest_framework.viewsets import ModelViewSet
+from rest_framework.fields import IntegerField
+
+from push_notifications.models import APNSDevice, GCMDevice
+from push_notifications.fields import hex_re
+from push_notifications.fields import UNSIGNED_64BIT_INT_MAX_VALUE
+
+# Fields
+
+
+class HexIntegerField(IntegerField):
+ """
+ Store an integer represented as a hex string of form "0x01".
+ """
+
+ def to_internal_value(self, data):
+ # validate hex string and convert it to the unsigned
+ # integer representation for internal use
+ try:
+ data = int(data, 16)
+ except ValueError:
+ raise ValidationError("Device ID is not a valid hex number")
+ return super(HexIntegerField, self).to_internal_value(data)
+
+ def to_representation(self, value):
+ return value
+
+
+# Serializers
+class DeviceSerializerMixin(ModelSerializer):
+ class Meta:
+ fields = ("name", "registration_id", "device_id", "active", "date_created")
+ read_only_fields = ("date_created", )
+
+ # See https://github.com/tomchristie/django-rest-framework/issues/1101
+ extra_kwargs = {"active": {"default": True}}
+
+
+class APNSDeviceSerializer(ModelSerializer):
+
+ class Meta(DeviceSerializerMixin.Meta):
+ model = APNSDevice
+
+ def validate_registration_id(self, value):
+ # iOS device tokens are 256-bit hexadecimal (64 characters). In 2016 Apple is increasing
+ # iOS device tokens to 100 bytes hexadecimal (200 characters).
+
+ if hex_re.match(value) is None or len(value) not in (64, 200):
+ raise ValidationError("Registration ID (device token) is invalid")
+
+ return value
+
+
+class GCMDeviceSerializer(ModelSerializer):
+ device_id = HexIntegerField(
+ help_text="ANDROID_ID / TelephonyManager.getDeviceId() (e.g: 0x01)",
+ style={'input_type': 'text'},
+ required=False
+ )
+
+ class Meta(DeviceSerializerMixin.Meta):
+ model = GCMDevice
+
+ extra_kwargs = {
+ # Work around an issue with validating the uniqueness of
+ # registration ids of up to 4k
+ 'registration_id': {
+ 'validators': [
+ UniqueValidator(queryset=GCMDevice.objects.all())
+ ]
+ }
+ }
+
+ def validate_device_id(self, value):
+ # device ids are 64 bit unsigned values
+ if value > UNSIGNED_64BIT_INT_MAX_VALUE:
+ raise ValidationError("Device ID is out of range")
+ return value
+
+
+# Permissions
+class IsOwner(permissions.BasePermission):
+ def has_object_permission(self, request, view, obj):
+ # must be the owner to view the object
+ return obj.user == request.user
+
+
+# Mixins
+class DeviceViewSetMixin(object):
+ lookup_field = "registration_id"
+
+ def perform_create(self, serializer):
+ if self.request.user.is_authenticated():
+ serializer.save(user=self.request.user)
+ return super(DeviceViewSetMixin, self).perform_create(serializer)
+
+
+class AuthorizedMixin(object):
+ permission_classes = (permissions.IsAuthenticated, IsOwner)
+
+ def get_queryset(self):
+ # filter all devices to only those belonging to the current user
+ return self.queryset.filter(user=self.request.user)
+
+
+# ViewSets
+class APNSDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
+ queryset = APNSDevice.objects.all()
+ serializer_class = APNSDeviceSerializer
+
+
+class APNSDeviceAuthorizedViewSet(AuthorizedMixin, APNSDeviceViewSet):
+ pass
+
+
+class GCMDeviceViewSet(DeviceViewSetMixin, ModelViewSet):
+ queryset = GCMDevice.objects.all()
+ serializer_class = GCMDeviceSerializer
+
+
+class GCMDeviceAuthorizedViewSet(AuthorizedMixin, GCMDeviceViewSet):
+ pass
diff --git a/push_notifications/api/tastypie.py b/push_notifications/api/tastypie.py
new file mode 100644
index 0000000..f1c048f
--- /dev/null
+++ b/push_notifications/api/tastypie.py
@@ -0,0 +1,42 @@
+from tastypie.authorization import Authorization
+from tastypie.authentication import BasicAuthentication
+from tastypie.resources import ModelResource
+from push_notifications.models import APNSDevice, GCMDevice
+
+
+class APNSDeviceResource(ModelResource):
+ class Meta:
+ authorization = Authorization()
+ queryset = APNSDevice.objects.all()
+ resource_name = "device/apns"
+
+
+class GCMDeviceResource(ModelResource):
+ class Meta:
+ authorization = Authorization()
+ queryset = GCMDevice.objects.all()
+ resource_name = "device/gcm"
+
+
+class APNSDeviceAuthenticatedResource(APNSDeviceResource):
+ # user = ForeignKey(UserResource, "user")
+
+ class Meta(APNSDeviceResource.Meta):
+ authentication = BasicAuthentication()
+ # authorization = SameUserAuthorization()
+
+ def obj_create(self, bundle, **kwargs):
+ # See https://github.com/toastdriven/django-tastypie/issues/854
+ return super(APNSDeviceAuthenticatedResource, self).obj_create(bundle, user=bundle.request.user, **kwargs)
+
+
+class GCMDeviceAuthenticatedResource(GCMDeviceResource):
+ # user = ForeignKey(UserResource, "user")
+
+ class Meta(GCMDeviceResource.Meta):
+ authentication = BasicAuthentication()
+ # authorization = SameUserAuthorization()
+
+ def obj_create(self, bundle, **kwargs):
+ # See https://github.com/toastdriven/django-tastypie/issues/854
+ return super(GCMDeviceAuthenticatedResource, self).obj_create(bundle, user=bundle.request.user, **kwargs)
diff --git a/push_notifications/apns.py b/push_notifications/apns.py
new file mode 100644
index 0000000..00a6879
--- /dev/null
+++ b/push_notifications/apns.py
@@ -0,0 +1,238 @@
+"""
+Apple Push Notification Service
+Documentation is available on the iOS Developer Library:
+https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/ApplePushService.html
+"""
+
+import codecs
+import json
+import ssl
+import struct
+import socket
+import time
+from contextlib import closing
+from binascii import unhexlify
+from django.core.exceptions import ImproperlyConfigured
+from . import NotificationError
+from .settings import PUSH_NOTIFICATIONS_SETTINGS as SETTINGS
+
+
+class APNSError(NotificationError):
+ pass
+
+
+class APNSServerError(APNSError):
+ def __init__(self, status, identifier):
+ super(APNSServerError, self).__init__(status, identifier)
+ self.status = status
+ self.identifier = identifier
+
+
+class APNSDataOverflow(APNSError):
+ pass
+
+
+def _apns_create_socket(address_tuple):
+ certfile = SETTINGS.get("APNS_CERTIFICATE")
+ if not certfile:
+ raise ImproperlyConfigured(
+ 'You need to set PUSH_NOTIFICATIONS_SETTINGS["APNS_CERTIFICATE"] to send messages through APNS.'
+ )
+
+ try:
+ with open(certfile, "r") as f:
+ f.read()
+ except Exception as e:
+ raise ImproperlyConfigured("The APNS certificate file at %r is not readable: %s" % (certfile, e))
+
+ ca_certs = SETTINGS.get("APNS_CA_CERTIFICATES")
+
+ sock = socket.socket()
+ sock = ssl.wrap_socket(sock, ssl_version=ssl.PROTOCOL_TLSv1, certfile=certfile, ca_certs=ca_certs)
+ sock.connect(address_tuple)
+
+ return sock
+
+
+def _apns_create_socket_to_push():
+ return _apns_create_socket((SETTINGS["APNS_HOST"], SETTINGS["APNS_PORT"]))
+
+
+def _apns_create_socket_to_feedback():
+ return _apns_create_socket((SETTINGS["APNS_FEEDBACK_HOST"], SETTINGS["APNS_FEEDBACK_PORT"]))
+
+
+def _apns_pack_frame(token_hex, payload, identifier, expiration, priority):
+ token = unhexlify(token_hex)
+ # |COMMAND|FRAME-LEN|{token}|{payload}|{id:4}|{expiration:4}|{priority:1}
+ frame_len = 3 * 5 + len(token) + len(payload) + 4 + 4 + 1 # 5 items, each 3 bytes prefix, then each item length
+ frame_fmt = "!BIBH%ssBH%ssBHIBHIBHB" % (len(token), len(payload))
+ frame = struct.pack(
+ frame_fmt,
+ 2, frame_len,
+ 1, len(token), token,
+ 2, len(payload), payload,
+ 3, 4, identifier,
+ 4, 4, expiration,
+ 5, 1, priority)
+
+ return frame
+
+
+def _apns_check_errors(sock):
+ timeout = SETTINGS["APNS_ERROR_TIMEOUT"]
+ if timeout is None:
+ return # assume everything went fine!
+ saved_timeout = sock.gettimeout()
+ try:
+ sock.settimeout(timeout)
+ data = sock.recv(6)
+ if data:
+ command, status, identifier = struct.unpack("!BBI", data)
+ # apple protocol says command is always 8. See http://goo.gl/ENUjXg
+ assert command == 8, "Command must be 8!"
+ if status != 0:
+ raise APNSServerError(status, identifier)
+ except socket.timeout: # py3, see http://bugs.python.org/issue10272
+ pass
+ except ssl.SSLError as e: # py2
+ if "timed out" not in e.message:
+ raise
+ finally:
+ sock.settimeout(saved_timeout)
+
+
+def _apns_send(token, alert, badge=None, sound=None, category=None, content_available=False,
+ action_loc_key=None, loc_key=None, loc_args=[], extra={}, identifier=0,
+ expiration=None, priority=10, socket=None):
+ data = {}
+ aps_data = {}
+
+ if action_loc_key or loc_key or loc_args:
+ alert = {"body": alert} if alert else {}
+ if action_loc_key:
+ alert["action-loc-key"] = action_loc_key
+ if loc_key:
+ alert["loc-key"] = loc_key
+ if loc_args:
+ alert["loc-args"] = loc_args
+
+ if alert is not None:
+ aps_data["alert"] = alert
+
+ if badge is not None:
+ aps_data["badge"] = badge
+
+ if sound is not None:
+ aps_data["sound"] = sound
+
+ if category is not None:
+ aps_data["category"] = category
+
+ if content_available:
+ aps_data["content-available"] = 1
+
+ data["aps"] = aps_data
+ data.update(extra)
+
+ # convert to json, avoiding unnecessary whitespace with separators (keys sorted for tests)
+ json_data = json.dumps(data, separators=(",", ":"), sort_keys=True).encode("utf-8")
+
+ max_size = SETTINGS["APNS_MAX_NOTIFICATION_SIZE"]
+ if len(json_data) > max_size:
+ raise APNSDataOverflow("Notification body cannot exceed %i bytes" % (max_size))
+
+ # if expiration isn't specified use 1 month from now
+ expiration_time = expiration if expiration is not None else int(time.time()) + 2592000
+
+ frame = _apns_pack_frame(token, json_data, identifier, expiration_time, priority)
+
+ if socket:
+ socket.write(frame)
+ else:
+ with closing(_apns_create_socket_to_push()) as socket:
+ socket.write(frame)
+ _apns_check_errors(socket)
+
+
+def _apns_read_and_unpack(socket, data_format):
+ length = struct.calcsize(data_format)
+ data = socket.recv(length)
+ if data:
+ return struct.unpack_from(data_format, data, 0)
... 1335 lines suppressed ...
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/python-django-push-notifications.git
More information about the Python-modules-commits
mailing list