├── .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 | ))
--------------------------------------------------------------------------------