So let’s get started ! ¶
Introduction ¶
To start, let’s clone our starter code, build our docker image, and run migrations.
Edit docker-compose version: “3.3”
git clone -b boilerplate --single-branch https://github.com/Jonathan-Adly/htmx-tictactoe.git
✦ ❯ git diff
diff --git a/docker-compose.yml b/docker-compose.yml
index 9f65e04..1766eb8 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,4 +1,4 @@
-version: "3.9"
+version: "3.3"
docker-compose up -d --build
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
33e99a10e6b4 htmx-tictactoe_web "python manage.py ru…" 11 seconds ago Up 11 seconds 0.0.0.0:8000->8000/tcp, :::8000->8000/tcp htmx-tictactoe_web_1
23cdc5808dd3 postgres:11 "docker-entrypoint.s…" 14 minutes ago Up 11 seconds 5432/tcp htmx-tictactoe_db_1
docker-compose exec web python manage.py migrate
❯ docker-compose exec web python manage.py migrate
Operations to perform:
Apply all migrations: account, accounts, admin, auth, contenttypes, sessions, sites, socialaccount
Running migrations:
Applying contenttypes.0001_initial... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0001_initial... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying accounts.0001_initial... OK
Applying account.0001_initial... OK
Applying account.0002_email_max_length... OK
Applying accounts.0002_auto_20211101_0104... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying sessions.0001_initial... OK
Applying sites.0001_initial... OK
Applying sites.0002_alter_domain_unique... OK
Applying socialaccount.0001_initial... OK
Applying socialaccount.0002_token_max_lengths... OK
Applying socialaccount.0003_extra_data_default_dict... OK
http://0.0.0.0:8000/accounts/signup/ ¶

