Tero's Classy Django Cheatsheet

Develop backend websites, quickly. Django 4 cheatsheet.

This is a beta version of the cheatsheet, not a tutorial.

I recommend you read and do a tutorial first:

Karvinen 2022: Django 4 Instant Customer Database Tutorial

I updated this published version from my cheatsheets I've used for a long time. This is not a tutorial, this is useful only if you already know some Django or are using some additional material. This is a beta version, which means that there could be bugs and lack of polish.

Benefits: Backend

  • Supports all operating systems and architectures. Even your "smart" TV or fridge. If it has a browser, it works. For the client, the pages are just like any other web page.
  • Clients can only use the latest version. No more "we fixed that bug in the 90's, could you please upgrade before filing another ticket...".
  • Logging done by web server automatically.
  • Many users can edit the same data. Most enterprise apps require this: a web shop, a web forum, a customer relationship management tool, a grading tool...
  • Server side validation is always required for security anyway. Clients can easily modify and fake any data they send from the browser. Obviously, server side frameworks validate on server side.

Benefits: Django

  • Python - simple and guides to clean way to code
  • Very little to write - especially with the style used in this cheatsheet
  • Widely used, lot of help available in search engines and Stack Overflow.
  • Use boring tech to build interesting apps. Python 1991- (five years older than Java), Django 2005- (seventeen already)
  • Used by the big guys in production. Instagram and Oda use Django. Youtube and Facebook use Python.

Generic Class Based Views means easy

I can't understand why many books, videos and tutorials show the easiest thing last - or not at all. Most stuff is very short to write and easier to get right when using CreateView, ListView, UpdateView and DeleteView.

You can also use class based views for users.

This cheatsheet uses Generic Class Based Views.

Helpful packages and tools

In Debian 11 and similar Linuxes

$ sudo apt-get update
$ sudo apt-get install micro ipython3

Micro settings

$ micro

In micro, open command bar ctrl-E to give settings. Don't autoclose quotes, use a simple theme and don't force space indenting in Python.

set autoclose false,
set colorscheme simple
set ftoptions false

Install Django

Virtualenv keeps pip installed packages separated. Also use packages from package manager (--system-site-packages). This is convenient with IPython (better Python shell) and psycopg2 (PostgreSQL database driver) Virtualenv is not a security mechanism, you have to be careful what you install with 'pip.

$ mkdir -p django/; cd django/
$ virtualenv -p python3 --system-site-packages env/
$ source env/bin/activate

$ echo "django" > requirements.txt
$ pip install -r requirements.txt

$ django-admin version
4.2.1

Start a project for a website & Hello world

$ django-admin startproject terokarvinencom

This generates terokarvinencom/ folder with some premade files. All other project management uses './manage.py'.

$ cd terokarvinencom
$ ./manage.py runserver 	# development only, do not show on the Internet

Now browse to http://localhost:8000 to see django's default page. Hello world!

Always keep your development server './manage.py runserver' on your local machine only - it must not be exposed to the Internet. For a production setup, see Karvinen 2022:Deploy Django 4 - Production Install. But development server is great for development.

If you're running development server inside vagrant virtual machine and using port forwarding, you can make dev server visible to all URLs with './manage.py runserver 0.0.0.0:8000'.

You code goes to your App

$ ./manage.py startapp todo
$ micro settings.py 	# must also add your new app to INSTALLED_APPS

settings.py

INSTALLED_APPS = [
	# ...
	'django.contrib.staticfiles',
	'todo',
]

CLI is my IDE - Developing on the Command Line

Micro opens instantly. You don't need to keep it open like an IDE.

Usually, stay on the folder where you can see "manage.py" with 'ls'. This way, it's easy to see the folder structure. You can press up-arrow or CTRL-R for previous commands.

To see your logs, you can keep './manage.py runserver' on a separate terminal window. If you're more advanced, 'tmux', ctrl-B " (ctrl-B double-quote), ctrl-B--down and ctrl-B--up to change size. Ctrl-B, up to jump between panes.

All paths from now on are relative to the folder containing "manage.py"

My first page - URLs, Views, Templates

Yes, there is another terokarvinencom/ inside terokarvinencom/. This is the project wide settings directory. When an app is set up, you have to include its urls.py once in the global urls.py.

terokarvinencom/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
	path('admin/', admin.site.urls),
	path('', include('todo.urls')),
]

Any time you create a new thing, it's URL, view and template.

todo/urls.py

from django.urls import path
from .views import *

urlpatterns = [
		path('about', AboutView.as_view()),
]

todo/views.py

from django.views.generic import TemplateView

