Django Tutorial: How to create a project

This post describes how to create a Django project on Ubuntu as of February 2024.

Create Django project

Install dependencies:

sudo apt install python3.11;
sudo apt install python3-venv;
sudo apt install python3-pip
sudo apt install libpq-dev

Create and activate a Python 3 virtual environment:

python3.11 -m venv env
source env/bin/activate

Next, install Python dependencies.

pip install Django==4.2.10
pip install Pillow==10.2.0

Create a project using Django’s default template

django-admin startproject project_slug
cd project_slug

Establish the Django project as a GitHub repository

Create a README.md markdown file:

nano README.md
# Project title
Description

## Requirements & Dependencies
- Requirement 1
- Requirement 2
- Requirement 3

## How to use
...

## Licenses & Credits

Add official python .gitignore

curl -o .gitignore https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore

It is best practice to save dependencies in a requirements.txt file. This makes it possible to clone the project easily from git.

Store dependencies

pip freeze > requirements.txt

Verify that the pkg-resources package is not in requirements.txt (this is an Ubuntu bug that causes problems later)

Create a new GitHub repository. Don’t add a .gitignore, README, or license per GitHub’s documentation. Make it private to avoid revealing server information.

Create a GitHub repository for our Django project

Install dependencies

sudo apt install git

Set up an SSH key for your GitHub for password-free development.

Check whether files already exist

ls -al ~/.ssh

If not, create them:

ssh-keygen -t ed25519 -C "your.email@example.com"
eval "$(ssh-agent -s)"
open ~/.ssh/config
touch ~/.ssh/config

Put this in the ~/.ssh/config file:

Host *
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519

Copy the full contents of the file to the SSH settings in your GitHub account:

ssh-add -K ~/.ssh/id_ed25519
pbcopy < ~/.ssh/id_ed25519.pub

Add your username and password to git:

git config --global user.name "John Doe"
git config --global user.email "johndoe@email.com"

Push to master

git init;
git add .;
git commit -m "Initial commit";
git remote add origin git@github.com:nathankjer/project-slug.git;
git push origin main

Setup

Move configurations to config directory.

mkdir config/
mkdir config/settings/
mv project_slug/settings.py config/settings/base.py
touch config/__init__.py
touch config/settings/__init__.py

Move wsgi and urls into config.

mv project_slug/urls.py config/urls.py
mv project_slug/wsgi.py config/wsgi.py
mv project_slug/asgi.py config/asgi.py

Delete the project_slug directory:

rm -r project_slug

Configure settings

Open base.py:

nano config/settings/base.py

Adjust the directory to reflect new location depth of settings. Take secret key offline.

import environ
...

BASE_DIR = Path(__file__).resolve().parent.parent.parent

# Remove. This is not a base setting.
# SECRET_KEY = "django-insecure-xxxxx"

Turn off debugging. We can turn it on again in certain environments, but this should be the default.

# This should always be false since it is defined to be true in the development environment
DEBUG = False

# Other security settings
SESSION_COOKIE_HTTPONLY = True
CSRF_COOKIE_HTTPONLY = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = "DENY"

ALLOWED_HOSTS = []
DJANGO_APPS = [
    ...
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    ...
]

THIRD_PARTY_APPS = [
]

LOCAL_APPS = [
]

INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS

Add context processors.

ROOT_URLCONF = "config.urls"

TEMPLATES = [
    {
        ...
        'DIRS': [str(BASE_DIR / "project_slug/templates")],
        ...
        OPTIONS': {
            'context_processors': [
                ...
                'django.template.context_processors.i18n',
                'django.template.context_processors.media',
                'django.template.context_processors.static',
                'django.template.context_processors.tz',
            ],
        },
    },
]

WSGI_APPLICATION = 'config.wsgi.application'
# Remove me
# DATABASES = {
#     'default': {
#         'ENGINE': 'django.db.backends.sqlite3',
#         'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
#     }
# }

Now close the file by holding Ctrl + X, then hitting Y and Enter to save.

Let’s create settings for our local development environment.

nano config/settings/development.py
import os

from config.settings.base import *

SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY")

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'project_slug',
        'USER': 'postgres',
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': '127.0.0.1',
        'PORT': '5432',
        'ATOMIC_REQUESTS': True,
        'CONN_MAX_AGE': 60
    }
}
#
# OR 
# 
# DATABASES = {
#     "default": {
#         "ENGINE": "django.db.backends.sqlite3",
#         "NAME": BASE_DIR / "db.sqlite3",
#     }
# }

