Step 1 – Set up the project
Virtual Environment & Django apps
You can skip this if you don’t care about your default Python environment. I’m using virtualenvwrapper for convenience.
In your terminal:
1 2 3 4 |
mkvirtualenv channels-obstruction pip install django==1.9 pip install channels==0.17.2 |
Now that we have Django installed, create the new project and our main app called “game”. (django-admin.py will create the project folder in the current folder):
1 2 3 |
django-admin.py startproject channels_obstruction cd channels_obstruction django-admin.py startapp game |
OK, so we have our project stubbed out now. Let’s create a couple of folders that will hold our templates and static files. I’m just going to create these at the root of the project for simplicity.
1 2 3 |
#shorthand to create static folder and subfolders - no spaces in between the subs mkdir -p static/{js,img,css} mkdir templates |
Below, click to download the needed images for this tutorial. Unzip this into the img folder you just created.
You should now have simple, empty Django project with a structure like this:
We’ve already created our game app and installed Channels in our virtualenv, so let’s configure the base settings for it in our project. Overwrite all of your settings.py with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
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! SECRET_KEY = '76t_o&oi@e!^@9age&8)1=@0bb71a_(19=)%l4k-k&um96l+w3' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = ['*'] INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'game', 'channels', ] 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.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] ROOT_URLCONF = 'channels_obstruction.urls' 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 = 'channels_obstruction.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] LOGIN_REDIRECT_URL = '/lobby/' LOGIN_URL = '/login/' LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ] CHANNEL_LAYERS = { "default": { "BACKEND": "asgiref.inmemory.ChannelLayer", "ROUTING": "channels_obstruction.routing.channel_routing", }, } |
The key lines that we changed here are highlighted. You’ll see we’ve added our game app and the channels app to the INSTALLED_APPS dictionary (lines 21, 22), we added the path to our templates folder (line 41), and the CHANNEL_LAYERS setting at the bottom of the file (lines 100-105). The BACKEND is the basic Channels settings that are good for development, but not what you would want to use in production. Refer to the docs for more info.
We also need to create the file indicated on line 103. It will basically be empty at this point, but we need it to configure Channels. So, create a file called routing.py in the channels_obstruction directory. This file works very similarly to how the Django urls.py, but handles routes for the websocket calls from the client.
1 2 3 4 5 6 |
from channels.routing import route, route_class from channels.staticfiles import StaticFilesConsumer # routes defined for channel calls # this is similar to the Django urls, but specifically for Channels channel_routing = [] |
That’s really all of the config you need with Channels! Now, let’s add a homepage and some registration pieces to allow players to create accounts and log-in.
Start by creating a folder in our Game app folder called “views“. Often, you’ll only have one views.py file in each app. In this case, for clarity, I want to separate some of the views we’ll be using into two files. We only have one file now, but we’ll add another later. Inside of the new folder, create an __init__.py and a views.py file with the following content:
1 |
from .views import * |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from django.views.generic import CreateView, TemplateView from django.contrib.auth.forms import UserCreationForm from django.contrib.auth import authenticate, login class HomeView(TemplateView): template_name = 'home.html' class CreateUserView(CreateView): template_name = 'register.html' form_class = UserCreationForm success_url = '/' def form_valid(self, form): valid = super(CreateUserView, self).form_valid(form) username, password = form.cleaned_data.get('username'), form.cleaned_data.get('password1') new_user = authenticate(username=username, password=password) login(self.request, new_user) return valid |
In the views file, we just created two simple views: HomeView and CreateUserView. Each referenced template files that we don’t have, so let’s create those now. In your templates folder, create four files: base.html, home.html, login.html and register.html.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <!-- Optional theme --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous"> <!-- LOCAL CSS --> <link rel="stylesheet" href="{% static "css/game.css " %}"> {% block additional_head %}{% endblock %} </head> <body style="padding-top:65px;"> <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/">Channels Obstruction</a> </div> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> {% if request.user.is_authenticated %} <li><a href="/lobby/">Lobby</a></li> <li><a href="/logout/">Logout</a></li> {% else %} <li><a href="/register/">Register</a></li> <li><a href="/login/">Login</a></li> {% endif %} </ul> {% if request.user.is_authenticated %} <p class="navbar-text pull-right">Hi, {{ request.user }}</p> {% endif %} </div> </div> </nav> <div class="container" style="margin-bottom:50px;"> {% if messages %} {% for message in messages %} <div class="alert alert-{% if message.tags == 'error' %}danger{% else %}{{ message.tags }}{% endif %}" role="alert"> {{ message }} </div> {% endfor %} {% endif %} <!-- MAIN PAGE CONTENT --> {% block main_content %}{% endblock %} </div> <!-- PAGE LEVEL JAVASCRIPT --> {% block page_javascript %} {% endblock %} </body> </html> |
1 2 3 4 5 6 7 8 9 10 |
{% extends "base.html" %} {% block main_content %} <div class="row"> <div class="col-lg-12"> <h3>Welcome to Channels Obstruction!</h3> <hr> </div> </div> {% endblock %} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
{% extends "base.html" %} {% block main_content %} <div class="row"> <div class="col-lg-12"> <h3>Register</h3> <hr> <form action="#" method="POST"> {% csrf_token %} <fieldset> {% for field in form %} {% if field.errors %} <div class="control-group error"> <label class="control-label">{{ field.label }}</label> <div class="controls">{{ field }} <span class="help-inline"> {% for error in field.errors %}{{ error }}{% endfor %} </span> </div> </div> {% else %} <div class="control-group"> <label class="control-label">{{ field.label }}</label> <div class="controls">{{ field }} </div> </div> {% endif %} {% endfor %} </fieldset> <br> <div class="form-actions"> <button type="submit" class="btn btn-primary">Submit</button> </div> </form> </div> </div> {% endblock %} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
{% extends "base.html" %} {% block main_content %} <div class="row"> <div class="col-lg-12"> <h3>Login</h3> <hr> {% if form.errors %} <p class="text-danger">Your username and password didn't match. Please try again.</p> {% endif %} <form action="#" method="POST"> {% csrf_token %} <fieldset> {% for field in form %} {% if field.errors %} <div class="control-group error"> <label class="control-label">{{ field.label }}</label> <div class="controls">{{ field }} <span class="help-inline text-danger"> {% for error in field.errors %}{{ error }}{% endfor %} </span> </div> </div> {% else %} <div class="control-group"> <label class="control-label">{{ field.label }}</label> <div class="controls">{{ field }}</div> </div> {% endif %} {% endfor %} </fieldset> <br> <div class="form-actions"> <button type="submit" class="btn btn-primary">Submit</button> </div> </form> </div> </div> {% endblock %} |
Those four files are:
- base.html: The base template that all others will extend. It contains the css/js imports, the navbar at the top, some Django messages display HTML (lines 49-55) and some standard Django template blocks that child templates will use to insert page-specific content.
- home.html: This page isn’t really going to have a lot of content, but it will show to non-authenticated users.
- login.html: A simple login page
- register.html: A simple registration page
To style it up a (very) little bit, create the css file we referenced with:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
.games-list li { background: rgba(140, 199, 255, 0.63); font-weight:500;} .games-list li:nth-child(odd) { background: rgba(244, 244, 244, 0.41); } .games-list li .btn {margin-top:-5px;} #wrapper { border: 1px solid black; width: 450px; height: 450px; margin: 0 auto; } div { contentEditable: true; } .filled { width: 28px; height: 28px; background: black; border: 1px solid black; padding: 0; margin: 0; float: left; } .empty { width: 28px; height: 28px; border: 1px solid black; padding: 0; margin: 0; float: left; } .numbered { counter-increment: value; } .numbered:nth-of-type(n)::before { content: counter(value); font-size: 10px; position: absolute; } table{ border:2px solid #3b3a6e; } table td { position:relative; border:1px solid #3b3a6e; } .player-line{ font-size:20px; line-height:80px; margin:5px; } .coords{ top:0; left:0; position: absolute; z-index: 999; font-size: 10px; color: rgb(120, 120, 200); } .player-badge{ display:block; margin-right:15px; } .creator-color { background-color: #00ABD0; } .opponent-color { background-color: #FF4C22; } |
Now, let’s wire these up to the two views we created earlier. Modify the urls.py file like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
from django.conf.urls import url from django.contrib import admin from game.views import * from django.contrib.auth.views import login, logout urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^register/', CreateUserView.as_view()), url(r'^login/$', login, {'template_name': 'login.html'}), url(r'^logout/$', logout, {'next_page': '/'}), url(r'^$', HomeView.as_view()) ] |
So the base app is wired up and ready now. Run Django’s migrate to create the database tables and we’re set to run it at localhost and see our app:
1 2 3 |
python manage.py migrate python manage.py runserver 8080 |
Now, hit http://localhost:8080/ in your browser and you’ll see our amazing home screen.
You can also now register and log in. Go ahead and create two users that will be our Obstruction players. Note: right now we have the login redirecting to a page that doesn’t exist. We’ll add that soon.
hello,
when I run your code, but the web don’t show the game, what’s happend?
Are you going through the tutorial? Or are you running the example project from github?
Do you have a similar project but with channels 2.0? Do you plan to update this one?
Yes, I’m actually working on one now. Hopefully I’ll have it ready in October, but my free time is tight right now.
I am getting an error, “ImportError: cannot import name ‘Group'”, while trying to import Group from channels in models.py. I am using channels 2.0.2. Please help.
Yeah, this tutorial is based on a very early version of channels – 0.17, I believe. Version 2.0.2 has quite of few changes from that old version, including requiring Python3. Here is an issue on the official github that deals with the error you’re seeing: https://github.com/django/channels/issues/989
I will try to get the tutorial updated to Django 2.0, Python 3, and the latest channels version soon.
Hey,
I’m trying to follow your tutorial, but I keep running into the following error:
OSError at /lobby/
Error reading [PATH]\webpack-stats.json. Are you sure webpack has generated the file and the path is correct?
Any idea how to fix it?
Sorry, I didn’t reply sooner. If the file is being generated, check your STATS_FILE setting under WEBPACK_LOADER in settings.py. It’s possible that it isn’t pointing to the correct location for the file.
If the file isn’t being generated, webpack may not be running properly. Are you seeing errors when you run wepback?
That is very good, I was making similar stuff as a video on my language and this sharing is very useful.
Power of sharing, thanks again.
When will the index.jsx be called ?
The index.jsx files are bundled with webpack (set on lines 8 and 9 in webpack.config.js). Those are the bundles that are loaded with the Django Webpack Loader and rendered on the related html pages (lobby.html and game.html).
I got a few questions to make the game a little bit more complex
Is it possible to allow to the user give a name for the room?
Is it possible to allow more than 2 players? (obviously adding the correct img stuff)
If the answer from above is yes, can the user choose a “limit” for ppl to connect before starting the game?
Cheers!
Yes, each of those suggestions would work just fine:
Thanks for this django/react tutorial. It is awesome helping me get my head around React and putting all the pieces together!!
Great to hear! I’m glad you found it useful. Thanks for letting me know.
Thank you so much for writing this tutorial!
From your description, it seems like moves and player messages should update automatically, however when I run it, they only update after a manual refresh of the page. Am I misunderstanding how the website works?
Thanks
The log should update when new chat messages are created or moves are made. I would check to make sure the game.send_game_update() method is being called at the appropriate times. Also, check the browser console for any errors on the client-side. That may give you more of a clue to the issue as well.
Thanks for writing this tutorial! However, some things that may be confusing to a beginner:
startapp game
is going to create a game/views.py by default, and anyone that forgets to move views.py is going to have a package conflict. Also,from views import *
inside __init__.py should befrom .views import *
just to avoid any namespace mismatches. I recommend you keep the default views.py.Thanks for your comments, Daniel. Yes, I thought that splitting the views could be confusing, but it’s something I like to do to organize views of different types. So here, I didn’t want to combine the DRF API views with the “standard” Django views. Also, the tutorial wasn’t exactly intended for beginners, but maybe I can clarify the views split a little more in the post. Also, thanks for catching the import fix. I had already updated the imports in the git project, but missed it in the post.
Hello! Great tutorial! I have a question, I want to use foundation-sites in my project. I am following you tutorial and instead of using bootstrap precompiled css I would like to install foundation with bower maybe? I know how to use the “foundation new” command to create a new project but I would like not to create a new project but integrate foundation sites with mine!
Hey David, sorry I haven’t worked with Foundation yet. But it looks like you can just use the CSS itself and avoid the CLI site generation: http://foundation.zurb.com/sites/download/
You could just include this as you would any other CSS, and my guess is that if you install the full Foundation package with NPM, you could just reference the CSS there as well.
not sure if it’s a django versioning thing or what, but on page 1 of this tutorial you are no longer allowed to specify views with strings and they must be callable, suggested edit follows:
original:
from django.conf.urls import url
from django.contrib import admin
from game.views import *
urlpatterns = [
url(r’^admin/’, admin.site.urls),
url(r’^register/’, CreateUserView.as_view()),
url(r’^login/$’, ‘django.contrib.auth.views.login’, {‘template_name’: ‘login.html’}),
url(r’^logout/$’, ‘django.contrib.auth.views.logout’, {‘next_page’: ‘/’}),
url(r’^$’, HomeView.as_view())
]
edit:
from django.conf.urls import url
from django.contrib import admin
from django.contrib.auth.views import login, logout
from game.views import *
urlpatterns = [
url(r’^admin/’, admin.site.urls),
url(r’^register/’, CreateUserView.as_view()),
url(r’^login/$’, login, {‘template_name’: ‘login.html’}),
url(r’^logout/$’, logout, {‘next_page’: ‘/’}),
url(r’^$’, HomeView.as_view())
]
Yep – you’re right, thanks for letting me know! I’ve updated the urls.py code.
Thank you so much, one of the most complete tutorials I have seen. Not all persons are willing to teach this things together, and the complexity of the scenario gives us good bases. This types of tutorials (even paid) are hard to find. Again, thank you.
Thank you for the nice comments! I’m glad you found the tutorial helpful.
Can you please show one example on how I can make api post call from react to django drf?
Is there any reason why all “post” calls are done via sockets not api in this tutorial?
You can see a few examples of calls from React to the DRF backend in my post. For example, take a look at the getGame() method on the GameBoard.jsx component. That method calls the DRF SingleGameViewSet endpoint to get game details.
And as I mentioned in the post, I tried to mix up different ways of getting data from the Django backend to the React frontend. I wanted to show different ways of achieving the same thing: sending data through the standard Django response via context, DRF calls to the backend, and Django Channels websocket calls. In reality, this isn’t how I would structure a production app, but I was hoping it would be informative. Hopefully not confusing at the same time.
Wow! Thanks again for sharing this tutorial. I am amazed by your generosity. The tutorial is intense.
Let me give some suggesting for how you can improve it. I found that very often the flow of tutorial is going from big concepts (code snippets) to a smaller ones. For example several times you are first putting together some views, react components or api_views and then go to show some serializers, consumers, routers, urls and so on. This can be sometimes confusing, since a student can receive error messages that those small parts are not yet existed. I think going from small concepts to bigger would be more easily to understand. Also, please check your github code. I think it does not work if you just download it and want to use. Several imports are configured improperly (signal in apps.py, for instance)
Thank you again!
Thanks for suggestions. Yes this was my first large tutorial so it definitely could be optimized and improved. I did the GitHub project well before the post, but it worked for me when I last tried it. It could be a python2/python3 import issue. I’ll update that tonight and get it working.
not sure if the instruction on page 8 is correct
class ClaimSquareView(APIView):
def get_object(self, pk):
try:
return Game.objects.get(pk=pk)
except Game.DoesNotExist:
raise Http404
def put(self, request, pk):
game = self.get_object(pk)
# update the owner
print(game)
return Response(serializer.errors)
– no import for Http404
– serializer is not defined
You’re right! That view isn’t even needed… I think I started going that direction to claim a square, but moved it to a Channels call using the consumer instead. I’ve removed that reference and the url reference.
Thank you very much for your suggestions and bug reports! I’ve added you to the “Thank you” section at the bottom of the post.
on page 7 views.py also should import
from django.contrib import messages
Added it, thank you.
Also, in my setup in view/__init__.py instead of
from views import *
from api_views import *
I need to enter
from .views import *
from .api_views import *
Yes this is probably because you’re on Python 3 and implicit relative imports like that won’t work. I’m on 2.7 and they work with it. Thanks for pointing that out. I’ll update the post to note this.
on the page 2, it is very important to highlights this setting in the settings.py
STATICFILES_DIRS = [
os.path.join(BASE_DIR, “static”),
]
it is not something that is added by default if start project with django-admin tools
Thanks, yes when I first talk about the settings file, I recommended overwriting all of the default code with what I show in the post. I’ll make sure that it’s more clear.
Thanks, confirm MIDDLEWARE_CLASSES fixed the issue with ‘AsgiRequest’ object has no attribute ‘session’
after following all instruction on the page 4, cannot login, getting this error
AttributeError at /login/
‘AsgiRequest’ object has no attribute ‘session’
Request Method: POST
Request URL: http://127.0.0.1:8080/login/
Django Version: 1.9.12
Exception Type: AttributeError
Exception Value:
‘AsgiRequest’ object has no attribute ‘session’
Exception Location: /home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/django/contrib/auth/__init__.py in login, line 101
Python Executable: /home/gideon/virtualenvs/vEnv_my_obstruct_game/bin/python
Python Version: 3.4.3
Python Path:
[‘/home/gideon/PycharmProjects/my_obstruct_game’,
‘/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/setuptools-18.1-py3.4.egg’,
‘/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/pip-7.1.0-py3.4.egg’,
‘/home/gideon/PycharmProjects/my_obstruct_game’,
‘/usr/lib/python3.4’,
‘/usr/lib/python3.4/plat-x86_64-linux-gnu’,
‘/usr/lib/python3.4/lib-dynload’,
‘/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages’]
Server time: Mon, 6 Feb 2017 15:53:42 +0000
Traceback Switch to copy-and-paste view
/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/django/core/handlers/base.py in get_response
response = self.process_exception_by_middleware(e, request) …
▶ Local vars
/home/gideon/virtualenvs/vEnv_my_obstruct_game/lib/python3.4/site-packages/channels/handler.py in process_exception_by_middleware
return super(AsgiHandler, self).process_exception_by_middleware(exception, request)
One thing that could cause this with Django 1.9+ is if you have MIDDLEWARE instead of MIDDLEWARE_CLASSES in your settings.py file. Can you check that?
It seems there is an error in this instruction
npm install –save-dev react react-dom webpack webpack-bundle-tracker babel-core babel babel-loadernpm babel-preset-es2015 react-websocket babel-preset-es2015 babel-preset-react jquery
“babel-loadernpm” should read “babel-loader”
Yep, you’re right – a little copy-paste issue on my part. It’s fixed now. Thanks for letting me know!
I have not yet finished your tutorial, but for what I see I can tell you huge THANK YOU!
Thank you, I hope it you find it useful. Please let me know if you have any issues!