A Real Django Project
By now, you hopefully have a solid understanding Git and Linux basics, and we can start applying that knowledge. In this chapter, we will create a new Django project, and push it to GitLab.
We will use direnv to manage our environmental variables, poetry to manage dependencies and virtual environment, and Git to track changes.
Pre-requisites
Before you begin this chapter, you should have:
- Git installed and configured, as per the instructions in the Using Git chapter.
- A GitLab account, with the polls repository created, as per the instructions in the previous chapter.
- Poetry, git, and direnv installed.
Installing pre-requisites
Arch Linux
If you need to install any of the pre-requisites on Arch Linux, you can do so via pacman
:
Ubuntu
In order to install the pre-requisites on Ubuntu, you can use the apt
package manager:
Gotchas
- Make sure you registered the direnv hooks in your shell, as per the instructions on the direnv website.
- Make sure you have configured your name and email in the Git config as per the Using Git chapter.
- Make sure your initial branch name is set to
main
rather thanmaster
.
Integrating Poetry
First, decide where you wish to create the project. I will use ~/projects/django-polls
. You can create the directory and open it as follows:
Info
The name django-polls
is arbitrary. Personally, I like using different names for the project root and the Django project itself. Also, note how it matches the name we used on GitLab in the previous chapter.
Next, initialize the Poetry project, and answer the questions when prompted (or leave them blank if you are satisfied with the default values):
$ poetry init
This command will guide you through creating your pyproject.toml config.
Package name [django-polls]: polls
Version [0.1.0]:
Description []: A Django Polls application.
Author [Your Name <your@email>>, n to skip]:
License []:
Compatible Python versions [^3.12]:
Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file
[tool.poetry]
name = "polls"
version = "0.1.0"
description = "A Django Polls application."
authors = ["Your Name <your@email>>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Do you confirm generation? (yes/no) [yes] yes
This will generate a pyproject.toml
file containing project metadata in the current directory.
Note that you could add packages, but I chose to skip it for now. This is simply a matter of personal preference. Naturally, this means we do not currently have Django installed in this project, so let's add it now, along with any other packages we might want. I will also be adding Django Classy Settings to easily work with environment variables.
Poetry will automatically create a virtual env for us, so we can omit using the venv
module.
Also, note that this created a poetry.lock
file containing file hash information for the project's dependencies. This allows to ensure that the same version of each dependency will be installed when we deploy the project to a production server.
Working with Git
It's important to commit your work often to ensure you can easily revert to a previous working state. As we have generated a few files, let's initialize our Git repo so we can track our current work:
Of course, no repository is complete without high quality documentation, so let's add a README.md
file. You can use any text or code editor to do that, such as VS Code, or neovim. Open the file in your editor and add some text in markdown format, e.g.:
This is very terse, and ideally, you should add more detailed information, including how to install and run the application, how others can contribute to your project if it is open-source, etc.
We are now ready to stage and commit our changes:
We can now push our project to the repository we created on GitLab. Make sure you use your own URL, as you will not have permissions to push to my repository. You can find it in the instructions of the empty repository created in the previous chapter.
The git remote add
command added a remote repo named origin
. We can then use that name as an argument to git push
. We also use the -u
flag so Git remembers it as the default remote for the current branch.
You can now refresh the open repository in your browser, and should see the files you committed, as well as the content of your README.
Integrating Django
Before we start working on a new feature, i.e. Django integration, it's a good idea to create a new branch in our repository to keep our main
branch clean. Let's switch to a new branch called feat/django
:
We can run commands in the Poetry managed virtual environment with poetry run
, but prefixing commands gets tedious quickly, so let's activate the virtual environment instead, then start our Django project:
Tip
Running poetry shell
is similar to sourcing the activate
script when using the venv
module.
Configuration via env vars
The default Django project template is a good starting point, but there are a few things we can improve.
To begin with, we should remove the default SECRET_KEY
setting, and read that along with the DEBUG
setting from an environment variable. Make the following changes to the file polls/settings.py
:
@@ -12,6 +12,12 @@
from pathlib import Path
+# Django Classy Settings imports
+from cbs import BaseSettings, env
+
+# Load env vars with the prefix DJANGO_
+denv = env["DJANGO_"]
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -19,11 +25,13 @@
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
-# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = "django-insecure-((*4a0lowtv%+fdgq)ybu=r((&)o3(xgt2*mc%3rq)l(i23up_"
-# SECURITY WARNING: don't run with debug turned on in production!
-DEBUG = True
+class Settings(BaseSettings):
+ """Settings for Django Polls."""
+
+ DEBUG = denv.bool(False)
+ SECRET_KEY = denv(env.Required)
+
ALLOWED_HOSTS = []
@@ -121,3 +129,6 @@
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+
+# Load CBS Settings
+__getattr__, __dir__ = BaseSettings.use()
Note
The first part of the changes are made near the top of the file, while the final two lines are added at the very end, below the last line.
With these changes, the denv
object can now read environment variables like DJANGO_DEBUG
and DJANGO_SECRET_KEY
. As we made the SECRET_KEY
setting required, we can try starting the server now, and see it fail:
$ python manage.py runserver
Traceback (most recent call last):
...
ValueError: Environment variable DJANGO_SECRET_KEY is required but not set.
We see a long error output, letting us know that our variable is not set. Let's fix that by defining it in our .envrc
file, allowing direnv to read it, and starting the server again:
$ echo export DJANGO_SECRET_KEY=django-insecure-local-dev-secret >> .envrc
direnv: error ~/projects/django-polls/.envrc is blocked. Run `direnv allow` to approve its content
$ direnv allow
direnv: loading ~/projects/django-polls/.envrc
direnv: export +DJANGO_SECRET_KEY
$ python manage.py runserver
CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False.
We now have a different error. As we set the default for the DEBUG
setting to False
, we either need to define a list of allowed hosts, or set the variable to enable DEBUG
. As the .envrc
file will only ever be used locally, let's use it to turn DEBUG
back on, and restart the server. Note that you will need to tell direnv to accept the new file after modifying it.
$ echo export DJANGO_DEBUG=True >> .envrc
$ direnv allow
$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s).
Run 'python manage.py migrate' to apply them.
Django version 5.1.2, using settings 'polls.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Success
You should now be able to see the server running on http://127.0.0.1:8000/.
Generating our database
One of Django's key features is its ORM, object-relational mapper, which allows us to define data models using Python classes. Django has a management command, makemigrations
, that generates migration files, and a migrate
command that applies them.
Django ships with some apps included, such as the django.contrib.auth
app that provides authentication, which is why it complained about some unapplied migrations.
As our project currently uses the SQLite database, applying migrations will automatically create a new database if needed.
SQLite is a single-file based database, and while it is often undervalued, it's perfectly fine to use, even in production, for most small to medium-sized projects. We will take a look at a different database, PostreSQL, in a later chapter, but for now, let's apply the migrations to our SQLite database. You can go back to the terminal where the Django server is running, and press ctrl+c
to stop the server, then run:
The message about unapplied migrations is now gone, and if we list the files in the current directory, we should now see a file named db.sqlite3
, which contains our data.
Keeping our repository clean
We have made a lot of progress, getting Django up and running, and generating our initial database. Now would be a great time to commit our changes, but first we want to ensure that we only commit things that belong in a repo. We can run git add
with the --dry-run
or -n
flag to see which files Git would add to staging.
$ git add -n .
add '.envrc'
add 'db.sqlite3'
add 'manage.py'
add 'polls/__init__.py'
add 'polls/__pycache__/__init__.cpython-312.pyc'
add 'polls/__pycache__/settings.cpython-312.pyc'
add 'polls/__pycache__/urls.cpython-312.pyc'
add 'polls/__pycache__/wsgi.cpython-312.pyc'
add 'polls/asgi.py'
add 'polls/settings.py'
add 'polls/urls.py'
add 'polls/wsgi.py'
We can see a few files we should never commit to Git, such as our .envrc
file, which can contain secrets, our database, and Python bytecode files. Create a new file named .gitignore
in the root of the repo, and use a code editor to give it the following content:
Save the file, and do another dry run:
$ git add -n .
add '.gitignore'
add 'manage.py'
add 'polls/__init__.py'
add 'polls/asgi.py'
add 'polls/settings.bak'
add 'polls/settings.py'
add 'polls/urls.py'
add 'polls/wsgi.py'
This looks much better. Let's commit our changes and push them to GitLab now:
Merging changes into main
As you can see, we pushed to a new branch named feat/django
on GitLab. We've contained our new feature entirely in this branch, both locally, and when pushing our changes. This is known as trunk-based development, where you work on small pieces of code, and frequently merge them into the main branch.
If you open your GitLab repository now, you should be given the option to create a merge request in a notification. Click the "Create merge request" button, and fill up the form.
You need to provide a title, which should describe the changes briefly. You can provide a description, which is especially important when working with others, and you can assign someone to manage the merge request or reviewers.
Assuming you are working by yourself at this time, just make sure to enter a title, and click the "Create merge request" button. You will be taken to a new page, where you can comment on the request, merge, or close the request (assuming you have the appropriate permissions).
Note that some merge requests may require extra work, e.g. due to conflicts, but as this one should say "Ready to merge!", you can go ahead, and click that "Merge" button.
Once done, the main branch on GitLab now contains the latest changes and the Django project.
It's time to fetch the changes to your local main branch.
Finally, we can check that the feat/django
branch has been merged, and delete it:
Adding our first Django app
Project layouts really come down to a matter of personal preference. The official Django tutorial recommends simply naming the app directory "polls", while some recommend skipping the app altogether, and using the project directory instead as the primary, or only, app.
One downside of the former is that it may cause conflicts if you are not careful with your app names. For example, the name, naming an app math
would conflict with the math module in the Python standard library, while my_school.math
should always work.
The Zen of Python
Namespaces are one honking great idea -- let's do more of those!
One downside of the latter is that it may not scale very well. Above a certain size, you may decide to split up your models.py
file into a models
package. You may then end up with a mixture of packages, some of which are apps, while others may be models, views, tests. etc.
For these reasons, I personally recommend namespacing the app inside of our polls
directory. As we already used "polls" for the project name, we can use the common name "core" for our first app that will contain the key models and other features of our Django project.
In order to namespace our app, we need to first create the directory, run the startapp
command, then change the app name in apps.py
.
Tip
Remember to use new branches when working on new features.
Open the file polls/core/apps.py
in your code editor, and make the following change:
@@ -3,4 +3,4 @@
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
- name = "core"
+ name = "polls.core"
You can now add "polls.core"
to the INSTALLED_APPS
list in settings.py
.
Adding models, views, etc.
By now, you should know how to use Git effectively, how to create feature branches, and merge them to main.
In order for your project to do useful things, you should now add models, views, tests, etc.
As this book is focused more on the Linux aspect than the Django aspect, you can learn all about models and views in the official Django tutorial, and I highly recommend you take a break from reading this, and complete at least parts one through three of the Django tutorial linked below.
Tip
Be sure to commit your changes regularly, and to always keep your main branch in a working state. Also, make sure to check which files would be committed, to keep your repository clean.
Conclusion
After reading this, and completing the first parts of the Django tutorial, you should be able to start building serious projects with Django, and feel comfortable using Git to manage your code.
In the next section, we will look at running Linux locally and in the cloud, and be one step closer to deploying your project to production.