DEBUG = True
ALLOWED_HOSTS = ['localhost','0.0.0.0','127.0.0.1']
CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
        "LOCATION": "",
    }
}

EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_TIMEOUT = 5
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

STATIC_URL = "/static/"
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, "project_slug/static/"),
]

STATICFILES_FINDERS = [
    "django.contrib.staticfiles.finders.FileSystemFinder",
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
]

MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(BASE_DIR, "project_slug/media/")

Now close the file.

Create Database

First, install the psycopg2-binary, and set up a local postgresql database.

pip install psycopg2-binary
sudo apt install postgresql
sudo -u postgres psql
\password postgres
\q
sudo service postgresql restart

Next, install pgadmin. Follow the instructions here. Then launch pgAdmin 4 and create a server.

Create a database for the project:

Add environment variables

Open the bash profile:

nano ~/.bashrc
# ~/.bash_profile in macOS

Add the following lines:

export DJANGO_SECRET_KEY="..."
export DJANGO_SETTINGS_MODULE="config.settings.development"
export DB_PASSWORD="..." # Password for postgres database, if used
export EMAIL_HOST_USER="username@gmail.com"
export EMAIL_HOST_PASSWORD="..." # Generated at https://security.google.com/settings/security/apppasswords

You may need to run the following in order to see the variables:

source ~/.bashrc
# source ~/.bash_profile in macOS

Configure global settings

Open manage.py:

nano manage.py

It’s convenient to have a base set of settings, plus different configurations on top of it for different environments.

    # Change settings path
    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.development')
        ) from exc

    # Allow apps in project_slug folder
    current_path = os.path.dirname(os.path.abspath(__file__))
    sys.path.append(os.path.join(current_path, 'project_slug'))

    execute_from_command_line(sys.argv)
nano config/wsgi.py
import os
import sys

from django.core.wsgi import get_wsgi_application

app_path = os.path.abspath(
    os.path.join(os.path.dirname(os.path.abspath(__file__)),os.pardir) 
)

sys.path.append(os.path.join(app_path, "project_slug"))

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.development')

application = get_wsgi_application()
nano config/urls.py
...
from django.conf.urls.static import static
from django.conf import settings