class AboutView(TemplateView):
		template_name = "todo/about.html"

For you new app, you should create the templates folder. Yes, the path mentions app name twice.

$ mkdir -p todo/templates/todo/

You only need to do HTML boilerplate once

todo/templates/todo/base.html

<!doctype html>
<html lang=en>
  <head>
    <title>Tero's Todo</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width; initial-scale=1">
  </head>
  <body>
   <header>
    <a href="https://TeroKarvinen.com/2023/django-cheatsheet/">Cheatsheet</a>
   </header>
   {% block "content" %}
     <h1>Tero's Todo</h1>
   {% endblock "content" %}
  </body>
</html>

Doctype makes it HTML5. Charset UTF-8 makes special characters like Ä and Ö work. Viewport makes font readable on mobile.

Template tags {% block "content" %} {% endblock "content" %} mark the area that will be replaced by each page. Yes, you never need to write this boilerplate again in your app.

todo/templates/todo/about.html

{% extends "todo/base.html" %}

{% block "content" %}
	<p>See you at TeroKarvinen.com</p>
{% endblock "content" %}

ORM - Your models create database, forms, validators...

Yes, it's true. You create a model, Django will do a lot for you: tables in database, some client side validators for convenience, server side validators for security, automatic forms.

from django.db import models

class Task(models.Model):
      name = models.CharField(max_length=160)

      def __str__(self):
              return self.name

Django has migrations, which make it convenient to modify production database when you are happy with your changes in the development version.

Any time you modify the models

$ ./manage.py makemigrations; ./manage.py migrate

Free web admin

If you add your model to admin, you have a free web interface to edit it.

todo/admin.py

	from django.contrib import admin
	from .models import *

	admin.site.register(Task)

Add a superuser, always use a good password, never use a bad password. Generate your password with 'pwgen -s 30 1' or Firefox or your password manager.

$ ./manage.py createsuperuser

Now edit your database in http://localhost:8000/admin

Field types

from django.db import models
from datetime import datetime

class Task(models.Model):
	name = models.CharField(max_length=160)
	
	created = models.DateTimeField(auto_now_add=True, null=True)
	modified = models.DateTimeField(auto_now=True, null=True)

	plan = models.TextField(blank=True)
	
	task_email = models.EmailField(max_length=255, blank=True)
	cost = models.FloatField(blank=True, null=True)
	done = models.BooleanField(default=False)
	postponed_cound = models.IntegerField(default=0)
	
	def __str__(self):
		return self.name

Allowing empty values

  • blank=True allows empty form field to be submitted.
  • null=True means allow NULL values in database column
  • Only blank=True is needed (and recommended) for string fields, where correct empty value is empty string "".
  • Number fields allowing empty values usually set both null=True and blank=True together

So form blank, database NULL.

Primary key "pk" is defined automatically. It's the integer to identify each field.

When? Use manage.py shell or code to see created and modified

Web admin does not show automatic dates by default. But you can use them in code and see them in shell:

$ ./manage.py shell -i=ipython
In [6]: Task.objects.all()[0].__dict__
Out[6]: 
{'_state': <django.db.models.base.ModelState at 0x7f8e4c85b3d0>,
 'id': 1,
 'name': 'Sing',
 'created': datetime.datetime(2023, 5, 21, 14, 38, 13, 70041, tzinfo=datetime.timezone.utc),
 'modified': datetime.datetime(2023, 5, 21, 14, 38, 13, 80112, tzinfo=datetime.timezone.utc),
 'plan': '',
 'task_email': '',
 'cost': None,
 'done': False,
 'postponed_cound': 0}

CRUD - Create, Read, Update, Delete

todo/urls.py

	from django.urls import path
	from .views import *

	urlpatterns = [
		path('', TaskListView.as_view()),
		path('task/new', TaskCreateView.as_view()),
		path('task/<int:pk>/delete', TaskDeleteView.as_view()),
		path('task/<int:pk>', TaskUpdateView.as_view()),
	]

todo/views.py

	from django.views.generic import TemplateView, ListView, UpdateView, CreateView, DeleteView
	from .models import *

	class TaskListView(ListView):
		model = Task

	class TaskUpdateView(UpdateView):
		model = Task
		fields = ["name"]
		success_url = "/"

	class TaskCreateView(CreateView):
		model = Task
		fields = ["name"]
		success_url = "/"

	class TaskDeleteView(DeleteView):
		model = Task
		success_url = "/"

Default template name for each view is shown when you define the view, try running it and read the error message.

All lists are pretty much the same, "for object in object_list" and using whiskers "object.name" to display the fields.

To see all variables available to template "{% debug %}".

