Django Tutorial: How to create a project

This post describes how to create a Django project on Ubuntu as of December 2019.

Create Django project

Install dependencies:

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

Create and activate a Python 3 virtual environment:

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

Next, install Python dependencies. In this case, when I installed more recent versions of django than 2.1.*, it caused problems later on.

pip install django==3.2.15
pip install Pillow==9.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-less development.

ssh-keygen -t rsa -b 4096;
cat ~/.ssh/id_rsa.pub

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

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 master

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(strict=True).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
from config.settings.base import *

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)

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

Create core app

I have found it to be convenient to create an app called “core”, and assign the Home view to it.

python manage.py startapp core
mv core/ project_slug/

Clear contents of admin.py and models.py files.

Set views.py to the following:

from django.views.generic import TemplateView
from django.contrib.auth.mixins import LoginRequiredMixin


class HomeView(LoginRequiredMixin, TemplateView):
    template_name = "core/home.html"

Set urls.py to the following:

from django.urls import path

from core.views import HomeView

urlpatterns = [
    path("", HomeView.as_view(), name="home"),
]

Add core app to config/settings/base.py:

LOCAL_APPS = [
    'core',
    ...
]

Create views

mkdir project_slug/templates;
nano project_slug/templates/base.html
{% load static %}

<!DOCTYPE html>
<html>
    <head>
        <!--[if IE]>
        <link rel="shortcut icon" href="{% static "images/favicon.png" %}">
        <![endif]-->
        {% block meta %}{% endblock %}
        <link rel="icon" href="{% static "images/favicon.ico" %}">
        <link rel="stylesheet" href="{% static "css/bootstrap.min.css" %}">
        <link rel="stylesheet" href="{% static "css/animate.min.css" %}">
        <link rel="stylesheet" href="{% static "css/hover.min.css" %}">
        <link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
        <link rel="stylesheet" href="{% static "css/style.css" %}">
        <title>{% block title %}{% endblock %}</title>
        <style>{% block style %}{% endblock %}</style>
    </head>
    <body>
        <nav class="navbar navbar-expand-lg navbar-light bg-light">
            <a class="navbar-brand" href="/">
                {{ request.site.name }}
            </a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbar">
                <ul class="nav navbar-nav ml-auto">
                  {% block navbar %}
                  {% endblock %}
                  {% if user.is_authenticated %}
                      <li class="nav-item mx-1">
                          <div class="btn-group" role="group">
                              <button class="btn btn-light dropdown-toggle" data-toggle="dropdown" id="account_dropdown" type="button">
                                  <i class="fas fa-user"></i> {{ user.username }}
                              </button>
                  						<div aria-labelledby="account_dropdown" class="dropdown-menu dropdown-menu-right">
                                  <a class="dropdown-item" href="{% url 'user_detail' user.username %}">My Account</a>
                                  <a class="dropdown-item" href="{% url 'account_change_password' %}">Change Password</a>
                                  <a class="dropdown-item" href="{% url 'account_email' %}">Change E-Mail</a>
                                  <a class="dropdown-item" href="{% url 'account_logout' %}">Log Out</a>
                                  {% if user.is_staff %}
                                      <div class="dropdown-divider"></div>
                                      <a class="dropdown-item" href="{% url 'admin:index' %}">Admin</a>
                                  {% endif %}
                  						</div>
                					</div>
                      </li>
                  {% else %}
                      <li class="nav-item mx-1">
                          <a class="nav-link" href="{% url 'account_login' %}">Log in</a>
                      </li>
                  {% endif %}
                </ul>
            </div>
        </nav>
        <main class="pb-4 pt-5" id="main">
            <div class="row">
                {% block feature_header %}{% endblock %}
            </div>
            <div class="row">
                <div class="container mt-5">
                    {% block content %}{% endblock %}
                </div>
            </div>
        </main>
        <footer class="footer">
            <div class="float-left mx-5">
                <ul class="list-inline">
                  {% for page in footer_list %}
                      <li class="list-inline-item"><a class="text-muted" href=""></a></li>
                  {% endfor %}
                </ul>
            </div>
            <small class="float-right mx-5 text-muted">
                Copyright © {% now 'Y' %} <a href="/" class="text-muted">{{ request.site.name }}</a>
            </small>
        </footer>
        <script src="{% static "js/jquery.min.js" %}"></script>
        <script src="{% static "js/popper.min.js" %}"></script>
        <script src="{% static "js/bootstrap.min.js" %}"></script>
        <script src="{% static "js/font-awesome.min.js" %}"></script>
        <script src="{% static "js/custom.js" %}"></script>
        <script>{% block script %}{% endblock %}</script>
    </body>
</html>
nano project_slug/templates/core/home.html
{% extends 'base.html' %}

{% block meta %}
{% endblock %}

{% block title %}Home{% endblock %}

{% block entry_title %}Home{% endblock %}

{% block content %}

{% endblock %}

Make forms Bootstrap 4 compatible with Crispy forms:

pip install --upgrade django-crispy-forms
nano config/settings/base.py
THIRD_PARTY_APPS = [
    'crispy_forms',
]
CRISPY_TEMPLATE_PACK = 'bootstrap4'
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://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css
curl -o project_slug/static/js/jquery.min.js https://code.jquery.com/jquery-3.3.1.slim.min.js
curl -o project_slug/static/js/popper.min.js https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js
curl -o project_slug/static/js/bootstrap.min.js https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/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',
]
...

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 %}
python manage.py makemigrations;
python manage.py migrate;
python manage.py createsuperuser

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 *