diff --git a/README.rst b/README.rst index d7d7dbac4..b4b3e443b 100644 --- a/README.rst +++ b/README.rst @@ -46,6 +46,7 @@ Optional Integrations * Integration with Sentry_ for error logging * Integration with NewRelic_ for performance monitoring * Integration with Opbeat_ for performance monitoring +* Experimental Channels_ support .. _alpha: http://blog.getbootstrap.com/2015/08/19/bootstrap-4-alpha/ .. _Hitch: https://github.com/hitchtest/hitchtest @@ -64,7 +65,7 @@ Optional Integrations .. _NewRelic: https://newrelic.com .. _docker-compose: https://www.github.com/docker/compose .. _Opbeat: https://opbeat.com/ - +.. _Channels: https://github.com/andrewgodwin/channels Constraints ----------- diff --git a/cookiecutter.json b/cookiecutter.json index a9b01f9da..6650fb7aa 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -16,5 +16,6 @@ "use_newrelic": "n", "use_opbeat": "n", "windows": "n", - "use_python2": "n" + "use_python2": "n", + "use_channels": "n" } diff --git a/docs/channels.rst b/docs/channels.rst new file mode 100644 index 000000000..6c52ab779 --- /dev/null +++ b/docs/channels.rst @@ -0,0 +1,29 @@ +Channels +======== + +Cookiecutter-django comes with (experimental) channels support. A basic skeleton of a Django +project with channels is generated automatically for you if you choose to `use_channels` during +project setup. + +.. note:: If you are using docker, a websocket server and the worker processes are started +automatically for you. Just replace 127.0.0.1 with the IP of your docker-machine you are running. + +There's a basic app called `channelsapp` created automatically for you. It uses the routes from +`conf/routes.py`. The app is based on the `channels getting started guide +`_. + +To see if your websocket server is started, go to http://127.0.0.1:9000/. You should see a greeting +message from autobahn. + +Now, to get started, just open a browser and put the following into the JavaScript console +to test your new code:: + + socket = new WebSocket("ws://127.0.0.1:9000"); + socket.onmessage = function(e) { + alert(e.data); + } + socket.send("hello world"); + + +You should see an alert come back immediately saying "hello world" - your +message has round-tripped through the server and come back to trigger the alert. diff --git a/docs/index.rst b/docs/index.rst index 8cfc45e85..60cc1d33b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,6 +21,7 @@ Contents: settings linters live-reloading-and-sass-compilation + channels deployment-on-heroku deployment-with-docker faq diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index 107d6f98d..18e7837c2 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -100,6 +100,20 @@ def remove_task_app(project_directory): ) shutil.rmtree(task_app_location) + +def remove_channels_files(project_directory): + """Removes channels files if it isn't going to be used""" + routing_file = os.path.join( + PROJECT_DIRECTORY, + "config/routing.py" + ) + os.remove(routing_file) + channels_app_location = os.path.join( + PROJECT_DIRECTORY, + '{{ cookiecutter.repo_name }}/channelsapp' + ) + shutil.rmtree(channels_app_location) + # IN PROGRESS # def copy_doc_files(project_directory): # cookiecutters_dir = DEFAULT_CONFIG['cookiecutters_dir'] @@ -125,5 +139,9 @@ make_secret_key(PROJECT_DIRECTORY) if '{{ cookiecutter.use_celery }}'.lower() == 'n': remove_task_app(PROJECT_DIRECTORY) +# 2. Removes the taskapp if celery isn't going to be used +if '{{ cookiecutter.use_channels }}'.lower() == 'n': + remove_channels_files(PROJECT_DIRECTORY) + # 3. Copy files from /docs/ to {{ cookiecutter.repo_name }}/docs/ # copy_doc_files(PROJECT_DIRECTORY) diff --git a/{{cookiecutter.repo_name}}/config/routing.py b/{{cookiecutter.repo_name}}/config/routing.py new file mode 100644 index 000000000..178b826a0 --- /dev/null +++ b/{{cookiecutter.repo_name}}/config/routing.py @@ -0,0 +1,10 @@ +""" +This is an example for a channels routing config using the getting started guide at +https://channels.readthedocs.org/en/latest/getting-started.html +""" +channel_routing = { + "websocket.connect": "{{cookiecutter.repo_name}}.channelsapp.consumers.ws_add", + "websocket.keepalive": "{{cookiecutter.repo_name}}.channelsapp.consumers.ws_add", + "websocket.receive": "{{cookiecutter.repo_name}}.channelsapp.consumers.ws_message", + "websocket.disconnect": "{{cookiecutter.repo_name}}.channelsapp.consumers.ws_disconnect", +} diff --git a/{{cookiecutter.repo_name}}/config/settings/common.py b/{{cookiecutter.repo_name}}/config/settings/common.py index 0a5deebef..1c3741853 100644 --- a/{{cookiecutter.repo_name}}/config/settings/common.py +++ b/{{cookiecutter.repo_name}}/config/settings/common.py @@ -231,6 +231,17 @@ INSTALLED_APPS += ('kombu.transport.django',) BROKER_URL = env("CELERY_BROKER_URL", default='django://') ########## END CELERY {% endif %} +{% if cookiecutter.use_channels == 'y' -%} +########## CHANNELS +INSTALLED_APPS += ('channels',) +CHANNEL_BACKENDS = { + "default": { + "BACKEND": "channels.backends.database.DatabaseChannelBackend", + "ROUTING": "config.routing.channel_routing", + }, +} +########## END CHANNELS +{% endif %} # Location of root django.contrib.admin URL, use {% raw %}{% url 'admin:index' %}{% endraw %} ADMIN_URL = r'^admin/' diff --git a/{{cookiecutter.repo_name}}/config/settings/production.py b/{{cookiecutter.repo_name}}/config/settings/production.py index abf5fc990..e65d66573 100644 --- a/{{cookiecutter.repo_name}}/config/settings/production.py +++ b/{{cookiecutter.repo_name}}/config/settings/production.py @@ -286,5 +286,12 @@ LOGGING = { {% endif %} # Custom Admin URL, use {% raw %}{% url 'admin:index' %}{% endraw %} ADMIN_URL = env('DJANGO_ADMIN_URL') +{% if cookiecutter.use_channels == 'y' -%} +########## CHANNELS +# the redis channel is broken on channels==0.8. Use the default DB backend in prod for now +# CHANNEL_BACKENDS["default"]["BACKEND"] = "channels.backends.redis_py.RedisChannelBackend" +# CHANNEL_BACKENDS["default"]["HOSTS"] = [("redis-channel", 6379)], +########## END CHANNELS +{% endif %} # Your production stuff: Below this line define 3rd party library settings diff --git a/{{cookiecutter.repo_name}}/dev.yml b/{{cookiecutter.repo_name}}/dev.yml index d018f7cd2..0e3b579a9 100644 --- a/{{cookiecutter.repo_name}}/dev.yml +++ b/{{cookiecutter.repo_name}}/dev.yml @@ -8,10 +8,29 @@ postgres: django: dockerfile: Dockerfile-dev build: . + {% if cookiecutter.use_channels == 'y' -%} + # we need to use runserver when using channels because runserver starts a worker process + # automatically when channels is installed + command: python /app/manage.py runserver 0.0.0.0:8000 + {% else %} command: python /app/manage.py runserver_plus 0.0.0.0:8000 + {% endif %} volumes: - .:/app ports: - "8000:8000" links: - postgres + +{% if cookiecutter.use_channels == 'y' -%} +channelserver: + dockerfile: Dockerfile-dev + build: . + command: python /app/manage.py runwsserver + volumes: + - .:/app + ports: + - "9000:9000" + links: + - postgres +{% endif %} diff --git a/{{cookiecutter.repo_name}}/docker-compose.yml b/{{cookiecutter.repo_name}}/docker-compose.yml index dc6810372..572d8c658 100644 --- a/{{cookiecutter.repo_name}}/docker-compose.yml +++ b/{{cookiecutter.repo_name}}/docker-compose.yml @@ -41,3 +41,25 @@ celerybeat: - redis command: celery -A {{cookiecutter.repo_name}}.taskapp beat -l INFO {% endif %} + +{% if cookiecutter.use_channels == 'y' -%} +channelworker: + build: . + user: django + env_file: .env + command: python /app/manage.py runworker + links: + - postgres + - redis + +channelserver: + build: . + user: django + env_file: .env + command: python /app/manage.py runwsserver + ports: + - "0.0.0.0:9000:9000" + links: + - postgres + - redis +{% endif %} diff --git a/{{cookiecutter.repo_name}}/requirements/base.txt b/{{cookiecutter.repo_name}}/requirements/base.txt index 7e54c7265..ba85cece5 100644 --- a/{{cookiecutter.repo_name}}/requirements/base.txt +++ b/{{cookiecutter.repo_name}}/requirements/base.txt @@ -54,5 +54,9 @@ redis>=2.10.0 {% if cookiecutter.use_celery == "y" %} celery==3.1.20 {% endif %} +{% if cookiecutter.use_channels == 'y' -%} +channels==0.8 +autobahn[twisted] +{% endif %} # Your custom requirements go here diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/channelsapp/__init__.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/channelsapp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/channelsapp/consumers.py b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/channelsapp/consumers.py new file mode 100644 index 000000000..04d906569 --- /dev/null +++ b/{{cookiecutter.repo_name}}/{{cookiecutter.repo_name}}/channelsapp/consumers.py @@ -0,0 +1,17 @@ +""" +This is an example for a channels app using the getting started guide at +https://channels.readthedocs.org/en/latest/getting-started.html +""" +from channels import Group + +# Connected to websocket.connect and websocket.keepalive +def ws_add(message): + Group("chat").add(message.reply_channel) + +# Connected to websocket.receive +def ws_message(message): + Group("chat").send(message.content) + +# Connected to websocket.disconnect +def ws_disconnect(message): + Group("chat").discard(message.reply_channel)