todo/templates/todo/task_list.html

{% extends "todo/base.html" %}

{% block "content" %}
	{% for object in object_list %}
		<p><a href="/task/{{ object.pk }}">{{ object }}</a></p>
	{% endfor %}

	<p><a href="/task/new">+ New Task</a></p>
{% endblock "content" %}

All forms are pretty much the same. When using class based views, even login and register forms look the same. Mostly, it's the text on the submit button that changes.

todo/templates/todo/task_form.html

{% extends "todo/base.html" %}

{% block "content" %}
        <form method=post>
                {% csrf_token %}
                {{ form.as_p }}
                <input type=submit value="Save">
        </form>

        {% if object %}
	        <p><a href="/task/{{ object.pk }}/delete">Delete</a></p>
        {% endif %}
{% endblock "content" %}

Confirm delete is like any form, but it doesn't have the form.as_p.

todo/templates/todo/task_confirm_delete.html

{% extends "todo/base.html" %}

{% block "content" %}
        <p>Permanently delete "<b>{{ object }}</b>". This operation cannot be undone.</p>

        <form method=post>
                {% csrf_token %}
                <input type=submit value="Delete">
        </form>
        <p><a href="/">Cancel</a></p>
{% endblock "content" %}

User authentication

User authentication is easy when we use class bases views and even generic class based views.

A generic form template works when you use class based views.

Request is self.request inside a class based view.

Mixin goes to the left when declaring a class.

You must prevent users from seeing each others private data. You must verify this on the server side, clients can modify data with Firefox F12 or a MITM proxy like OWASP ZAP. So update and delete need UserPassesTestMixin - it's not enough to be any logged-in user.

todo/urls.py

from django.urls import path, include
from django.contrib.auth.views import LoginView, LogoutView
from .views import *

urlpatterns = [
	path('about', AboutView.as_view()),
	path('', TaskListView.as_view()),
	path('task/new', TaskCreateView.as_view()),
	path('task/<int:pk>/delete', TaskDeleteView.as_view()),
	path('task/<int:pk>', TaskUpdateView.as_view()),
	
	path('register', RegisterView.as_view()),
	path('login', LoginView.as_view(next_page="/")),
	path('accounts/login/', LoginView.as_view(next_page="/")),
	path('logout', LogoutView.as_view(next_page="/")),
]

todo/views.py

from django.views.generic import TemplateView, ListView, UpdateView, CreateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.forms import UserCreationForm
from django.contrib.messages.views import SuccessMessageMixin

from .models import Task

class TaskListView(LoginRequiredMixin, ListView): # mixin must be on left side
	model = Task

	def get_queryset(self):
		return Task.objects.filter(owner__username=self.request.user)

class TaskCreateView(LoginRequiredMixin, CreateView):
	model = Task
	fields = ["name"]
	success_url = "/"

	def form_valid(self, form):
		form.instance.owner = self.request.user
		return super().form_valid(form)

class TaskUpdateView(UserPassesTestMixin, UpdateView):
	model = Task
	fields = ["name"]
	success_url = "/"

	def test_func(self):
		object = self.get_object()
		return self.request.user == object.owner

class TaskDeleteView(UserPassesTestMixin, DeleteView):
	model = Task
	success_url = "/"

	def test_func(self):
		object = self.get_object()
		return self.request.user == object.owner

User Authentication

class RegisterView(SuccessMessageMixin, CreateView):
	form_class = UserCreationForm
	template_name = "registration/register.html"
	success_url = "/login"
	success_message = "Registration accepted. You can now log in. "

Django Shell with iPython

Ipython completes with tab, and shows help with "?" and sources with "??".

Interactive shell (REPL) is convenient when building searching with Django's own query language.

Ipython can be installed with package manager, because we created virtualenv with --system-site-packages.

sudo apt-get update                                                             
sudo apt-get install ipython3      

./manage.py shell -i ipython

iPython commands:

ls		# regular shell commands
from cars.models import Car		# tab completes, tab-tab-tab shows options
Car??	# source code, including fields
Car._meta.get_fields()	# fields, including foreign
Car? 	# short help

Car.objects.all()	# all records, like ListView does with model=Car

Car.objects.filter(name__contains="a").filter(name__contains="ini") # QuerySet, many
Car.objects.filter(name__contains="a").filter(name__contains="ini")[0].name # first

TeroKarvinen.com/django

See also

Django 4.2 Official Documentation

Classy Class Based Views for Django 4.2

Karvinen 2022:Deploy Django 4 - Production Install

TeroKarvinen.com - Articles tagged Django

Adminstrivia

Updated after publishing.