├── .gitignore ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── pusherable ├── __init__.py ├── example │ ├── __init__.py │ ├── models.py │ ├── templates │ │ └── example.html │ ├── urls.py │ └── views.py ├── mixins.py └── templatetags │ ├── __init__.py │ └── pusherable_tags.py ├── requirements-test.txt ├── runtests.py ├── setup.cfg ├── setup.py └── tests ├── __init__.py └── test_example.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | 37 | # Complexity 38 | output/*.html 39 | output/*/index.html 40 | 41 | # Sphinx 42 | docs/_build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Pusher 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of django-pusherable nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include pusherable *.html *.py 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs 2 | 3 | help: 4 | @echo "clean-build - remove build artifacts" 5 | @echo "clean-pyc - remove Python file artifacts" 6 | @echo "release - package and upload a release" 7 | @echo "sdist - package" 8 | 9 | clean: clean-build clean-pyc 10 | 11 | clean-build: 12 | rm -fr build/ 13 | rm -fr dist/ 14 | rm -fr *.egg-info 15 | 16 | clean-pyc: 17 | find . -name '*.pyc' -exec rm -f {} + 18 | find . -name '*.pyo' -exec rm -f {} + 19 | find . -name '*~' -exec rm -f {} + 20 | 21 | release: clean 22 | python setup.py sdist upload 23 | python setup.py bdist_wheel upload 24 | 25 | sdist: clean 26 | python setup.py sdist 27 | ls -l dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-pusherable 2 | 3 | Real-time object access notifications via [Pusher](https://pusher.com). 4 | 5 | ## Installation 6 | 7 | Install django-pusherable: 8 | 9 | ```bash 10 | pip install django-pusherable 11 | ``` 12 | 13 | ## Configuration 14 | 15 | Then add `pusherable` to your `INSTALLED_APPS`. You will also need to add your Pusher 16 | app credentials to `settings.py`. These are available on your app keys page: 17 | 18 | ```python 19 | PUSHER_APP_ID = u"" 20 | PUSHER_KEY = u"" 21 | PUSHER_SECRET = u"" 22 | PUSHER_CLUSTER = u"" 23 | ``` 24 | 25 | ## Mixins 26 | 27 | To begin receiving notifications about an object use the mixins: 28 | 29 | ```python 30 | from pusherable.mixins import PusherDetailMixin, PusherUpdateMixin 31 | 32 | class PostDetail(PusherDetailMixin, DetailView): 33 | model = Post 34 | 35 | class PostUpdate(PusherUpdateMixin, UpdateView): 36 | model = Post 37 | form_class = PostUpdateForm 38 | ``` 39 | 40 | When the view is accessed it will send an event on the channel 41 | `modelname_pk` which contains a JSON representation of the object (model instance) 42 | being accessed as well as the user. 43 | 44 | The data will be in the form: 45 | 46 | ```json 47 | { 48 | "object": { 49 | "question": "What's up?", 50 | "pub_date": "2013-08-08T11:16:24", 51 | "id": 1 52 | }, 53 | "user": "admin" 54 | } 55 | ``` 56 | 57 | Which fields are included and excluded within the `object` is configurable via 58 | `pusher_include_model_fields` and `pusher_exclude_model_fields`. For example, 59 | the following would exclude the `pub_date` from the event payload: 60 | 61 | ```python 62 | class PostUpdate(PusherUpdateMixin, UpdateView): 63 | model = Post 64 | form_class = PostUpdateForm 65 | pusher_exclude_model_fields = 'pub_date' 66 | ``` 67 | 68 | ## Template tags 69 | 70 | To subscribe to these events on your page you can use the templatetags: 71 | 72 | ``` 73 | {% load pusherable_tags %} 74 | 75 | {% pusherable_script %} 76 | ``` 77 | 78 | The `pusherable_script` tag will include the Pusher library. Place this in the 79 | head of your page: 80 | 81 | ``` 82 | {% pusherable_subscribe 'update' object %} 83 | ``` 84 | 85 | The `pusherable_subscribe` tag will begin subscribe you to the channel for the 86 | object. The first argument is the type of event you want to subscribe to. 87 | The default events are `update` and `view`. 88 | 89 | When a new event is received it will pass event type and data to a Javascript 90 | function called `pusherable_notify`. Create this function and use it to alert your 91 | users to the new event. For example: 92 | 93 | ```html 94 | 99 | ``` 100 | 101 | ## Running Tests 102 | 103 | Pusherable comes with test requirements and a test runner: 104 | 105 | ```bash 106 | pip install -r requirements-test.txt 107 | python runtests.py 108 | ``` 109 | 110 | ## Credits 111 | 112 | django-pusherable was built by [Aaron Bassett](https://twitter.com/aaronbassett) for Pusher. 113 | -------------------------------------------------------------------------------- /pusherable/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.2.0' 2 | -------------------------------------------------------------------------------- /pusherable/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pusherable/example/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models 4 | 5 | 6 | class PusherableExample(models.Model): 7 | text = models.TextField() 8 | -------------------------------------------------------------------------------- /pusherable/example/templates/example.html: -------------------------------------------------------------------------------- 1 | {% load pusherable_tags %} 2 | 3 | {% pusherable_script %} 4 | 5 | {% pusherable_subscribe 'view' object %} -------------------------------------------------------------------------------- /pusherable/example/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.conf.urls import url 4 | from .views import PusherableExampleDetail 5 | 6 | 7 | urlpatterns = [ 8 | url(r'^(?P\d+)/$', PusherableExampleDetail.as_view(), name="example"), 9 | ] 10 | -------------------------------------------------------------------------------- /pusherable/example/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.views.generic.detail import DetailView 4 | from pusherable.mixins import PusherDetailMixin 5 | from .models import PusherableExample 6 | 7 | 8 | class PusherableExampleDetail(PusherDetailMixin, DetailView): 9 | model = PusherableExample 10 | template_name = "example.html" 11 | -------------------------------------------------------------------------------- /pusherable/mixins.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import json 4 | 5 | from django.conf import settings 6 | from django.core.serializers.json import DjangoJSONEncoder 7 | from django.forms.models import model_to_dict 8 | 9 | from pusher import Pusher 10 | 11 | 12 | class PusherMixin(object): 13 | pusher_include_model_fields = None 14 | pusher_exclude_model_fields = None 15 | 16 | def render_to_response(self, context, **response_kwargs): 17 | 18 | channel = u"{model}_{pk}".format( 19 | model=self.object._meta.model_name, 20 | pk=self.object.pk 21 | ) 22 | 23 | data = self.__object_to_json_serializable(self.object) 24 | 25 | try: 26 | pusher_cluster = settings.PUSHER_CLUSTER 27 | except AttributeError: 28 | pusher_cluster = 'mt1' 29 | 30 | pusher = Pusher(app_id=settings.PUSHER_APP_ID, 31 | key=settings.PUSHER_KEY, 32 | secret=settings.PUSHER_SECRET, 33 | cluster=pusher_cluster) 34 | pusher.trigger( 35 | [channel, ], 36 | self.pusher_event_name, 37 | { 38 | 'object': data, 39 | 'user': self.request.user.username 40 | } 41 | ) 42 | 43 | return super(PusherMixin, self).render_to_response(context, **response_kwargs) 44 | 45 | def __object_to_json_serializable(self, object): 46 | model_dict = model_to_dict(object, 47 | fields=self.pusher_include_model_fields, exclude=self.pusher_exclude_model_fields) 48 | json_data = json.dumps(model_dict, cls=DjangoJSONEncoder) 49 | data = json.loads(json_data) 50 | return data 51 | 52 | 53 | class PusherUpdateMixin(PusherMixin): 54 | pusher_event_name = u"update" 55 | 56 | class PusherDetailMixin(PusherMixin): 57 | pusher_event_name = u"view" 58 | 59 | class PusherDeleteMixin(PusherMixin): 60 | pusher_event_name = u"delete" 61 | -------------------------------------------------------------------------------- /pusherable/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pusherable/templatetags/pusherable_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.conf import settings 3 | 4 | register = template.Library() 5 | 6 | @register.simple_tag 7 | def pusherable_script(): 8 | return "" 9 | 10 | 11 | @register.simple_tag 12 | def pusherable_subscribe(event, instance): 13 | 14 | channel = u"{model}_{pk}".format( 15 | model=instance._meta.model_name, 16 | pk=instance.pk 17 | ) 18 | 19 | return """ 20 | 27 | """.format( 28 | key=settings.PUSHER_KEY, 29 | channel=channel, 30 | event=event 31 | ) 32 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | django>=1.8 2 | pusher==1.1.3 3 | coverage 4 | coveralls 5 | mock>=1.0.1 6 | nose>=1.3.0 7 | django-nose>=1.2 -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | try: 4 | from django.conf import settings 5 | 6 | settings.configure( 7 | DEBUG=True, 8 | USE_TZ=True, 9 | DATABASES={ 10 | "default": { 11 | "ENGINE": "django.db.backends.sqlite3", 12 | } 13 | }, 14 | ROOT_URLCONF="pusherable.example.urls", 15 | INSTALLED_APPS=[ 16 | "django.contrib.auth", 17 | "django.contrib.contenttypes", 18 | "django.contrib.sites", 19 | "pusherable", 20 | "pusherable.example", 21 | ], 22 | SITE_ID=1, 23 | NOSE_ARGS=['-s'], 24 | MIDDLEWARE_CLASSES=(), 25 | 26 | PUSHER_APP_ID=u"xxxxxxxxxxxxx", 27 | PUSHER_KEY=u"xxxxxxxxxxxxx", 28 | PUSHER_SECRET=u"xxxxxxxxxxxxx", 29 | ) 30 | 31 | try: 32 | import django 33 | setup = django.setup 34 | except AttributeError: 35 | pass 36 | else: 37 | setup() 38 | 39 | from django_nose import NoseTestSuiteRunner 40 | except ImportError: 41 | import traceback 42 | traceback.print_exc() 43 | raise ImportError("To fix this error, run: pip install -r requirements-test.txt") 44 | 45 | 46 | def run_tests(*test_args): 47 | if not test_args: 48 | test_args = ['tests'] 49 | 50 | # Run tests 51 | test_runner = NoseTestSuiteRunner(verbosity=1) 52 | 53 | failures = test_runner.run_tests(test_args) 54 | 55 | if failures: 56 | sys.exit(failures) 57 | 58 | 59 | if __name__ == '__main__': 60 | run_tests(*sys.argv[1:]) 61 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | [metadata] 4 | description-file = README.md 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | 7 | import pusherable 8 | 9 | try: 10 | from setuptools import setup 11 | except ImportError: 12 | from distutils.core import setup 13 | 14 | version = pusherable.__version__ 15 | 16 | if sys.argv[-1] == 'publish': 17 | os.system('python setup.py sdist upload') 18 | print("You probably want to also tag the version now:") 19 | print(" git tag -a %s -m 'version %s'" % (version, version)) 20 | print(" git push --tags") 21 | sys.exit() 22 | 23 | readme = open('README.md').read() 24 | 25 | setup( 26 | name='django-pusherable', 27 | version=version, 28 | description="""Real time object access notifications via Pusher""", 29 | long_description=readme, 30 | author='Aaron Bassett, Pusher', 31 | author_email='aaron@rawtech.io, support@pusher.com', 32 | url='https://github.com/pusher/django-pusherable', 33 | packages=[ 34 | 'pusherable', 35 | ], 36 | include_package_data=True, 37 | install_requires=[ 38 | "pusher", 39 | ], 40 | license="BSD", 41 | zip_safe=False, 42 | keywords='django-pusherable', 43 | classifiers=[ 44 | 'Framework :: Django', 45 | 'Intended Audience :: Developers', 46 | 'License :: OSI Approved :: BSD License', 47 | 'Natural Language :: English', 48 | 'Programming Language :: Python :: 2', 49 | 'Programming Language :: Python :: 2.6', 50 | 'Programming Language :: Python :: 2.7', 51 | ], 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import shutil 6 | import mock 7 | 8 | from django.contrib.auth.models import User 9 | from django.test import Client, RequestFactory, TestCase 10 | from django.core.urlresolvers import reverse 11 | from pusherable.mixins import PusherMixin 12 | from pusherable.example.models import PusherableExample 13 | from pusherable.example.views import PusherableExampleDetail 14 | 15 | 16 | class TestPusherable(TestCase): 17 | 18 | def setUp(self): 19 | self.factory = RequestFactory() 20 | self.user = User.objects.create_user( 21 | username='pusher', email='pusher@example.com', password='hunter2' 22 | ) 23 | self.object = PusherableExample.objects.create( 24 | text = "This is a test PusherableExample object" 25 | ) 26 | 27 | @mock.patch("pusherable.mixins.Pusher") 28 | def test_pusher_templatetags(self, Pusher): 29 | request = self.factory.get(reverse("example", kwargs={"pk": self.object.pk})) 30 | request.user = self.user 31 | response = PusherableExampleDetail.as_view()(request, pk=self.object.pk) 32 | 33 | channel = u"{model}_{pk}".format( 34 | model=self.object._meta.model_name, 35 | pk=self.object.pk 36 | ) 37 | 38 | self.assertContains(response, "js.pusher.com/2.2/pusher.min.js") 39 | self.assertContains(response, "pusher.subscribe('{channel}');".format( 40 | channel=channel 41 | )) --------------------------------------------------------------------------------