34 | {% endblock content %}
35 |
36 | {% block script %}
37 |
44 | {% endblock %}
45 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Quantmind
2 |
3 | Redistribution and use in source and binary forms, with or without modification,
4 | are permitted provided that the following conditions are met:
5 |
6 | * Redistributions of source code must retain the above copyright notice,
7 | this list of conditions and the following disclaimer.
8 | * Redistributions in binary form must reproduce the above copyright notice,
9 | this list of conditions and the following disclaimer in the documentation
10 | and/or other materials provided with the distribution.
11 | * Neither the name of the author nor the names of its contributors
12 | may be used to endorse or promote products derived from this software without
13 | specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
19 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
20 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
22 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
23 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
24 | OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/djchat/static/djchat/chat.js:
--------------------------------------------------------------------------------
1 | (function ($) {
2 | "use strict";
3 | $.wschat = function (ws) {
4 | var messages = $('#messages'),
5 | users = $('#users'),
6 | message = $('#message'),
7 | user_label,
8 | who_ami,
9 | wall = function (user, message) {
10 | messages.prepend('
' + user + ' ' + message + '
');
11 | };
12 |
13 | ws.onmessage = function (e) {
14 | var data = $.parseJSON(e.data),
15 | label = 'info">@',
16 | user = data.user,
17 | userid = 'chatuser-' + user;
18 | if (!data.authenticated) {
19 | label = 'inverse">';
20 | }
21 | if (!who_ami) {
22 | who_ami = user;
23 | }
24 | user_label = '' + user_label + '');
31 | wall(user_label, 'joined the chat');
32 | }
33 | } else {
34 | wall(user_label, 'left the chat');
35 | users.find('#' + userid).remove();
36 | }
37 | }
38 | };
39 | $('#publish').click(function () {
40 | var msg = message.val();
41 | ws.send(msg);
42 | message.val('');
43 | });
44 | };
45 |
46 | }(jQuery));
47 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 |
4 | from setuptools import setup, find_packages
5 |
6 | import pulse
7 |
8 |
9 | def read(name):
10 | filename = os.path.join(os.path.dirname(__file__), name)
11 | with open(filename) as fp:
12 | return fp.read()
13 |
14 |
15 | def requirements(name):
16 | install_requires = []
17 | dependency_links = []
18 |
19 | for line in read(name).split('\n'):
20 | if line.startswith('-e '):
21 | link = line[3:].strip()
22 | if link == '.':
23 | continue
24 | dependency_links.append(link)
25 | line = link.split('=')[1]
26 | line = line.strip()
27 | if line:
28 | install_requires.append(line)
29 |
30 | return install_requires, dependency_links
31 |
32 |
33 | meta = dict(
34 | version=pulse.__version__,
35 | description=pulse.__doc__,
36 | name='pulsar-django',
37 | author='Luca Sbardella',
38 | author_email="luca@quantmind.com",
39 | maintainer_email="luca@quantmind.com",
40 | url="https://github.com/quantmind/pulsar-django",
41 | license="BSD",
42 | long_description=read('README.rst'),
43 | packages=find_packages(include=['pulse', 'pulse.*']),
44 | include_package_data=True,
45 | zip_safe=False,
46 | setup_requires=['pulsar', 'wheel'],
47 | install_requires=requirements('requirements/hard.txt')[0],
48 | classifiers=[
49 | 'Development Status :: 4 - Beta',
50 | 'Environment :: Web Environment',
51 | 'Intended Audience :: Developers',
52 | 'License :: OSI Approved :: BSD License',
53 | 'Operating System :: OS Independent',
54 | 'Programming Language :: Python',
55 | 'Programming Language :: Python :: 3.5',
56 | 'Programming Language :: Python :: 3.6',
57 | 'Topic :: Utilities'
58 | ]
59 | )
60 |
61 |
62 | if __name__ == '__main__':
63 | try:
64 | from pulsar import cmds
65 | meta['cmdclass'] = dict(pypi=cmds.PyPi)
66 | except ImportError:
67 | pass
68 | setup(**meta)
69 |
--------------------------------------------------------------------------------
/pulse/management/commands/pulse.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -
2 | import sys
3 |
4 | import pulsar
5 | from pulsar.api import Setting, Config
6 | from pulsar.apps.wsgi import WSGIServer
7 |
8 | from pulse.wsgi import Wsgi
9 |
10 | from django.core.management.base import (
11 | BaseCommand, CommandError, OutputWrapper, handle_default_options
12 | )
13 |
14 |
15 | class PulseAppName(Setting):
16 | section = "Django Pulse"
17 | app = "pulse"
18 | name = "pulse_app_name"
19 | flags = ["--pulse-app-name"]
20 | default = 'django_pulsar'
21 | desc = """\
22 | Name for the django pulse application
23 | """
24 |
25 |
26 | class Command(BaseCommand):
27 | help = "Starts a fully-functional Web server using pulsar."
28 | args = 'pulse --help for usage'
29 |
30 | def handle(self, *args, **options):
31 | if args:
32 | raise CommandError('pulse --help for usage')
33 | app_name = options.get('pulse_app_name')
34 | callable = Wsgi()
35 | if options.pop('dryrun', False) is True: # used for testing
36 | return callable
37 | # callable.setup()
38 | cfg = Config(apps=['socket', 'pulse'],
39 | server_software=pulsar.SERVER_SOFTWARE,
40 | **options)
41 | server = WSGIServer(callable=callable, name=app_name, cfg=cfg,
42 | parse_console=False)
43 | callable.cfg = server.cfg
44 | server.start()
45 |
46 | def get_version(self):
47 | return pulsar.__version__
48 |
49 | def create_parser(self, prog_name, subcommand):
50 | parser = super().create_parser(prog_name, subcommand)
51 | cfg = Config(apps=['socket', 'pulse'],
52 | exclude=['debug'],
53 | description=self.help,
54 | version=self.get_version())
55 | return cfg.add_to_parser(parser)
56 |
57 | def run_from_argv(self, argv):
58 | parser = self.create_parser(argv[0], argv[1])
59 | options = parser.parse_args(argv[2:])
60 | handle_default_options(options)
61 | try:
62 | self.execute(**options.__dict__)
63 | except Exception as e:
64 | if options.traceback or not isinstance(e, CommandError):
65 | raise
66 |
67 | # self.stderr is not guaranteed to be set here
68 | stderr = getattr(self, 'stderr',
69 | OutputWrapper(sys.stderr, self.style.ERROR))
70 | stderr.write('%s: %s' % (e.__class__.__name__, e))
71 | sys.exit(1)
72 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | main:
4 | working_directory: ~/main
5 | docker:
6 | - image: python:3.6.3
7 | steps:
8 | - checkout
9 | - run:
10 | name: install packages
11 | command: ci/install.sh
12 | - run:
13 | name: test
14 | command: make test
15 | coverage:
16 | working_directory: ~/coverage
17 | docker:
18 | - image: python:3.6.3
19 | steps:
20 | - checkout
21 | - run:
22 | name: install packages
23 | command: ci/install.sh
24 | - run:
25 | name: run tests for coverage
26 | command: make coverage
27 | - run:
28 | name: upload coverage stats
29 | command: codecov
30 | legacy:
31 | working_directory: ~/legacy
32 | docker:
33 | - image: python:3.5.4
34 | steps:
35 | - checkout
36 | - run:
37 | name: install packages
38 | command: ci/install.sh
39 | - run:
40 | name: test
41 | command: make test
42 | deploy-release:
43 | working_directory: ~/deploy
44 | docker:
45 | - image: python:3.6.3
46 | steps:
47 | - checkout
48 | - run:
49 | name: install packages
50 | command: ci/install.sh
51 | - run:
52 | name: check version
53 | command: python setup.py pypi --final
54 | - run:
55 | name: create source distribution
56 | command: python setup.py sdist
57 | - run:
58 | name: release source distribution
59 | command: twine upload dist/* --username lsbardel --password $PYPI_PASSWORD
60 | - run:
61 | name: tag
62 | command: ci/tag.sh
63 |
64 | workflows:
65 | version: 2
66 | build-deploy:
67 | jobs:
68 | - main:
69 | filters:
70 | branches:
71 | ignore: release
72 | tags:
73 | ignore: /.*/
74 | - coverage:
75 | filters:
76 | branches:
77 | ignore: release
78 | tags:
79 | ignore: /.*/
80 | - legacy:
81 | filters:
82 | branches:
83 | ignore: release
84 | tags:
85 | ignore: /.*/
86 | - deploy-release:
87 | filters:
88 | branches:
89 | only: release
90 | tags:
91 | ignore: /.*/
92 |
--------------------------------------------------------------------------------
/tests/test_app.py:
--------------------------------------------------------------------------------
1 | """Tests django chat application"""
2 | import unittest
3 | import asyncio
4 | import json
5 |
6 | from pulsar.api import send, get_application
7 | from pulsar.apps import http, ws
8 |
9 | from djchat import server
10 |
11 |
12 | async def start_server(actor, name, argv):
13 | server(argv)
14 | await asyncio.sleep(0.5)
15 | app = await get_application(name)
16 | return app.cfg
17 |
18 |
19 | class MessageHandler(ws.WS):
20 |
21 | def __init__(self, loop):
22 | self.queue = asyncio.Queue(loop=loop)
23 |
24 | def get(self):
25 | return self.queue.get()
26 |
27 | def on_message(self, websocket, message):
28 | return self.queue.put(message)
29 |
30 |
31 | class TestDjangoChat(unittest.TestCase):
32 | concurrency = 'process'
33 | app_cfg = None
34 |
35 | @classmethod
36 | async def setUpClass(cls):
37 | name = cls.__name__.lower()
38 | argv = [__file__, 'pulse',
39 | '-b', '127.0.0.1:0',
40 | '--concurrency', cls.concurrency,
41 | '--pulse-app-name', name,
42 | '--data-store', 'pulsar://127.0.0.1:6410/1']
43 | cls.app_cfg = await send('arbiter', 'run', start_server, name, argv)
44 | addr = cls.app_cfg.addresses[0]
45 | cls.uri = 'http://{0}:{1}'.format(*addr)
46 | cls.ws = 'ws://{0}:{1}/message'.format(*addr)
47 | cls.http = http.HttpClient()
48 |
49 | @classmethod
50 | def tearDownClass(cls):
51 | if cls.app_cfg:
52 | return send('arbiter', 'kill_actor', cls.app_cfg.name)
53 |
54 | async def test_home(self):
55 | result = await self.http.get(self.uri)
56 | self.assertEqual(result.status_code, 200)
57 | self.assertEqual(result.headers['content-type'],
58 | 'text/html; charset=utf-8')
59 |
60 | async def test_404(self):
61 | result = await self.http.get('%s/bsjdhcbjsdh' % self.uri)
62 | self.assertEqual(result.status_code, 404)
63 |
64 | async def test_websocket(self):
65 | c = self.http
66 | ws = await c.get(self.ws, websocket_handler=MessageHandler(c._loop))
67 | response = ws.handshake
68 | self.assertEqual(response.status_code, 101)
69 | self.assertEqual(response.headers['upgrade'], 'websocket')
70 | self.assertEqual(response.connection, ws.connection)
71 | self.assertTrue(ws.connection)
72 | self.assertIsInstance(ws.handler, MessageHandler)
73 | # send a message
74 | ws.write('Hi there!')
75 | message = await ws.handler.queue.get()
76 | self.assertTrue(message)
77 | data = json.loads(message)
78 | self.assertEqual(data['message'], 'Hi there!')
79 | self.assertEqual(data['channel'], 'webchat')
80 | self.assertFalse(data['authenticated'])
81 | #
82 | # close connection
83 | ws.write_close()
84 | await ws.connection.event('connection_lost').waiter()
85 |
--------------------------------------------------------------------------------
/djchat/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for djchat project.
3 |
4 | For more information on this file, see
5 | https://docs.djangoproject.com/en/1.7/topics/settings/
6 |
7 | For the full list of settings and their values, see
8 | https://docs.djangoproject.com/en/1.7/ref/settings/
9 | """
10 |
11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
12 | import os
13 | APP_DIR = os.path.dirname(__file__)
14 | BASE_DIR = os.path.dirname(APP_DIR)
15 |
16 |
17 | # Quick-start development settings - unsuitable for production
18 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
19 |
20 | # SECURITY WARNING: keep the secret key used in production secret!
21 | SECRET_KEY = 'fux9z2i)6ab$b_5*^z@96hdtqfj5=ct7b)m6_6cfrr5g%x#=81'
22 |
23 | # SECURITY WARNING: don't run with debug turned on in production!
24 | DEBUG = True
25 |
26 | ALLOWED_HOSTS = []
27 |
28 |
29 | # Application definition
30 |
31 | INSTALLED_APPS = (
32 | 'django.contrib.admin',
33 | 'django.contrib.auth',
34 | 'django.contrib.contenttypes',
35 | 'django.contrib.sessions',
36 | 'django.contrib.messages',
37 | 'django.contrib.staticfiles',
38 | 'pulse',
39 | 'djchat'
40 | )
41 |
42 | TEMPLATES = [
43 | {
44 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
45 | 'DIRS': [os.path.join(APP_DIR, 'templates')],
46 | 'APP_DIRS': True,
47 | 'OPTIONS': {
48 | 'context_processors': [
49 | 'django.template.context_processors.debug',
50 | 'django.template.context_processors.request',
51 | 'django.contrib.auth.context_processors.auth',
52 | 'django.contrib.messages.context_processors.messages'
53 | ]
54 | }
55 | }
56 | ]
57 |
58 | MIDDLEWARE_CLASSES = (
59 | 'django.contrib.sessions.middleware.SessionMiddleware',
60 | 'django.middleware.common.CommonMiddleware',
61 | 'django.middleware.csrf.CsrfViewMiddleware',
62 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
63 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
64 | 'django.contrib.messages.middleware.MessageMiddleware',
65 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
66 | 'djchat.views.middleware'
67 | )
68 |
69 | ROOT_URLCONF = 'djchat.urls'
70 |
71 |
72 | # Database
73 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases
74 |
75 | DATABASES = {
76 | 'default': {
77 | 'ENGINE': 'django.db.backends.sqlite3',
78 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
79 | }
80 | }
81 |
82 | # Internationalization
83 | # https://docs.djangoproject.com/en/1.7/topics/i18n/
84 |
85 | LANGUAGE_CODE = 'en-us'
86 |
87 | TIME_ZONE = 'UTC'
88 |
89 | USE_I18N = True
90 |
91 | USE_L10N = True
92 |
93 | USE_TZ = True
94 |
95 |
96 | # Static files (CSS, JavaScript, Images)
97 | # https://docs.djangoproject.com/en/1.7/howto/static-files/
98 |
99 | STATIC_URL = '/static/'
100 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Asynchronous Django
2 | =========================
3 |
4 | :Badges: |license| |pyversions| |status| |pypiversion|
5 | :CI: |circleci| |coverage|
6 | :Documentation: https://github.com/quantmind/pulsar-django
7 | :Downloads: http://pypi.python.org/pypi/pulsar-django
8 | :Source: https://github.com/quantmind/pulsar-django
9 | :Keywords: asynchronous, django, wsgi, websocket, redis
10 |
11 |
12 | The `pulse` module is a django_ application
13 | for running a django web site with pulsar_.
14 | Add it to the list of your ``INSTALLED_APPS``:
15 |
16 | .. code:: python
17 |
18 | INSTALLED_APPS = (
19 | ...,
20 | 'pulse',
21 | ...
22 | )
23 |
24 | and run the site via the ``pulse`` command::
25 |
26 | python manage.py pulse
27 |
28 | Check the django chat example ``djchat`` for a django chat
29 | application served by a multiprocessing pulsar server.
30 |
31 | By default, the ``pulse`` command creates a ``Wsgi`` middleware which
32 | runs the django application in a separate thread of execution from the
33 | main event loop.
34 | This is a standard programming pattern when using asyncio with blocking
35 | functions.
36 | To control the number of thread workers in the event loop executor (which
37 | is a pool of threads) one uses the
38 | ``thread-workers`` option. For example, the
39 | following command::
40 |
41 | python manage.py pulse -w 4 --thread-workers 20
42 |
43 | will run four process based actors, each with
44 | an executor with up to 20 threads.
45 |
46 | Greenlets
47 | ===============
48 |
49 | It is possible to run django in fully asynchronous mode, i.e. without
50 | running the middleware in the event loop executor.
51 | Currently, this is available when using PostgreSql backend
52 | only, and it requires the greenlet_ library.
53 |
54 | To run django using greenlet support::
55 |
56 | python manage.py pulse -w 4 --greenlet
57 |
58 | By default it will run the django middleware on a pool of 100 greenlets (and
59 | therefore approximately 100 separate database connections per actor). To
60 | adjust this number::
61 |
62 | python manage.py pulse -w 4 --greenlet 200
63 |
64 |
65 | Django Chat Example
66 | =======================
67 |
68 | This is a web chat application which illustrates how to run a django
69 | site with pulsar and how to include pulsar asynchronous request middleware
70 | into django.
71 |
72 | To run::
73 |
74 | python manage.py pulse
75 |
76 | If running for the first time, issue the::
77 |
78 | python manage.py migrate
79 |
80 | command and create the super user::
81 |
82 | python manage.py createsuperuser
83 |
84 |
85 | Message and data backend
86 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
87 |
88 | By default, messages from connected (websocket) clients are synchronised via
89 | the pulsar data store which starts when the django
90 | site starts.
91 |
92 | It is possible to specify a different data store via the
93 | ``data-store`` option. For example, it is possible
94 | to use redis_ as an alternative data store
95 | by issuing the following start up command::
96 |
97 | python manage.py pulse --data-store redis://127.0.0.1:6379/3
98 |
99 |
100 |
101 | .. _redis: http://redis.io/
102 | .. _django: https://docs.djangoproject.com/en/1.9/ref/applications/
103 | .. _pulsar: https://github.com/quantmind/pulsar
104 | .. _greenlet: https://greenlet.readthedocs.io
105 | .. |pypiversion| image:: https://badge.fury.io/py/pulsar-django.svg
106 | :target: https://pypi.python.org/pypi/pulsar-django
107 | .. |pyversions| image:: https://img.shields.io/pypi/pyversions/pulsar-django.svg
108 | :target: https://pypi.python.org/pypi/pulsar-django
109 | .. |license| image:: https://img.shields.io/pypi/l/pulsar-django.svg
110 | :target: https://pypi.python.org/pypi/pulsar-django
111 | .. |status| image:: https://img.shields.io/pypi/status/pulsar-django.svg
112 | :target: https://pypi.python.org/pypi/pulsar-django
113 | .. |coverage| image:: https://codecov.io/gh/quantmind/pulsar-django/branch/master/graph/badge.svg
114 | :target: https://codecov.io/gh/quantmind/pulsar-django
115 | .. |circleci| image:: https://circleci.com/gh/quantmind/pulsar-django.svg?style=svg
116 | :target: https://circleci.com/gh/quantmind/pulsar-django
117 |
--------------------------------------------------------------------------------
/djchat/views.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from django.shortcuts import render_to_response
4 | from django.http import HttpResponse
5 |
6 | from pulsar.api import HttpException
7 | from pulsar.apps import ws
8 | from pulsar.apps.data import PubSubClient, create_store
9 | from pulsar.utils.system import json
10 | from pulsar.utils.string import random_string
11 |
12 |
13 | def home(request):
14 | return render_to_response('home.html', {
15 | 'HOST': request.get_host()
16 | })
17 |
18 |
19 | class ChatClient(PubSubClient):
20 |
21 | def __init__(self, websocket):
22 | self.joined = time.time()
23 | self.websocket = websocket
24 | self.websocket._chat_client = self
25 |
26 | def __call__(self, channel, message):
27 | # The message is an encoded JSON string
28 | self.websocket.write(message, opcode=1)
29 |
30 |
31 | class Chat(ws.WS):
32 | ''':class:`.WS` handler managing the chat application.'''
33 | _store = None
34 | _pubsub = None
35 | _client = None
36 |
37 | async def get_pubsub(self, websocket):
38 | '''Create the pubsub handler if not already available'''
39 | if not self._store:
40 | cfg = websocket.cfg
41 | self._store = create_store(cfg.data_store)
42 | self._client = self._store.client()
43 | self._pubsub = self._store.pubsub()
44 | webchat = '%s:webchat' % cfg.exc_id
45 | chatuser = '%s:chatuser' % cfg.exc_id
46 | await self._pubsub.subscribe(webchat, chatuser)
47 | return self._pubsub
48 |
49 | async def on_open(self, websocket):
50 | '''A new websocket connection is established.
51 |
52 | Add it to the set of clients listening for messages.
53 | '''
54 | pubsub = await self.get_pubsub(websocket)
55 | pubsub.add_client(ChatClient(websocket))
56 | user, _ = self.user(websocket)
57 | users_key = 'webchatusers:%s' % websocket.cfg.exc_id
58 | # add counter to users
59 | registered = await self._client.hincrby(users_key, user, 1)
60 | if registered == 1:
61 | await self.publish(websocket, 'chatuser', 'joined')
62 |
63 | async def on_close(self, websocket):
64 | '''Leave the chat room
65 | '''
66 | user, _ = self.user(websocket)
67 | users_key = 'webchatusers:%s' % websocket.cfg.exc_id
68 | registered = await self._client.hincrby(users_key, user, -1)
69 | pubsub = await self.get_pubsub(websocket)
70 | pubsub.remove_client(websocket._chat_client)
71 | if not registered:
72 | await self.publish(websocket, 'chatuser', 'gone')
73 | if registered <= 0:
74 | await self._client.hdel(users_key, user)
75 |
76 | def on_message(self, websocket, msg):
77 | '''When a new message arrives, it publishes to all listening clients.
78 | '''
79 | if msg:
80 | lines = []
81 | for li in msg.split('\n'):
82 | li = li.strip()
83 | if li:
84 | lines.append(li)
85 | msg = ' '.join(lines)
86 | if msg:
87 | return self.publish(websocket, 'webchat', msg)
88 |
89 | def user(self, websocket):
90 | user = websocket.handshake.get('django.user')
91 | if user.is_authenticated():
92 | return user.username, True
93 | else:
94 | session = websocket.handshake.get('django.session')
95 | user = session.get('chatuser')
96 | if not user:
97 | user = 'an_%s' % random_string(length=6).lower()
98 | session['chatuser'] = user
99 | return user, False
100 |
101 | def publish(self, websocket, channel, message=''):
102 | user, authenticated = self.user(websocket)
103 | msg = {'message': message,
104 | 'user': user,
105 | 'authenticated': authenticated,
106 | 'channel': channel}
107 | channel = '%s:%s' % (websocket.cfg.exc_id, channel)
108 | return self._pubsub.publish(channel, json.dumps(msg))
109 |
110 |
111 | class middleware:
112 | '''Django middleware for serving the Chat websocket.'''
113 | def __init__(self):
114 | self._web_socket = ws.WebSocket('/message', Chat())
115 |
116 | def process_request(self, request):
117 | environ = request.META.copy()
118 | environ['django.user'] = request.user
119 | environ['django.session'] = request.session
120 | try:
121 | response = self._web_socket(environ)
122 | except HttpException as e:
123 | return HttpResponse(status=e.status)
124 | if response is not None:
125 | # we have a response, this is the websocket upgrade.
126 | # Convert to django response
127 | resp = HttpResponse(status=response.status_code,
128 | content_type=response.content_type)
129 | for header, value in response.headers.items():
130 | resp[header] = value
131 | return resp
132 |
--------------------------------------------------------------------------------