Our starter code is a Django project with three applications.
The first is an accounts application that uses django-allauth for authentications.
If you went through the previous tutorials (I highly recommend) , it is the same. The only difference is we added a couple of new fields to our user model.
To represent the “symbol” that our users would use ( “X” vs. “O”), we added a Charfield with a default of “X”.
The interesting part though was adding a nested ArrayField to represent our game board. ArrayField is a PostgreSQL specific field that allows us to store lists in our database.
ArrayField is a PostgreSQL specific field that allows us to store lists in our database.
We would represent our board with something like this:
board = [
[0,0,0],
[0,0,0],
[0,0,0]
]
Where 0 is an empty cell, 1 would be a cell filled by “X”, and 2 would be a cell filled by “O”.
This allows us to use any symbol we want, not just “X” and “O”.
Also, ArrayField data types have to be the same. So we can’t have an integer and a string in the same board.
When an X player picks a board cell, let’s say the middle cell. Our board will change as such:
board = [[0,0,0][0,1,0][0,0,0]]
Then, the computer move might change the board as follow,:
board = [[0,0,0][0,1,2][0,0,0]]
And so on.
There are three things we need when using an ArrayField.
A base_field that represents the type of data in the list.
For example, a list of [0,1,2] would have an IntegerField as its base. Another list of [“a”,”b”,”c”] would have a CharField as its base field. The base_field could be another ArrayField, giving us a nested ArrayField which we will use in our project to represent our board.
We can’t have an array of [1, “a”], the type of data has to stay the same If we are to give our field a default, it should be a callable or a function that returns a list. A default of [] is not recommended. Lastly, if we decide to nest our arrays, PostgreSQL requires that the arrays be rectangular.
In other words, you can’t have one array with three values and another with one value.
accounts/models.py ¶
Config application ¶
The config application doesn’t have any changes from our minimal boilerplate tutorial (you can read it modern-django-boilerplate ( https://htmx-django.com/blog/a-minimalistic-modern-django-boilerplate )).
We just added our tictactoe application to the APPLICATION_LIST and its URLS to the urls.py module
config/settings.py ¶
1"""
2Django settings for config project.
3
4Generated by 'django-admin startproject' using Django 3.2.8.
5
6For more information on this file, see
7https://docs.djangoproject.com/en/3.2/topics/settings/
8
9For the full list of settings and their values, see
10https://docs.djangoproject.com/en/3.2/ref/settings/
11"""
12
13from pathlib import Path
14
15# Build paths inside the project like this: BASE_DIR / 'subdir'.
16BASE_DIR = Path(__file__).resolve().parent.parent
17
18
19# Quick-start development settings - unsuitable for production
20# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
21
22# SECURITY WARNING: keep the secret key used in production secret!
23SECRET_KEY = "django-insecure-(&r4r!@4m*dl^bwdo4g)h+v+3ba=ck2gx(0^@_@vsydhaj%rr^"
24
25# SECURITY WARNING: don't run with debug turned on in production!
26DEBUG = True
27
28ALLOWED_HOSTS = ["0.0.0.0"]
29
30
31# Application definition
32
33INSTALLED_APPS = [
34 "django.contrib.admin",
35 "django.contrib.auth",
36 "django.contrib.contenttypes",
37 "django.contrib.sessions",
38 "django.contrib.messages",
39 "django.contrib.staticfiles",
40 "django.contrib.sites",
41 "allauth",
42 "allauth.account",
43 "accounts",
44 "allauth.socialaccount",
45 "tictactoe",
46]
47
48MIDDLEWARE = [
49 "django.middleware.security.SecurityMiddleware",
50 "django.contrib.sessions.middleware.SessionMiddleware",
51 "django.middleware.common.CommonMiddleware",
52 "django.middleware.csrf.CsrfViewMiddleware",
53 "django.contrib.auth.middleware.AuthenticationMiddleware",
54 "django.contrib.messages.middleware.MessageMiddleware",
55 "django.middleware.clickjacking.XFrameOptionsMiddleware",
56]
57
58ROOT_URLCONF = "config.urls"
59
60TEMPLATES = [
61 {
62 "BACKEND": "django.template.backends.django.DjangoTemplates",
63 "DIRS": [str(BASE_DIR.joinpath("templates"))],
64 "APP_DIRS": True,
65 "OPTIONS": {
66 "context_processors": [
67 "django.template.context_processors.debug",
68 "django.template.context_processors.request",
69 "django.contrib.auth.context_processors.auth",
70 "django.contrib.messages.context_processors.messages",
71 ],
72 },
73 },
74]
75
76WSGI_APPLICATION = "config.wsgi.application"
77
78
79# Database
80# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
81
82DATABASES = {
83 "default": {
84 "ENGINE": "django.db.backends.postgresql", # changed from sqlite
85 "NAME": "postgres", # development settings, will change in production
86 "USER": "postgres", # development settings, will change in production
87 "PASSWORD": "postgres", # development settings, will change in production
88 "HOST": "db",
89 "PORT": 5432,
90 }
91}
92
93# Password validation
94# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
95
96AUTH_PASSWORD_VALIDATORS = [
97 {
98 "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
99 },
100 {
101 "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
102 },
103 {
104 "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
105 },
106 {
107 "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
108 },
109]
110
111
112# Internationalization
113# https://docs.djangoproject.com/en/3.2/topics/i18n/
114
115LANGUAGE_CODE = "en-us"
116
117TIME_ZONE = "UTC"
118
119USE_I18N = True
120
121USE_L10N = True
122
123USE_TZ = True
124
125
126# Static files (CSS, JavaScript, Images)
127# https://docs.djangoproject.com/en/3.2/howto/static-files/
128
129STATIC_URL = "/static/"
130
131# Default primary key field type
132# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
133
134DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
135# in boilerplate/config/settings.py
136# all auth settings
137
138SITE_ID = 1 # all-auth supports multiple sites
139
140
141AUTHENTICATION_BACKENDS = [
142 "django.contrib.auth.backends.ModelBackend", # this how admins will log in to the admin site
143 "allauth.account.auth_backends.AuthenticationBackend", # this how users log in
144]
145LOGIN_REDIRECT_URL = "home" # change to desired url name
146LOGOUT_REDIRECT_URL = "home" # change to desired url name
147ACCOUNT_SESSION_REMEMBER = True # remember user via sessions
148ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False # preferred UX
149ACCOUNT_USERNAME_REQUIRED = False # preferred UX
150ACCOUNT_AUTHENTICATION_METHOD = "email"
151ACCOUNT_EMAIL_REQUIRED = True # required for email authentication
152ACCOUNT_UNIQUE_EMAIL = True # required for email authentication
153ACCOUNT_EMAIL_VERIFICATION = "optional" # can use as a welcome email as well
154ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 5
155ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 5
156ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 86400
157ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https"
158AUTH_USER_MODEL = "accounts.CustomUser"
159
160# email
161# email will go to console for now, need to change in production
162EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
config/urls.py ¶
1from django.contrib import admin
2from django.urls import path, include
3
4
5urlpatterns = [
6 path("admin/", admin.site.urls),
7 path("accounts/", include("allauth.urls")),
8 path("", include("tictactoe.urls")),
9]
Tic-Tac-Toe ¶
Our last application has only 1 view/endpoint that returns a home template.
As before, we are using a base template and extending it for use in our home. Also, we have a component directory that holds a navbar.
The base template has a CSS style tag in the head. This handles our game board look and feel. It is from the React official tutorial with no changes. The “include” block renders our navbar.
(if you have trouble following, I highly recommend going through the previous tutorials in that course. We already went through the step-by-step process for building those patterns).
The interesting part is in the home template. Here is how we are rendering the game board.
{% for i in request.user.board %}
<div class="board-row">
{% for j in i %}
<button
class="square"
>
</button>
{% endfor %}
</div>
{% endfor %}
We are taking advantage of the magic of Django templates to dynamically render our game board based on the user board. The end result is a 3x3 grid with each cell containing a button(square).
Something like this.

Now that we went over our starter code. Let’s go ahead and work on our user stories. You can see the code ( https://github.com/Jonathan-Adly/htmx-tictactoe/tree/boilerplate ) for this part here.