├── .gitignore ├── CHANGELOG.MD ├── LICENSE ├── MANIFEST.in ├── README.md ├── django_sns_mobile_push_notification ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── docs └── index.md ├── manage.py ├── mkdocs.yml ├── requirements.txt ├── setup.py └── sns_mobile_push_notification ├── __init__.py ├── admin.py ├── apps.py ├── client.py ├── migrations ├── 0001_initial.py └── __init__.py ├── models.py ├── tasks.py ├── tests.py └── views.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### OSX ### 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | Icon 6 | 7 | # DB 8 | *.sqlite3 9 | 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django: 57 | *.log 58 | 59 | # documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | ### PyCharm ### 66 | # PyCharm 67 | # http://www.jetbrains.com/pycharm/webhelp/project.html 68 | .idea 69 | .iml -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.9] - 2018-05-01 4 | ### Changed 5 | - Initial release 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 @pythonicrubyist 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include sns_mobile_push_notification * -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django SNS Mobile Push Notifications 2 | 3 | 4 | Send mobile push notification to IOS and android devices using Amazon SNS. 5 | 6 | [![Documentation Status](https://readthedocs.org/projects/django-sns-mobile-push-notification/badge/?version=latest)](http://django-sns-mobile-push-notification.readthedocs.io/en/latest/?badge=latest) 7 | 8 | Documentation 9 | ----------- 10 | https://django-sns-mobile-push-notification.readthedocs.io/ 11 | 12 | Project Home 13 | ------------ 14 | https://github.com/fuzz-productions/django-sns-mobile-push-notification 15 | 16 | PyPi 17 | ------------ 18 | https://pypi.python.org/pypi/django-sns-mobile-push-notification 19 | -------------------------------------------------------------------------------- /django_sns_mobile_push_notification/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/django-sns-mobile-push-notification/9864e1e395d421aafe6b8b3cf6f546ee38142da3/django_sns_mobile_push_notification/__init__.py -------------------------------------------------------------------------------- /django_sns_mobile_push_notification/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_sns_mobile_push_notification project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'uzx$i4%^z$rb@bujzuc9(+^kele!3pev7!6(w-_b2)h5x%zv-n' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | # Application definition 31 | 32 | INSTALLED_APPS = [ 33 | 'sns_mobile_push_notification', 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'django_sns_mobile_push_notification.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'django_sns_mobile_push_notification.wsgi.application' 71 | 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 80 | } 81 | } 82 | 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 99 | }, 100 | ] 101 | 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-us' 107 | 108 | TIME_ZONE = 'UTC' 109 | 110 | USE_I18N = True 111 | 112 | USE_L10N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 119 | 120 | STATIC_URL = '/static/' 121 | 122 | # AWS settings 123 | AWS_ACCESS_KEY_ID = 'xxxxxxxxx' 124 | AWS_SECRET_ACCESS_KEY = 'xxxxxxxxx' 125 | IOS_PLATFORM_APPLICATION_ARN = 'xxxxxxxxx' 126 | ANDROID_PLATFORM_APPLICATION_ARN = 'xxxxxxxxx' 127 | AWS_SNS_REGION_NAME = 'xxxxxxxxx' 128 | -------------------------------------------------------------------------------- /django_sns_mobile_push_notification/urls.py: -------------------------------------------------------------------------------- 1 | """django_sns_mobile_push_notification URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.conf.urls import url 18 | 19 | urlpatterns = [ 20 | url(r'^admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /django_sns_mobile_push_notification/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_sns_mobile_push_notification project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_sns_mobile_push_notification.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to Django SNS Mobile Push Notifications. 2 | 3 | Send push notifications to IOS and Android devices using Amazon SNS. 4 | 5 | ## Getting Started 6 | 7 | ### Installation 8 | 9 | You can install sns_mobile_push_notification directly from pypi using pip: 10 | ```zsh 11 | pip install django-sns-mobile-push-notification 12 | ``` 13 | 14 | 15 | Edit your settings.py file: 16 | 17 | ```python 18 | INSTALLED_APPS = ( 19 | ... 20 | "sns_mobile_push_notification", 21 | ) 22 | ``` 23 | 24 | ## Required Settings 25 | 26 | Login to AWS's SNS Dashboard and manually create 2 platform applications. One for IOS, and one for Android. 27 | Add the following variables to your Django's settings file: 28 | 29 | | Name | Description | 30 | |------|-------------| 31 | | ``AWS_ACCESS_KEY_ID`` | Access key of your AWS user. | 32 | | ``AWS_SECRET_ACCESS_KEY`` | Secret key of your AWS user. | 33 | | ``AWS_SNS_REGION_NAME`` | The region your SNS application is located in( e.g. 'eu-central-1'). | 34 | | ``IOS_PLATFORM_APPLICATION_ARN`` | ARN for IOS platform application. | 35 | | ``ANDROID_PLATFORM_APPLICATION_ARN`` | ARN for Android platform application. | 36 | 37 | 38 | ## Usage 39 | ```python 40 | from sns_mobile_push_notification.models import Device 41 | from sns_mobile_push_notification.tasks import register_device, refresh_device, send_sns_mobile_push_notification_to_device, deregister_device 42 | 43 | # Given a valid token from Google's FCM(GCM), or Apple's APNs, create a device object. 44 | device = Device() 45 | device.token = "123456" 46 | device.os = Device.IOS_OS 47 | device.save() 48 | 49 | # By registering a device, the token will be sent to SNS and SNS will return an ARN key which will be saved in the device object. 50 | # ARN is required to send future push notification to SNS, regardless of the device type. 51 | register_device(device) 52 | device.refresh_from_db() 53 | print(device.arn) 54 | 55 | # You can refresh the device to make sure it is enabled and ready to use. 56 | refresh_device(device) 57 | 58 | # Now you can send the push notification to the the registered device. 59 | if device.active and device.arn: 60 | send_sns_mobile_push_notification_to_device( 61 | device=device, 62 | notification_type="type", 63 | text="text", 64 | data={"a": "b"}, 65 | title="title" 66 | ) 67 | 68 | # remove a device from SNS. 69 | deregister_device(device) 70 | ``` 71 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_sns_mobile_push_notification.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: django-sns-mobile-push-notification 2 | site_url: https://github.com/fuzz-productions/django-sns-mobile-push-notification 3 | site_author: Ramtin Vaziri @pythonicrubyist 4 | pages: 5 | - [index.md, Getting Started] 6 | theme: readthedocs -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.7.10 2 | botocore==1.10.10 3 | Django==1.11.12 4 | docutils==0.14 5 | jmespath==0.9.3 6 | mock==2.0.0 7 | pbr==4.0.2 8 | python-dateutil==2.7.2 9 | pytz==2018.4 10 | s3transfer==0.1.13 11 | six==1.11.0 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | from setuptools import setup 5 | 6 | if sys.version_info >= (3, 0): 7 | README = open(os.path.join(os.path.dirname(__file__), 'README.md'), encoding='utf-8').read() 8 | else: 9 | README = open(os.path.join(os.path.dirname(__file__), 'README.md')).read() 10 | 11 | reqs = ['boto3>=1', ] 12 | 13 | # allow setup.py to be run from any path 14 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 15 | 16 | setup( 17 | name='django-sns-mobile-push-notification', 18 | version='0.9', 19 | packages=['sns_mobile_push_notification'], 20 | include_package_data=True, 21 | license='MIT License', 22 | description='Send mobile push notification to IOS and android devices using Amazon SNS.', 23 | long_description=README, 24 | url='https://github.com/pythonicrubyist', 25 | author='Ramtin Vaziri', 26 | author_email='pythonicrubyist@gmail.com', 27 | classifiers=[ 28 | 'Environment :: Web Environment', 29 | 'Framework :: Django', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Operating System :: OS Independent', 33 | 'Programming Language :: Python', 34 | 'Programming Language :: Python :: 3', 35 | 'Topic :: Internet :: WWW/HTTP', 36 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 37 | ], 38 | install_requires=reqs, 39 | ) -------------------------------------------------------------------------------- /sns_mobile_push_notification/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/django-sns-mobile-push-notification/9864e1e395d421aafe6b8b3cf6f546ee38142da3/sns_mobile_push_notification/__init__.py -------------------------------------------------------------------------------- /sns_mobile_push_notification/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from sns_mobile_push_notification.models import Device, Log 3 | 4 | 5 | admin.site.register(Device) 6 | admin.site.register(Log) 7 | -------------------------------------------------------------------------------- /sns_mobile_push_notification/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SnsNotificationConfig(AppConfig): 5 | name = 'sns_mobile_push_notification' 6 | verbose_name = 'SNS Mobile Push Notification fro Django' 7 | -------------------------------------------------------------------------------- /sns_mobile_push_notification/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | AWS SNS Client 3 | """ 4 | 5 | from django.conf import settings 6 | import boto3 7 | import json 8 | 9 | 10 | class Client(object): 11 | """ Class representing an AWS SNS client that supports mobile push notifications. 12 | 13 | Design Pattern: 14 | 15 | It follows Borg design pattern. 16 | https://github.com/faif/python-patterns/blob/master/creational/borg.py 17 | """ 18 | 19 | # The state shared by all instances of this client class. 20 | __shared_state = {} 21 | 22 | def __init__(self): 23 | """ 24 | Constructor method. 25 | """ 26 | self.__dict__ = self.__shared_state 27 | self.connection = self.connect() 28 | 29 | # retrieve AWS credentials from settings. 30 | self.ios_arn = getattr(settings, 'IOS_PLATFORM_APPLICATION_ARN') 31 | self.android_arn = getattr(settings, 'ANDROID_PLATFORM_APPLICATION_ARN') 32 | 33 | @staticmethod 34 | def connect(): 35 | """ 36 | Method that creates a connection to AWS SNS 37 | :return: AWS boto3 connection object 38 | """ 39 | # start an AWS session. 40 | session = boto3.Session() 41 | if getattr(settings, 'AWS_SNS_REGION_NAME', None) and getattr(settings, 'AWS_ACCESS_KEY_ID', None): 42 | return session.client( 43 | "sns", 44 | region_name=getattr(settings, 'AWS_SNS_REGION_NAME'), 45 | aws_access_key_id=getattr(settings, 'AWS_ACCESS_KEY_ID'), 46 | aws_secret_access_key=getattr(settings, 'AWS_SECRET_ACCESS_KEY'), 47 | ) 48 | else: 49 | return session.client( 50 | "sns", 51 | region_name=getattr(settings, 'AWS_SNS_REGION_NAME') 52 | ) 53 | 54 | def retrieve_platform_endpoint_attributs(self, arn): 55 | """ 56 | Method that retrieves a platform endpoint for an IOS device. 57 | :param arn: ARN(Amazon resource name) 58 | :return: attributes of the endpoint 59 | """ 60 | response = self.connection.get_endpoint_attributes( 61 | EndpointArn=arn 62 | ) 63 | return response['Attributes'] 64 | 65 | def delete_platform_endpoint(self, arn): 66 | self.connection.delete_endpoint( 67 | EndpointArn=arn 68 | ) 69 | 70 | def create_ios_platform_endpoint(self, token): 71 | """ 72 | Method that creates a platform endpoint for an IOS device. 73 | :param token: device token 74 | :return: response from SNS 75 | """ 76 | response = self.connection.create_platform_endpoint( 77 | PlatformApplicationArn=self.ios_arn, 78 | Token=token, 79 | ) 80 | return response 81 | 82 | def create_android_platform_endpoint(self, token): 83 | """ 84 | Method that creates a platform endpoint for an Android device. 85 | :param token: device token 86 | :return: response from SNS 87 | """ 88 | response = self.connection.create_platform_endpoint( 89 | PlatformApplicationArn=self.android_arn, 90 | Token=token, 91 | ) 92 | return response 93 | 94 | def publish_to_android(self, arn, title, text, notification_type, data, id): 95 | """ 96 | Method that sends a mobile push notification to an android device. 97 | :param arn: ARN(Amazon resource name) 98 | :param title: message title 99 | :param text: message body 100 | :param notification_type: type of notification 101 | :param data: data to be used for deep-linking 102 | :param id: notification ID 103 | :return: response from SNS 104 | """ 105 | message = { 106 | "GCM": "{ \"notification\": { \"title\": \"%s\", \"text\": \"%s\", \"body\": \"%s\", \"sound\": \"default\" }, \"data\": { \"id\": \"%s\", \"type\": \"%s\", \"serializer\": \"%s\" } }" % (title, text, text, id, notification_type, json.dumps(data).replace("'", "").replace('"', "'"))} 107 | response = self.connection.publish( 108 | TargetArn=arn, 109 | Message=json.dumps(message), 110 | MessageStructure='json', 111 | ) 112 | return message, response 113 | 114 | def publish_to_ios(self, arn, title, text, notification_type, data, id): 115 | """ 116 | Method that sends a mobile push notification to an IOS device. 117 | :param arn: ARN(Amazon resource name) 118 | :param title: message title 119 | :param text: message body 120 | :param notification_type: type of notification 121 | :param data: data to be used for deep-linking 122 | :param id: notification ID 123 | :return: response from SNS 124 | """ 125 | message = {"APNS": "{ \"aps\": { \"alert\": { \"title\": \"%s\", \"body\": \"%s\" }, \"sound\": \"default\" }, \"id\": \"%s\", \"type\": \"%s\", \"serializer\": \"%s\" }" % (title, text, id, notification_type, json.dumps(data).replace("'", "").replace('"', "'"))} 126 | response = self.connection.publish( 127 | TargetArn=arn, 128 | Message=json.dumps(message), 129 | MessageStructure='json', 130 | ) 131 | return message, response 132 | -------------------------------------------------------------------------------- /sns_mobile_push_notification/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0.4 on 2018-04-27 21:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Device', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('created_at', models.DateTimeField(auto_now_add=True)), 20 | ('updated_at', models.DateTimeField(auto_now=True)), 21 | ('os', models.IntegerField(choices=[(0, 'IOS'), (1, 'Android')])), 22 | ('token', models.CharField(max_length=255, unique=True)), 23 | ('arn', models.CharField(blank=True, max_length=255, null=True, unique=True)), 24 | ('active', models.BooleanField(default=True)), 25 | ], 26 | options={ 27 | 'ordering': ['-id'], 28 | }, 29 | ), 30 | migrations.CreateModel( 31 | name='Log', 32 | fields=[ 33 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 34 | ('created_at', models.DateTimeField(auto_now_add=True)), 35 | ('updated_at', models.DateTimeField(auto_now=True)), 36 | ('notification_type', models.CharField(blank=True, max_length=255, null=True)), 37 | ('arn', models.CharField(blank=True, max_length=255, null=True)), 38 | ('message', models.TextField(blank=True, null=True)), 39 | ('response', models.TextField(blank=True, null=True)), 40 | ('device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notification_logs', to='sns_mobile_push_notification.Device')), 41 | ], 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /sns_mobile_push_notification/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/django-sns-mobile-push-notification/9864e1e395d421aafe6b8b3cf6f546ee38142da3/sns_mobile_push_notification/migrations/__init__.py -------------------------------------------------------------------------------- /sns_mobile_push_notification/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from sns_mobile_push_notification.client import Client 3 | 4 | 5 | class Device(models.Model): 6 | """ 7 | Django model class representing a device. 8 | """ 9 | 10 | # Constants 11 | IOS_OS = 0 12 | ANDROID_OS = 1 13 | OS_CHOICES = ( 14 | (IOS_OS, 'IOS'), 15 | (ANDROID_OS, 'Android'), 16 | ) 17 | 18 | # Fields 19 | created_at = models.DateTimeField(auto_now_add=True) 20 | updated_at = models.DateTimeField(auto_now=True) 21 | os = models.IntegerField(choices=OS_CHOICES) 22 | token = models.CharField(max_length=255, unique=True) 23 | arn = models.CharField(max_length=255, unique=True, blank=True, null=True) 24 | active = models.BooleanField(default=True) 25 | 26 | # Methods 27 | def __str__(self): 28 | """ 29 | :return: string representation of this class. 30 | """ 31 | return '%s device' % self.os_name 32 | 33 | # Metadata 34 | class Meta: 35 | ordering = ['-id'] 36 | 37 | # Properties 38 | @property 39 | def is_android(self): 40 | return self.os == Device.ANDROID_OS 41 | 42 | @property 43 | def is_ios(self): 44 | return self.os == Device.IOS_OS 45 | 46 | @property 47 | def os_name(self): 48 | if self.is_android: 49 | return "ANDROID" 50 | elif self.is_ios: 51 | return "IOS" 52 | else: 53 | return "unknown" 54 | 55 | def register(self): 56 | """ 57 | Method that registered a device on SNS for the first time, 58 | so that the device can receive mobile push notifications. 59 | it retrieves the endpoints ARN code and stores it. 60 | the ARN code will be used as the identifier for the device to send out mobile push notifications. 61 | :return: response from SNS 62 | """ 63 | client = Client() 64 | if self.is_android: 65 | response = client.create_android_platform_endpoint(self.token) 66 | elif self.is_ios: 67 | response = client.create_ios_platform_endpoint(self.token) 68 | self.arn = response['EndpointArn'] 69 | self.save(update_fields=['arn']) 70 | return response 71 | 72 | def refresh(self): 73 | """ 74 | Method that checks/fixes the SNS endpoint corresponding a self. 75 | If the endpoint is deleted, disabled, or the it's token does not match the device token, 76 | it tries to recreate it. 77 | This task should be called upon a device update. 78 | :return: attributes retrieved from SNS 79 | """ 80 | client = Client() 81 | try: 82 | attributes = client.retrieve_platform_endpoint_attributs(self.arn) 83 | endpoint_enabled = (attributes['Enabled'] == True) or (attributes['Enabled'].lower() == "true") 84 | tokens_matched = attributes['Token'] == self.token 85 | if not (endpoint_enabled and tokens_matched): 86 | client.delete_platform_endpoint(self.arn) 87 | self.register() 88 | attributes = client.retrieve_platform_endpoint_attributs(self.arn) 89 | return attributes 90 | except Exception as e: 91 | if 'Endpoint does not exist' in str(e): 92 | self.register() 93 | attributes = client.retrieve_platform_endpoint_attributs(self.arn) 94 | return attributes 95 | else: 96 | self.active = False 97 | self.save(update_fields=['active']) 98 | 99 | def deregister(self): 100 | """ 101 | Method that deletes registered a device from SNS. 102 | :return: none 103 | """ 104 | client = Client() 105 | client.delete_platform_endpoint(self.arn) 106 | self.active = False 107 | self.save(update_fields=['active']) 108 | 109 | def send(self, notification_type, text, data, title): 110 | """ 111 | Method that sends out a mobile push notification to a specific self. 112 | :param notification_type: type of notification to be sent 113 | :param text: text to be included in the push notification 114 | :param data: data to be included in the push notification 115 | :param title: title to be included in the push notification 116 | :return: response from SNS 117 | """ 118 | log = Log( 119 | device=self, 120 | notification_type=notification_type, 121 | ) 122 | log.save() 123 | 124 | client = Client() 125 | 126 | if self.is_android: 127 | message, response = client.publish_to_android( 128 | arn=self.arn, 129 | text=text, 130 | title=title, 131 | notification_type=notification_type, 132 | data=data, 133 | id=log.id, 134 | ) 135 | elif self.is_ios: 136 | message, response = client.publish_to_ios( 137 | arn=self.arn, 138 | text=text, 139 | title=title, 140 | notification_type=notification_type, 141 | data=data, 142 | id=log.id, 143 | ) 144 | 145 | log.message = message 146 | log.response = response 147 | log.save() 148 | 149 | return response 150 | 151 | 152 | class Log(models.Model): 153 | """ 154 | Django model class representing a notification log. 155 | """ 156 | 157 | # Fields 158 | created_at = models.DateTimeField(auto_now_add=True) 159 | updated_at = models.DateTimeField(auto_now=True) 160 | device = models.ForeignKey('Device', on_delete=models.CASCADE, related_name='notification_logs', null=True, blank=True) 161 | notification_type = models.CharField(max_length=255, null=True, blank=True) 162 | arn = models.CharField(max_length=255, null=True, blank=True) 163 | message = models.TextField(null=True, blank=True) 164 | response = models.TextField(null=True, blank=True) 165 | 166 | # Methods 167 | def __str__(self): 168 | """ 169 | :return: string representation of this class. 170 | """ 171 | return '"%s" notification log for - "%s"' % (self.notification_type, self.device) -------------------------------------------------------------------------------- /sns_mobile_push_notification/tasks.py: -------------------------------------------------------------------------------- 1 | def register_device(device): 2 | """ 3 | Task that registers a device. 4 | :param device: device to be registered. 5 | :return: response from SNS 6 | """ 7 | return device.register() 8 | 9 | 10 | def deregister_device(device): 11 | """ 12 | Task that deregisters a device. 13 | :param device: device to be deregistered. 14 | :return: response from SNS 15 | """ 16 | return device.deregister() 17 | 18 | 19 | def refresh_device(device): 20 | """ 21 | Task that refreshes a device. 22 | :param device: device to be refreshed. 23 | :return: response from SNS 24 | """ 25 | return device.refresh() 26 | 27 | 28 | def send_sns_mobile_push_notification_to_device(device, notification_type, text, data, title): 29 | """ 30 | Method that sends out a mobile push notification to a specific self. 31 | :param device: device to send the notification to. 32 | :param notification_type: type of notification to be sent 33 | :param text: text to be included in the push notification 34 | :param data: data to be included in the push notification 35 | :param title: title to be included in the push notification 36 | :return: response from SNS 37 | """ 38 | return device.send( 39 | notification_type=notification_type, 40 | text=text, 41 | data=data, 42 | title=title 43 | ) 44 | -------------------------------------------------------------------------------- /sns_mobile_push_notification/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | from unittest.mock import patch 3 | import json 4 | from .models import Device, Log 5 | from .tasks import * 6 | 7 | 8 | class TestNotificationTasks(TestCase): 9 | @classmethod 10 | def setUpClass(cls): 11 | Device.objects.all().delete() 12 | Log.objects.all().delete() 13 | 14 | @classmethod 15 | def tearDownClass(cls): 16 | Device.objects.all().delete() 17 | Log.objects.all().delete() 18 | 19 | @patch('sns_mobile_push_notification.models.Client') 20 | def test_register(self, mock_Client): 21 | Log.objects.all().delete() 22 | token = 'token' 23 | device = Device.objects.create( 24 | token=token, 25 | os=Device.ANDROID_OS, 26 | arn='arn' 27 | ) 28 | 29 | mock_response = {'EndpointArn': 'arn'} 30 | mock_Client().create_android_platform_endpoint.return_value = mock_response 31 | response = register_device(device) 32 | device.refresh_from_db() 33 | self.assertEquals(response['EndpointArn'], mock_response['EndpointArn']) 34 | self.assertEquals(device.arn, mock_response['EndpointArn']) 35 | 36 | 37 | @patch('sns_mobile_push_notification.models.Client') 38 | def test_refresh_when_enabled(self, mock_Client): 39 | Log.objects.all().delete() 40 | token = 'token' 41 | device = Device.objects.create( 42 | token=token, 43 | os=Device.ANDROID_OS, 44 | arn='arn' 45 | ) 46 | mock_response = {'Enabled': 'true', 'Token': token} 47 | mock_Client().retrieve_platform_endpoint_attributs.return_value = mock_response 48 | mock_Client().delete_platform_endpoint.return_value = '' 49 | response = refresh_device(device) 50 | self.assertEquals(response, mock_response) 51 | self.assertEquals(device.token, mock_response['Token']) 52 | 53 | @patch('sns_mobile_push_notification.models.Client') 54 | def test_refresh_when_disabled(self, mock_Client): 55 | Log.objects.all().delete() 56 | token = 'token' 57 | device = Device.objects.create( 58 | token=token, 59 | os=Device.ANDROID_OS, 60 | arn='arn' 61 | ) 62 | mock_response_1 = {'Enabled': 'false', 'Token': token} 63 | mock_Client().retrieve_platform_endpoint_attributs.return_value = mock_response_1 64 | mock_response_2 = {'EndpointArn': 'arn'} 65 | mock_Client().create_android_platform_endpoint.return_value = mock_response_2 66 | mock_Client().delete_platform_endpoint.return_value = '' 67 | response = refresh_device(device) 68 | self.assertEquals(response, mock_response_1) 69 | self.assertEquals(device.arn, mock_response_2['EndpointArn']) 70 | 71 | @patch('sns_mobile_push_notification.models.Client') 72 | def test_deregister(self, mock_Client): 73 | Log.objects.all().delete() 74 | token = 'token' 75 | device = Device.objects.create( 76 | token=token, 77 | os=Device.ANDROID_OS, 78 | arn='arn' 79 | ) 80 | mock_Client().delete_platform_endpoint.return_value = None 81 | response = deregister_device(device) 82 | self.assertEquals(True, True) 83 | 84 | @patch('sns_mobile_push_notification.models.Client') 85 | def test_publish_to_android(self, mock_Client): 86 | Log.objects.all().delete() 87 | token = 'token' 88 | device = Device.objects.create( 89 | token=token, 90 | os=Device.ANDROID_OS, 91 | arn='arn' 92 | ) 93 | 94 | mock_response = ("message", {'EndpointArn': 'arn', 'ResponseMetadata': {'RetryAttempts': 0, 'HTTPHeaders': {'x-amzn-requestid': 'e08722bb-4218-5b6a-8e55-71fa82e9ffc3', 'content-length': '424', 'date': 'Fri, 06 Apr 2018 18:38:40 GMT', 'content-type': 'text/xml'}, 'HTTPStatusCode': 200, 'RequestId': 'e08722bb-4218-5b6a-8e55-71fa82e9ffc3'}},) 95 | mock_Client().publish_to_android.return_value = mock_response 96 | response = send_sns_mobile_push_notification_to_device( 97 | device=device, 98 | notification_type="type", 99 | text="text", 100 | data={"a": "b"}, 101 | title="title" 102 | ) 103 | self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200) 104 | 105 | log = Log.objects.first() 106 | self.assertEquals(log.device_id, device.id) 107 | self.assertEquals(log.message, "message") 108 | self.assertEquals(log.response, json.dumps(mock_response[1]).replace('"', "'")) 109 | 110 | @patch('sns_mobile_push_notification.models.Client') 111 | def test_publish_to_ios(self, mock_Client): 112 | Log.objects.all().delete() 113 | token = 'token' 114 | device = Device.objects.create( 115 | token=token, 116 | os=Device.IOS_OS, 117 | arn='arn' 118 | ) 119 | 120 | mock_response = ("message", {'EndpointArn': 'arn', 'ResponseMetadata': {'RetryAttempts': 0, 'HTTPHeaders': {'x-amzn-requestid': 'e08722bb-4218-5b6a-8e55-71fa82e9ffc3', 'content-length': '424', 'date': 'Fri, 06 Apr 2018 18:38:40 GMT', 'content-type': 'text/xml'}, 'HTTPStatusCode': 200, 'RequestId': 'e08722bb-4218-5b6a-8e55-71fa82e9ffc3'}},) 121 | mock_Client().publish_to_ios.return_value = mock_response 122 | 123 | response = send_sns_mobile_push_notification_to_device( 124 | device=device, 125 | notification_type="type", 126 | text="text", 127 | data={"a": "b"}, 128 | title="title" 129 | ) 130 | self.assertEquals(response['ResponseMetadata']['HTTPStatusCode'], 200) 131 | 132 | log = Log.objects.first() 133 | self.assertEquals(log.device_id, device.id) 134 | self.assertEquals(log.message, "message") 135 | self.assertEquals(log.response, json.dumps(mock_response[1]).replace('"', "'")) 136 | -------------------------------------------------------------------------------- /sns_mobile_push_notification/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | --------------------------------------------------------------------------------