urlpatterns = [
    path('admin/', admin.site.urls),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Make migrations to initialize the database:

python manage.py makemigrations;
python manage.py migrate;
python manage.py createsuperuser

Now let’s see if it’s working. We can leave a browser tab open while we apply changes in real-time.

python manage.py runserver
Splash page of the default Django project

Push changes

git add .;
git commit -m "Project structure";
git push origin master

Add bootstrap support

Make forms Bootstrap 5 compatible with Crispy forms:

pip install --upgrade django-crispy-forms
nano config/settings/base.py
THIRD_PARTY_APPS = [
    'crispy_forms',
]
CRISPY_TEMPLATE_PACK = 'bootstrap5'
nano project_slug/templates/create.html
{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Create {{ form|form_verbose_name }}{% endblock %}

{% block content %}
    <div class="container mt-5">
        <h1 class="mb-3">Create {{ form|form_verbose_name }}</h1>
        <form method="post" enctype="multipart/form-data">{% csrf_token %}
            {{ form|crispy }}
            <button type="submit" class="save btn btn-primary">Save</button>
        </form>
    </div>
{% endblock %}
nano project_slug/templates/update.html
{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Edit "{{ object }}"{% endblock %}

{% block content %}
<div class="container mt-5">
    <h1 class="mb-3">Edit "{{ object }}"</h1>
    <form method="post" enctype="multipart/form-data">{% csrf_token %}
        {{ form|crispy }}
        <button type="submit" class="save btn btn-primary">Save</button><a href="../delete/" class="btn btn-outline-danger mx-1">Delete</a>
    </form>
</div>
{% endblock %}
nano project_slug/templates/delete.html
{% extends 'base.html' %}

{% block title %}Delete "{{ object }}"{% endblock %}

{% block content %}
    <div class="container mt-5">
        <h1 class="mb-3">Delete "{{ object }}"</h1>
        <form method="post">{% csrf_token %}
            <p>Are you sure you want to delete "{{ object }}"?</p>
            <button type="submit" class="save btn btn-danger">Confirm</button>
        </form>
    </div>
{% endblock %}

Error pages & account forms

Copy error page forms from the cookiecutter-django repository.

curl -o project_slug/templates/403.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/403.html;
curl -o project_slug/templates/404.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/404.html;
curl -o project_slug/templates/500.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/500.html

Prepare static

mkdir project_slug/media
mkdir project_slug/media/images
mkdir project_slug/static
mkdir project_slug/static/css
mkdir project_slug/static/js
touch project_slug/static/css/style.css
touch project_slug/static/js/custom.js

Add the default profile picture:

curl -o project_slug/media/images/user.jpg https://i.stack.imgur.com/34AD2.jpg
nano project_slug/static/css/style.css
html {
    position:relative;
    min-height:100%;
}

body {
    margin-bottom:60px;
}

body, .badge {
    font-size:18px;
    font-family: 'Roboto', sans-serif;
    font-weight: 300;
}

a {
    text-decoration: none!important;
}

.card-img-top {
    width: 100%;
    height: 200px;
    object-fit: cover;
}

.btn {
    min-width:78px;
}

.row {
    margin:0;
}

.footer {
    position:absolute;
    bottom:0;
    width:100%;
    height:60px;
    line-height:60px;
    overflow:hidden;
    border-top:1px solid rgba(0,0,0,0.1);
}

.post-img-wrapper {
    max-height:500px;
    width:100%;
    overflow:hidden;
    background-color:#000;
}

.post-img {
    width: 100%;
}

Download bootstrap dependencies

curl -o project_slug/static/css/bootstrap.min.css https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css;
curl -o project_slug/static/css/bootstrap-icons.min.js https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css;
curl -o project_slug/static/js/popper.min.js https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js;
curl -o project_slug/static/js/bootstrap.min.js https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js

Install third party libraries

curl -o project_slug/static/js/font-awesome.min.js https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/js/all.min.js
nano config/urls.py
...
from django.views.generic import TemplateView
from django.views import defaults as default_views

urlpatterns = [
    path('', include('core.urls')),
    ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

if settings.DEBUG:
    urlpatterns += [
        path("400/",default_views.bad_request,kwargs={"exception": Exception("Bad Request")},),
        path("403/",default_views.permission_denied,kwargs={"exception": Exception("Permission Denied")},),
        path("404/",default_views.page_not_found,kwargs={"exception": Exception("Page not Found")},),
        path("500/", default_views.server_error),
    ]

User authentication using django-allauth

pip install django-allauth
nano config/settings/base.py
DJANGO_APPS = [
    ...
    'django.contrib.auth', # Make sure these are here
    ...
    'django.contrib.sites',
    'django.contrib.messages',
]
THIRD_PARTY_APPS = [
    ...
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
)

MIDDLEWARE = [
...
   'django.contrib.sites.middleware.CurrentSiteMiddleware',
    'allauth.account.middleware.AccountMiddleware',
]
...

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
)

SITE_ID = 1

ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS =1
ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = True
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS =1
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = 5
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT = 86400 # 1 day in seconds
ACCOUNT_LOGOUT_REDIRECT_URL ='/accounts/login/'
LOGIN_REDIRECT_URL = '/'


Now let’s double check that the site has the correct ID (which should be 1).

python manage.py shell
from django.contrib.sites.models import Site
>>> Site.objects.all().delete()
>>> Site.objects.create(id=1,name='Site name',domain='127.0.0.1:8000')
>>> quit()

Copy account forms from the cookiecutter-django repository.

mkdir project_slug/templates/account/
curl -o project_slug/templates/account/account_inactive.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/account_inactive.html;
curl -o project_slug/templates/account/base.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/base.html;
curl -o project_slug/templates/account/email.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/email.html;
curl -o project_slug/templates/account/email_confirm.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/email_confirm.html;
curl -o project_slug/templates/account/login.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/login.html;
curl -o project_slug/templates/account/logout.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/logout.html;
curl -o project_slug/templates/account/password_change.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/password_change.html;
curl -o project_slug/templates/account/password_reset.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/password_reset.html;
curl -o project_slug/templates/account/password_reset_done.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/password_reset_done.html;
curl -o project_slug/templates/account/password_reset_from_key.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/password_reset_from_key.html;
curl -o project_slug/templates/account/password_reset_from_key_done.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/password_reset_from_key_done.html;
curl -o project_slug/templates/account/password_set.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/password_set.html;
curl -o project_slug/templates/account/signup.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/signup.html;
curl -o project_slug/templates/account/signup_closed.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/signup_closed.html;
curl -o project_slug/templates/account/verification_sent.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/verification_sent.html;
curl -o project_slug/templates/account/verified_email_required.html https://raw.githubusercontent.com/pydanny/cookiecutter-django/master/%7B%7Bcookiecutter.project_slug%7D%7D/%7B%7Bcookiecutter.project_slug%7D%7D/templates/account/verified_email_required.html
nano config/urls.py
...
from django.urls import path, include
...

urlpatterns = [
    ...
    path('accounts/', include('allauth.urls')),
    ...
]

Custom user

python manage.py startapp users
mv users/ project_slug/
nano config/settings/base.py
LOCAL_APPS = [
    'users',
]
...
AUTH_USER_MODEL = 'users.User'
nano project_slug/users/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):

    # Auto Fields
    id = models.AutoField(primary_key=True)

    # Required
    name = models.CharField(max_length=255)
    email = models.EmailField(unique=True)

    # Optional
    avatar = models.ImageField(upload_to='images',default='images/user.jpg',null=True,blank=True)
    bio = models.TextField(null=True, blank=True)

nano project_slug/users/admin.py
from django.contrib import admin

from .models import User

@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    pass
nano config/urls.py
from django.urls import path, include
...

urlpatterns = [
    ...
    path('users/', include('users.urls')),
    ...
]
nano project_slug/users/urls.py
from django.urls import path

from . import views

urlpatterns = [
    # Users
    path('<slug:username>/update/', views.UserUpdateView.as_view(), name='user_update'),
    path('<slug:username>/', views.UserDetailView.as_view(), name='user_detail'),
]
nano project_slug/users/views.py
from django.views.generic import DetailView
from django.views.generic.edit import UpdateView
from django.shortcuts import get_object_or_404
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse

from .models import User
from .forms import UserForm

class UserDetailView(DetailView):
    model = User
    template_name = 'users/user_detail.html'
    def get_object(self):
        return get_object_or_404(User, username=self.kwargs['username'])

class UserUpdateView(LoginRequiredMixin, UpdateView):
    model = User
    form_class = UserForm
    login_url = '/accounts/login/'
    template_name = 'update.html'
    def get_object(self):
        return get_object_or_404(User, username=self.kwargs['username'])
    def get_success_url(self):
        return reverse('user_detail', kwargs={'username': self.object.username})
    def dispatch(self, request, *args, **kwargs):
        obj = self.get_object()
        if obj != self.request.user and not self.request.user.is_staff:
            raise Http404("You are not allowed to edit this")
        return super(UserUpdateView, self).dispatch(request, *args, **kwargs)

nano project_slug/users/forms.py
from django import forms

from .models import *

class UserForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(UserForm, self).__init__(*args, **kwargs)

    class Meta:
        model = User
        fields = ['name','bio','avatar']

mkdir project_slug/templates/users;
nano project_slug/templates/users/user_detail.html
{% extends 'base.html' %}

{% block title %}{{ object }}{% endblock %}

{% block navbar %}
    {% if user.is_staff or object.created_by == user or object == user %}
        <li class="nav-item"><a href="update/" class="btn btn-light mx-1"><i class="fas fa-edit"></i> Edit</a></li>
    {% endif %}
{% endblock %}

{% block content %}
<div class="container mt-5">
    <div class="mb-3 text-center">
    {% if object.avatar %}
        <img src="{{ object.avatar.url }}" class="img-thumbnail rounded-circle" style="height:100px;width:100px;">
    {% endif %}
    <p>
        {% if object.name %}
            <b>{{ object.name }}</b>
            <span class="text-muted">|</span>
        {% endif %}
        <a href="{% url 'user_detail' object.username %}">@{{ object.username }}</a>
    </p>
    {% if object.bio %}
        <p class="lead">{{ object.bio }}</p>
    {% endif %}
    </div>
    <p><a href="{% url 'home' %}">← Home</a></p>
</div>
{% endblock %}

Go to http://127.0.0.1:8000/admin and log in.

Admin login page of our Django project

Version control with django-reversion

pip install django-reversion
nano config/settings/base.py
THIRD_PARTY_APPS = [
    'reversion',
]
MIDDLEWARE = [
    ...
    'reversion.middleware.RevisionMiddleware',
]

Store dependencies to reflect updated third-party apps.

pip freeze > requirements.txt

Commit changes.

git add .;
git commit -m "Third-party apps";
git push origin master

Stop the server with Control+C and deactivate:

Deactivate the virtual environment by typing in the following:

deactivate

About the author



Hi, I'm Nathan. Thanks for reading! Keep an eye out for more content being posted soon.


Leave a Reply

Your email address will not be published. Required fields are marked *