[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