Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 47c02e9
Showing
27 changed files
with
718 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
*.sqlite3 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
Channels Examples | ||
================= | ||
|
||
This is a repository of simple, well-commented examples of how to implement a | ||
few different site features in Django Channels. | ||
|
||
Each example is a standalone Django project; instructions for running them | ||
are in their own README files. | ||
|
||
Pull requests of whole new examples, or small improvements to existing ones, | ||
are welcome; however, they should still be simple and not add too many complex | ||
features or additional steps to running it. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
Liveblog | ||
======== | ||
|
||
Illustrates a "liveblog" using Channels. This is a page that shows a series | ||
of short-form posts in descending date order, with new ones appearing at the | ||
top as they're posted. | ||
|
||
The site supports multiple liveblogs at once, and clients only listen for new | ||
posts on the blog they're currently viewing. | ||
|
||
When you view a liveblog page, we open a WebSocket to Django, and the consumer | ||
there adds it to a Group based on the liveblog slug it used in the URL of the | ||
socket. Then, in the ``save()`` method of the ``Post`` model, we send notifications | ||
onto that Group that all currently connected clients pick up on, and insert | ||
the new post at the top of the page. | ||
|
||
Updates are also supported - the notification is sent with an ID, and if a post | ||
with that ID is already on the page, the JavaScript just replaces its content | ||
instead. | ||
|
||
|
||
Installation | ||
------------ | ||
|
||
Make a new virtualenv for the project, and run:: | ||
|
||
pip install -r requirements.txt | ||
|
||
Then, you'll need Redis running locally; the settings are configured to | ||
point to ``localhost``, port ``6379``, but you can change this in the | ||
``CHANNEL_LAYERS`` setting in ``settings.py`. | ||
Finally, run:: | ||
python manage.py migrate | ||
python manage.py runserver | ||
Suggested Exercises | ||
------------------- | ||
If you want to try out making some changes and getting a feel for Channels, | ||
here's some ideas and hints on how to do them: | ||
* Make the posts disappear immediately on post deletion. You'll need to send | ||
notifications triggered on the model delete, in a similar way to the ones | ||
for save, and modify the JavaScript to understand them. | ||
* Implement a view of all liveblogs at once. You'll need to make a new group | ||
that people who connect to this endpoint will subscribe to, insert into that | ||
group from the save() method as well as the existing per-liveblog group, | ||
and make new consumers to add and remove people from that group as they | ||
connect to a WebSocket on a different path (including new routing entries). | ||
* Make the front page list of liveblogs update. You'll need another new WebSocket | ||
endpoint (with new consumers and routing), a new group to send updates down, | ||
and to tie that group into LiveBlog's save process. Decide if you want to | ||
send differential updates, or just re-send the whole list each time a new one | ||
is created. Both have advantages - what are they? | ||
* Try adding Like functionality to the posts, so viewers can "like" a post and | ||
it's sent back over the WebSocket to a ``websocket.receive`` consumer that | ||
saves the like into the database, then propagates it to all existing clients. | ||
Can you reuse the existing Post.save() hook? What happens if hundreds | ||
of people are trying to like every second? | ||
|
||
|
||
Further Reading | ||
--------------- | ||
|
||
You can find the Channels documentation at http://channels.readthedocs.org |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
""" | ||
ASGI entrypoint file for default channel layer. | ||
Points to the channel layer configured as "default" so you can point | ||
ASGI applications at "liveblog.asgi:channel_layer" as their channel layer. | ||
""" | ||
|
||
import os | ||
from channels.asgi import get_channel_layer | ||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "liveblog.settings") | ||
channel_layer = get_channel_layer() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from channels import route | ||
from posts.consumers import connect_blog, disconnect_blog | ||
|
||
|
||
# The channel routing defines what channels get handled by what consumers, | ||
# including optional matching on message attributes. WebSocket messages of all | ||
# types have a 'path' attribute, so we're using that to route the socket. | ||
# While this is under stream/ compared to the HTML page, we could have it on the | ||
# same URL if we wanted; Daphne separates by protocol as it negotiates with a browser. | ||
channel_routing = [ | ||
# Called when incoming WebSockets connect | ||
route("websocket.connect", connect_blog, path=r'^/liveblog/(?P<slug>[^/]+)/stream/$'), | ||
|
||
# Called when the client closes the socket | ||
route("websocket.disconnect", disconnect_blog, path=r'^/liveblog/(?P<slug>[^/]+)/stream/$'), | ||
|
||
# A default "http.request" route is always inserted by Django at the end of the routing list | ||
# that routes all unmatched HTTP requests to the Django view system. If you want lower-level | ||
# HTTP handling - e.g. long-polling - you can do it here and route by path, and let the rest | ||
# fall through to normal views. | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
""" | ||
Django settings for liveblog project. | ||
Generated by 'django-admin startproject' using Django 1.10.dev20151126161447. | ||
For more information on this file, see | ||
https://docs.djangoproject.com/en/dev/topics/settings/ | ||
For the full list of settings and their values, see | ||
https://docs.djangoproject.com/en/dev/ref/settings/ | ||
""" | ||
|
||
import os | ||
|
||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) | ||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||
|
||
# SECURITY WARNING: keep the secret key used in production secret! And don't use debug=True in production! | ||
SECRET_KEY = 'imasecret' | ||
DEBUG = True | ||
ALLOWED_HOSTS = [] | ||
|
||
# Application definition | ||
INSTALLED_APPS = [ | ||
'django.contrib.admin', | ||
'django.contrib.auth', | ||
'django.contrib.contenttypes', | ||
'django.contrib.sessions', | ||
'django.contrib.messages', | ||
'django.contrib.staticfiles', | ||
'channels', | ||
'posts', | ||
] | ||
|
||
MIDDLEWARE_CLASSES = [ | ||
'django.middleware.security.SecurityMiddleware', | ||
'django.contrib.sessions.middleware.SessionMiddleware', | ||
'django.middleware.common.CommonMiddleware', | ||
'django.middleware.csrf.CsrfViewMiddleware', | ||
'django.contrib.auth.middleware.AuthenticationMiddleware', | ||
'django.contrib.messages.middleware.MessageMiddleware', | ||
'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||
] | ||
|
||
ROOT_URLCONF = 'liveblog.urls' | ||
|
||
# Channel layer definitions | ||
# http://channels.readthedocs.org/en/latest/deploying.html#setting-up-a-channel-backend | ||
CHANNEL_LAYERS = { | ||
"default": { | ||
# This example app uses the Redis channel layer implementation asgi_redis | ||
"BACKEND": "asgi_redis.RedisChannelLayer", | ||
"CONFIG": { | ||
"hosts": [("localhost", 6379)], | ||
}, | ||
"ROUTING": "liveblog.routing.channel_routing", | ||
}, | ||
} | ||
|
||
TEMPLATES = [ | ||
{ | ||
'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||
'DIRS': [ | ||
os.path.join(BASE_DIR, 'templates'), | ||
], | ||
'APP_DIRS': True, | ||
'OPTIONS': { | ||
'context_processors': [ | ||
'django.template.context_processors.debug', | ||
'django.template.context_processors.request', | ||
'django.contrib.auth.context_processors.auth', | ||
'django.contrib.messages.context_processors.messages', | ||
], | ||
}, | ||
}, | ||
] | ||
|
||
WSGI_APPLICATION = 'liveblog.wsgi.application' | ||
|
||
|
||
# Database | ||
# https://docs.djangoproject.com/en/dev/ref/settings/#databases | ||
|
||
DATABASES = { | ||
'default': { | ||
'ENGINE': 'django.db.backends.sqlite3', | ||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), | ||
} | ||
} | ||
|
||
|
||
# Password validation | ||
# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators | ||
# Deliberately turned off for this example. | ||
AUTH_PASSWORD_VALIDATORS = [] | ||
|
||
|
||
# Internationalization | ||
# https://docs.djangoproject.com/en/dev/topics/i18n/ | ||
LANGUAGE_CODE = 'en-us' | ||
TIME_ZONE = 'UTC' | ||
USE_I18N = True | ||
USE_L10N = True | ||
USE_TZ = True | ||
|
||
|
||
# Static files (CSS, JavaScript, Images) | ||
# https://docs.djangoproject.com/en/dev/howto/static-files/ | ||
|
||
STATIC_URL = '/static/' | ||
STATICFILES_DIRS = [ | ||
os.path.join(BASE_DIR, "static"), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from django.conf.urls import url | ||
from django.contrib import admin | ||
from posts.views import index, liveblog | ||
|
||
|
||
urlpatterns = [ | ||
url(r'^$', index), | ||
url(r'^liveblog/(?P<slug>[^/]+)/$', liveblog), | ||
url(r'^admin/', admin.site.urls), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
""" | ||
WSGI config for liveblog project. | ||
It exposes the WSGI callable as a module-level variable named ``application``. | ||
For more information on this file, see | ||
https://docs.djangoproject.com/en/dev/howto/deployment/wsgi/ | ||
""" | ||
|
||
import os | ||
|
||
from django.core.wsgi import get_wsgi_application | ||
|
||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "liveblog.settings") | ||
|
||
application = get_wsgi_application() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
#!/usr/bin/env python | ||
import os | ||
import sys | ||
|
||
if __name__ == "__main__": | ||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "liveblog.settings") | ||
|
||
from django.core.management import execute_from_command_line | ||
|
||
execute_from_command_line(sys.argv) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from django.contrib import admin | ||
from .models import Liveblog, Post | ||
|
||
|
||
admin.site.register( | ||
Liveblog, | ||
list_display=["id", "title", "slug"], | ||
list_display_links=["id", "title"], | ||
ordering=["title"], | ||
prepopulated_fields={"slug": ("title",)}, | ||
) | ||
|
||
|
||
admin.site.register( | ||
Post, | ||
list_display=["id", "liveblog", "created", "body_intro"], | ||
ordering=["-id"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from django.apps import AppConfig | ||
|
||
|
||
class PostsConfig(AppConfig): | ||
name = 'posts' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import json | ||
from channels import Group | ||
from .models import Liveblog | ||
|
||
|
||
# The "slug" keyword argument here comes from the regex capture group in | ||
# routing.py. | ||
def connect_blog(message, slug): | ||
""" | ||
When the user opens a WebSocket to a liveblog stream, adds them to the | ||
group for that stream so they receive new post notifications. | ||
The notifications are actually sent in the Post model on save. | ||
""" | ||
# Try to fetch the liveblog by slug; if that fails, close the socket. | ||
try: | ||
liveblog = Liveblog.objects.get(slug=slug) | ||
except Liveblog.DoesNotExist: | ||
# You can see what messages back to a WebSocket look like in the spec: | ||
# http://channels.readthedocs.org/en/latest/asgi.html#send-close | ||
# Here, we send "close" to make Daphne close off the socket, and some | ||
# error text for the client. | ||
message.reply_channel.send({ | ||
# WebSockets send either a text or binary payload each frame. | ||
# We do JSON over the text portion. | ||
"text": json.dumps({"error": "bad_slug"}), | ||
"close": True, | ||
}) | ||
# Each different client has a different "reply_channel", which is how you | ||
# send information back to them. We can add all the different reply channels | ||
# to a single Group, and then when we send to the group, they'll all get the | ||
# same message. | ||
Group(liveblog.group_name).add(message.reply_channel) | ||
|
||
|
||
def disconnect_blog(message, slug): | ||
""" | ||
Removes the user from the liveblog group when they disconnect. | ||
Channels will auto-cleanup eventually, but it can take a while, and having old | ||
entries cluttering up your group will reduce performance. | ||
""" | ||
try: | ||
liveblog = Liveblog.objects.get(slug=slug) | ||
except Liveblog.DoesNotExist: | ||
# This is the disconnect message, so the socket is already gone; we can't | ||
# send an error back. Instead, we just return from the consumer. | ||
return | ||
# It's called .discard() because if the reply channel is already there it | ||
# won't fail - just lie the set() type. | ||
Group(liveblog.group_name).discard(message.reply_channel) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# -*- coding: utf-8 -*- | ||
# Generated by Django 1.10.dev20151126161447 on 2016-03-23 18:51 | ||
from __future__ import unicode_literals | ||
|
||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name='Liveblog', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('title', models.CharField(max_length=255)), | ||
('slug', models.SlugField()), | ||
], | ||
), | ||
migrations.CreateModel( | ||
name='Post', | ||
fields=[ | ||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||
('body', models.TextField()), | ||
('created', models.DateTimeField(auto_now_add=True)), | ||
('updated', models.DateTimeField(auto_now=True)), | ||
('liveblog', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='posts', to='posts.Liveblog')), | ||
], | ||
), | ||
] |
Empty file.
Oops, something went wrong.