Skip to content

azegas/CDP

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

No longer using it, using https://github.com/azegas/Django_starter instead.

CDP - Core Django Project

With each new project that I build I keep finding better ways to start a new project. Here I will keep a CORE things that each of my future Django app will have to have.

How this file is created

README.org file is created. It is the main documentation file. After modifications to it are made, this file is then converted to README.md file (the one you are reading now) with the help of ox-gfm Emacs package - https://github.com/larstvei/ox-gfm and org-gfm-export-to-markdown command. Previously I would use org-md-export-to-markdown command, it does a great job, but syntax highlighting is not present in this method. That is the main reason I am using ox-gfm.

I use .org file since I am used to Emacs keybindings and it's much quicker for me to do the formatting and text transformations and etc. It also generates a table of content for me, which is nice in such large document.

Resources used

  • Pro Django tutorials by thenewboston

Steps taken to create this Django Core Project

Steps taken to create this repo are described here.

Create a Github repository

Create a Github repo with the name of your project. Clone it to your machine. Open a text editor inside of it.

Create a Django project

Make sure Python is installed globally on your system. You can do this by writing python3 --version. Then install django globally on your machine by running this command in your terminal - pip install django. To check if django has successfully installed, do pip list command and see if django package is there. Okay, now you have python and django installed.

Time to create a django project. Run the following command - django-admin startproject project. Now you should see a folder called "project" in your current directory. Inside of that folder there is another folder called "project". Delete one "project" folder, make sure only one "project" folder, manage.py and README.md remains.

Push to github.

Create .gitignore file

Add content to it from your most recent Django project.

Create a python virtual environment

https://docs.python.org/3/library/venv.html

Navigate to the same folder where README.md and manage.py files are. Open a terminal and type:

python3 -m venv venv

# activate that virtual environment
source venv/bin/activate

# list packages inside the virtual environment
pip list

Package    Version
---------- -------
pip        22.0.2
setuptools 59.6.0

# deactivate virtual environment
deactivate

# check if you have excited the virtual environment by:
pip list

# you now should see way more packages than there were in the virtual
# environment(these are your python packages installed globally on
# your machine).

Poetry setup

https://python-poetry.org/ - poetry is a modern tool for package management in Python that simplifies the process of creating, managing, and publishing Python packages. Helps to make sure each developer's environment is exactly the same, without a smallest changes in installed packages.

Install poetry

curl -sSL https://install.python-poetry.org | python3 -

export PATH="/home/arvy/.local/bin:$PATH"

# verify that poetry is installed
poetry --version
poetry init
# go over the guide (no/no/yes)

We now should have pyproject.toml file(https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/) in our git directory. This file will contain all the dependencies of our python project.

To install a package you now should use:

poetry add django

Now django dependency got added to pyproject.toml file, also a new file - poetry.lock was created automatically. When we ran poetry add command, another command poetry install ran as well.

In the future if we want to install new packages we will use poetry add command, it will install AND add write the packages in both files. But for developers that have just pulled this repository from github, they will not have to use poetry add to install the packages, they will be able to use poetry install, to simply install all the listed(in those files) dependencies.

NOTE: by default poetry will try to create it's own virtual environment, but since we have created our's already, nothing additional will be created.

Try running the server with poetry command

Inside your folder where manage.py exists, run this command:

python manage.py runserver

You should be able to access the server on http://127.0.0.1:8000/. Ignore the warning about migrations for now.

Run everything through Poetry from now on

We want everything to run through poetry from now on, so poetry can keep everything consistent between different environments. We don't want any surprises :)

Anytime we want to run something through poetry, we run it through poetry run command, for example.

poetry run python manage.py runserver

Creating a Makefile

Instead of writing all the needed commands in here or in a google doc or something, we can create a Makefile and describe all the commands in it, so you yourself in other projects or other developers can use the same commands as you do. This will become my new standard I hope.

Make is used when compiling software, it's a linux tool that comes with every linux installation.

touch Makefile

If we now add such line to this makefile:

run-server:
        poetry run python manage.py runserver

The server runs.

It runs through poetry and through make command. Poetry - so there are no surprises with dependencies, make - so we don't have to type long commands each time.

We can also add more make commands into the Makefile, but this time we will also add .PHONY above each command(https://ftp.gnu.org/old-gnu/Manuals/make-3.79.1/html_node/make_34.html#SEC33).

.PHONY: run-server
run-server:
        poetry run python manage.py runserver

.PHONY first of all improves performance according to the documentation. It says "don't look for a FILE called run-server in all of the directories of the project, but instead look for it in makefile".

Other times our commands might be like "make install" or "make clean" or something similar and files might already exist with those names in our directories, so make will try to run those first if there is no .PHONY described.

Restructuring the codebase

We will be leaving the top level directory "" for various config files and etc, don't want to have Django files (like manage.py) and project folder and all business logic in the same location as config files.

That is why we will create a folder called "core".

Currently my directories looks like this:

$ tree CDP -L 1
CDP
├── Makefile
├── README.md
├── README.org
├── db.sqlite3
├── manage.py
├── poetry.lock
├── project
├── pyproject.toml
└── venv

2 directories, 7 files

Create a core folder and move Django files to it.

pwd
# make sure you are in CDP directory
mkdir core
touch core/__init__.py
mv project/ core/
mv manage.py core/

Project directory structure now looks like this:

$ tree CDP -L 2
CDP
├── Makefile
├── README.md
├── README.org
├── core
│   ├── __init__.py
│   ├── manage.py
│   └── project
├── db.sqlite3
├── poetry.lock
├── pyproject.toml
└── venv

If we try to run project now - we will get an error saying that project settings and django files can not be found. Let's fix that.

in manage.py do this change:

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
# change to
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.project.settings")

in settings.py do this change:

BASE_DIR = Path(__file__).resolve().parent.parent
# change to (since we moved one folder deeper)
BASE_DIR = Path(__file__).resolve().parent.parent.parent

ROOT_URLCONF = "project.urls"
# change to
ROOT_URLCONF = "core.project.urls"

WSGI_APPLICATION = "project.wsgi.application"
# change to
WSGI_APPLICATION = "core.project.wsgi.application"

If we try to run a server again, we will still get an error:

poetry run python manage.py runserver
/home/arvy/src/CDP/venv/bin/python: can't open file '/home/arvy/src/CDP/manage.py': [Errno 2] No such file or directory
make: *** [Makefile:7: run-server] Error 2

We need to modify our Makefile so it runs the MODULE (since we added __init__.py file in core) which is core.manage.

# so instead of this:

.PHONY: run-server
run-server:
     poetry run python manage.py runserver

# is now this:

.PHONY: run-server
run-server:
     poetry run python -m core.manage runserver

Now command make run-server works just fine.

Settings management

As our projects scales, we need to make sure our projects is organized. Settings.py file can get pretty beefy when adding testing, docker and other settings to it.

Let's install django-split-settings(https://pypi.org/project/django-split-settings/) package that will help us with that:

poetry add django-split-settings

Move the settings.py file to a separate settings folder:

pwd
/home/arvy/src/CDP/core/project

mkdir settings
touch settings/__init__.py

mv settings.py base.py
mv base.py settings/

in base.py modify one line, since we put the settings file one level deeper:

BASE_DIR = Path(__file__).resolve().parent.parent.parent
# change to:
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent

in init.py file - (/home/arvy/src/CDP/core/project/settings/__init__.py)

from split_settings.tools import include

include(
    'base.py',                  # main settings that are needed for each project
)

Try to run the server now, it should work.

make run-server

Settings management for developers

Will create a separate location where developers can describe their settings and they will be plugged in to the project.

Create:

mkdir local
touch local/settings.dev.py

In base.py change these values:

SECRET_KEY = "django-insecure-wn(!#y#4s*07ux!9qkp$!)=oqgmgieak3xg@u"
# change to
SECRET_KEY = NotImplemented

DEBUG = True
# change to
DEBUG = False

# remove these two lines
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent

The whole content of settings.dev.py should look like this:

import os.path
from pathlib import Path
from split_settings.tools import include, optional

# our base directory, in our case its CDP folder
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent
# /home/arvy/src/CDP

# whenever we want a system variable to be pulled in here
# (whether from docker or system you are currently developing on),
# we will have to prefix it with this string below:
ENVVAR_SETTINGS_PREFIX = 'CORESETTINGS_'

LOCAL_SETTINGS_PATH = os.getenv(f"{ENVVAR_SETTINGS_PREFIX}LOCAL_SETTINGS_PATH")

if not LOCAL_SETTINGS_PATH:
    LOCAL_SETTINGS_PATH = 'local/settings.dev.py'

# if relative path is found, converting it to absolute path
if not os.path.isabs(LOCAL_SETTINGS_PATH):
    LOCAL_SETTINGS_PATH = str(BASE_DIR / LOCAL_SETTINGS_PATH)
    # /home/arvy/src/CDP/local/settings.dev.py

include(
    'base.py',                  # base settings that we will use for every environment
    optional(LOCAL_SETTINGS_PATH) # Include if exist. They will override the  base.py
)

Then also since we took BASE_DIR description from base.py file, we need to modify the database location.

Chante the "NAME" to the absolute location of db.sqlite3 file like so:

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": "/home/arvy/src/CDP/db.sqlite3",
    }
}

Try to make run-server now - the server should work properly.

If you remove settings.dev.py - it won't work, you will have ALLOWED_HOSTS error because DEBUG = False in base.py. It just confirms that the new dev settings file works as intended.

After you confirm that it works, make sure to add /local folder to .gitignore.

Also we can make a template file for other developers to look into, to know what values they can change:

in /home/arvy/src/CDP/core/project/settings

mkdir templates
touch settings.dev.py

add reference content to it:

"""

Sample settings template file that other developers can use to
override base.py settings file with their own settings. Should be
placed in "local/settings.dev.py file.

/home/arvy/src/CDP/local/settings.dev.py

"""

SECRET_KEY = "secretkey"
DEBUG = True

So now when new developer comes, he knows that he must create local folder and copy this settings.dev.py file to that local folder.

Settings management for our application

In here we will place unique settings that are related to our application only. Not Django settings should go in here.

Create:

touch core/project/settings/custom.py

In custom.py add this content:

-

add 'custom.py' to init.py

Configure settings of code editor in one place

Whenever there is more than one person working on the project, it's good to have a standard according to which the code is going to be written. https://editorconfig.org/ comes to help with this.

Next to .gitignore or README.md files, in the same directory, create a file called .editorconfig. Add content to it:

root = true

[*.{html,py}]
charset = utf-8
indent_size = 4
indent_style = space
max_line_length = 119
trim_trailing_whitespace = true

This configuration tells the text editor and IDE's how to automatically clean up our code.

VScode needs an extension installed to be able to read these instructions. Pycharm does it automatically. Emacs needs an extension as well.

Flake8

Keep your code consistent and clean. https://flake8.pycqa.org/en/latest/user/configuration.html

Install flake9 as a DEV (-D) dependency, since we don't need it in our final build to push up to production, its more for developers, to make sure their code is consistent.

poetry add -D flake8

Check pyproject.toml and poetry.lock files to confirm that it was installed and added as dev dependency.

Also check in this way:

poetry add -D flake8 --help

Let's test flake9. Go to core/manage.py and add 5 or so blank lines, save the file.

Then in terminal run this command:

poetry run flake8 core/manage.py -v

This will scan that file with flake8 and tell us what's wrong with it. Should say E303 too many blank lines(5). That's great!

Remove those 5 blank lines and run the same command again. Error should not appear anymore.

If you ever want to run this check for every single file in your directory/subdirectory, then you can run the same command without specifying the file path:

poetry run flake8

But since we have venv folder with lots of crap in it, we will not do this. Let's create a config file for flake8 so we can specify what to ignore along with other settings.

If there is a particular error you are getting but would like for flake8 to ignore, you can add # noqa: F821 for example. Replace the error code with the error you want to ignore on the line you want to ignore it onto.

If you want we can put the poetry run flake8 command to Makefile.

But we will not do that just yet, since we will use pre-commit tool and run flake8 over it!

pre-commit

pre-commit allows us to manage hooks. Hook is basically just a little script that can check your code for various issues(style, linting…). You can create those hooks yourself or you can use the hooks that other people have created. Fox example - a hook that checks if your imports are properly sorted, if you don't have any extra whitespace.

Install it as a DEV(-d) dependency.

poetry add -D pre-commit

check poetry.lock and pyproject.toml if pre-commit was added.

Let's generate a sample config for us to use:

poetry run pre-commit sample-config

Then create a .pre-commit-config.yaml file next to all other config files and paste the sample code in it.

Let's install those hooks that we described by doing:

poetry run pre-commit install

Now if we would make a commit, those hooks would run, but sometimes we might want to run those hooks manually. We can do that by running this command:

poetry run pre-commit run --all-files

(venv) arvy@DESKTOP-AUDMJ7D:~/src/CDP$ poetry run pre-commit run --all-files
[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook

Fixing .editorconfig
Fixing .flake8
Fixing .gitignore

Check Yaml...........................................(no files to check)Skipped
Check for added large files..............................................Passed


(venv) arvy@DESKTOP-AUDMJ7D:~/src/CDP$ poetry run pre-commit run --all-files
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...........................................(no files to check)Skipped
Check for added large files..............................................Passed

The result might be similar for you as well. First there was an environment initialization, took a bit, then it found some errors and fixed them!

When I ran the same command the second time - you can see that there was no initialization anymore and it was not screaming about the errors - since they were fixed by the first run! Great!

An example of a proper config:

# File introduces automated checks triggered on git events
# to enable run `pip install pre-commit && pre-commit install`

repos:

  # general checks (see here: https://pre-commit.com/hooks.html)
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      -   id: trailing-whitespace
      -   id: check-docstring-first
      -   id: check-added-large-files
      -   id: debug-statements
      -   id: check-yaml
      -   id: check-merge-conflict
      -   id: end-of-file-fixer
      -   id: detect-private-key

  # yapf - the most OCD developer, following the most strict style guide
  - repo: https://github.com/google/yapf
    rev: v0.40.2
    hooks:
      - id: yapf

  # isort - sorting python imports
  - repo: https://github.com/pycqa/isort
    rev: 5.12.0
    hooks:
      - id: isort
        name: isort (python)

  # flake8 - linting
  - repo: https://github.com/PyCQA/flake8
    rev: 4.0.1
    hooks:
      - id: flake8
        additional_dependencies:
          - flake8-bugbear
          - flake8-builtins
          - flake8-coding
          - flake8-import-order
          - flake8-polyfill
          - flake8-quotes

  # helps you catch type-related errors in your code early, during
  # development, rather than at runtime
  - repo: https://github.com/pre-commit/mirrors-mypy
    rev: 'v0.910'
    hooks:
      - id: mypy
        additional_dependencies: [ types-requests, types-PyYAML, types-toml ]

pre-commit-config.yaml says what hooks do we want to install and in pyproject.toml we configure them there.

For example, we can add such configuration for isort:

[tool.isort]
multi_line_output = 5
line_length = 119

After you are done making changes, uninstall the hooks and install them all again:

poetry run pre-commit uninstall
poetry run pre-commit install
poetry run pre-commit run --all-files

There might be a lot of warnings about single/double quotes. That is because we have specified "inline-quotes = single" in .flake8 and flake8 has been ran with pre-commit task. When we created django files - we had double quotes everywhere. I went over and changed those from double to single quotes. ([2023-10-30 Mon] changed back to double quotes since double quotes seem to be default everywhere.)

Since we are running flake8 over pre-commit now, we can remove flake8 package from dev dependencies:

poetry remove -D flake8
poetry run pre-commit run --all-files

You can see that flake8 still runs and we still get it's logic even after we have uninstalled flake8 package. Great. Less dependencies, more clean.

Now we also should update our Makefile by adding these:

.PHONY: install-pre-commit
install-pre-commit:
        poetry run pre-commit uninstall; poetry run pre-commit install

.PHONY: lint
lint:
        poetry run pre-commit run --all-files

update "update" by adding install-pre-commit:

.PHONY: update
update: install migrate install-pre-commit ;

logging

Logging can provide us with more(than print statements) - and better structured - information about the state and health of your application.

Besides

Inspiration and explanation - https://www.youtube.com/watch?v=sGbzjzO1LHI&list=PL6gx4Cwl9DGDYbs0jJdGefNN8eZRSwWqy&index=3&ab_channel=thenewboston

Official docs - https://docs.djangoproject.com/en/4.2/topics/logging/

In settings folder, next to base.py and custom.py, create logging.py file.

Now for every logging task you will need 3 things(formatter, handler, logger):

  • formatter

Describe HOW you want to format your messages

'formatters': {
    'main_formatter': {
        'format': '%(asctime)s %(levelname)s %(module)s %(name)s %(message)s'
    },
},
  • handler

Where to put those logs. StreamHandler - output logs to the console. FileHandler - write logs to the external file.

'handlers': {
    'console': {
        'level': 'INFO',
        'class': 'logging.StreamHandler',
        'formatter': 'main_formatter',
        'filters': [],
    },
},
  • logger
'loggers': {
    logger_name: {
        'level': 'INFO',
        'propagate': True,
    } for logger_name in ('django', 'django.request', 'django.db.backends', 'django.template', 'core')
},

The whole logging.py file can look like so at the end in our case:

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'main_formatter': {
            'format': '%(asctime)s %(levelname)s %(module)s %(name)s %(message)s'
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'main_formatter',
            'filters': [],
        },
    },
    'loggers': {
        logger_name: {
            'level': 'INFO',
            'propagate': True,
        } for logger_name in ('django', 'django.request', 'django.db.backends', 'django.template', 'core')
    },
    'root': {
        'level': 'DEBUG',
        'handlers': ['console'],
    }
}

If we specify INFO logging level - then INFO and everything ABOVE info will be logged. If I chose to have WARNING only, then only WARNING and everything above warning will be logged, I will not see info or debug logs.

In production it's better not to set to see DEBUG messages, since some of them potentially might be dangerous if exposed.

So basically if you want to change the depth level of your logs - modify 'loggers', if you want to change the formatting - change the formatter, if you want to log to different places (terminal, file..), modify 'handlers'.

Also don't forget to add logging.py file to __init__.py so out config file gets read by Django app.

include(
    'base.py',
    'custom.py',
    'logging.py',                 # ADD THIS
    optional(LOCAL_SETTINGS_PATH)
)

Then to make the log messages colorful, not sure about the exact syntax and what it does, but can add this code in your local folder, settings.dev.py file:

LOGGING['formatters']['colored'] = {
    '()': 'colorlog.ColoredFormatter',
    'format': '%(log_color)s%(asctime)s %(levelname)s %(name)s %(bold_white)s%(message)s',
}
LOGGING['loggers']['core']['level'] = 'DEBUG'
LOGGING['handlers']['console']['level'] = 'DEBUG'
LOGGING['handlers']['console']['formatter'] = 'colored'

But before adding those, first you have to install a package:

poetry add -D colorlog

Then to check the logs, in urls.py file for example you can add this:

import logging

logger = logging.getLogger(__name__)

logger.debug('This is the debug message')
logger.info('This is an info message')
logger.warning('This is the warning message')
logger.error('This is an error message')
logger.critical('This is a critical message')

Now with each page refresh you should see some messages exactly how we styled. They should be colorful also.

The goal with logging now is to figure out how to use it effectively in my Django project.

Create a welcome app

Creating an app just to show the process of it, also it's better to have something tangible at the beginning to continue to build some other features. Will have authentication, debug toolbar, base templates, error pages, etc… none of that could be done without a simple welcome starter app.

poetry run python -m core.manage startapp welcome

Then create "apps" folder inside of the "core" folder. Then simply drag and drop the "welcome" app folder to "apps" folder.

Inside of the apps/welcome/apps.py file change "name" to be:

name = "core.apps.welcome"

Then inside our base.py file, add this newly created app to installed apps:

     'django.contrib.sessions',
     'django.contrib.messages',
     'django.contrib.staticfiles',
+    'core.apps.welcome',

Inside of the project's url file add the app's urls:

from django.contrib import admin
from django.urls import path, include  # dont forget to add "include"

urlpatterns = [
    path('admin/', admin.site.urls),
    path("", include("core.apps.welcome.urls")),
]

Then create some views in "core/apps/welcome/views.py":

from django.shortcuts import render

def HomePageView(request):
    return render(request, 'welcome/home.html')

def AboutPageView(request):
    return render(request, 'welcome/about.html')

Create urls.py in the same folder as the views and add this content:

from django.urls import path
from . import views

urlpatterns = [
    path("", views.HomePageView, name="home"),
    path("about", views.AboutPageView, name="about"),
]

Then lets fix the templates. We could specify the templates in each app, but this time I decided to try to have a centralized place for all the templates. For that we need to:

Create templates folder inside the core folder, then inside the templates folder we will be creating a separate folder for each app.

https://docs.djangoproject.com/en/4.2/topics/templates/

So in core/templates/welcome we now create two html files:

home.html:

<h1>Home</h1>

about.html:

<h1>About</h1>

Then let's go back to base.py settings file and specify our templates location:

-        'DIRS': [],
+        'DIRS': ['core/templates/'],

Try to run server, you should be able to see the home page as well as about page.

Static files

Let's make it possible to add custom js, css files. Also let's make it possible to describe navbar and footer in one file (_base.html) and then reuse it on all other templates.

First of all let's modify base.py settings:

Add STATICFILES_DIRS - this will be the location where Django will look for static files(css, js, images).

# https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-STATICFILES_DIRS
STATICFILES_DIRS = [
    "core/static/",
]

Then create 4 folders.

mkdir core/static
mkdir core/static/css
mkdir core/static/js
mkdir core/static/images

and create files within them:

touch core/static/css/base.css
touch core/static/js/base.js

Can also add an image or favicon icon to images folder.

Content of base.css can be any, but in my case it's:

/* Sticky footer styles
-------------------------------------------------- */
html {
  position: relative;
  min-height: 100%;
  font-size: 14px;
}
@media (min-width: 768px) {
  html {
    font-size: 16px;
  }
}

body {
  margin-bottom: 60px; /* Margin bottom by footer height */
 }

.container {
  max-width: 960px;
}

.footer {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 60px; /* Set the fixed height of the footer here */
  line-height: 60px; /* Vertically center the text there */
  background-color: #f5f5f5;
}

Let's also create _base.html file in core/templates folder:

{% load static %}
<!DOCTYPE html>
<html lang="en">

    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
        <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
        <title>{% block title %}CDP{% endblock title %}</title>
        <meta name="description" content="A framework for launching new Django projects quickly.">
        <meta name="author" content="">
        <link rel="shortcut icon" type="image/x-icon" href="{% static 'images/favicon.ico' %}">

        {% block css %}
            <!-- Bootstrap CSS -->
            <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
                  integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">

            <link rel="stylesheet" href="{% static 'css/base.css' %}">
        {% endblock %}
    </head>

    <body>
        <header class="p-3 mb-3 border-bottom">
            <div class="container">
                <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
                    <a href="/" class="d-flex align-items-center mb-2 mb-lg-0 text-dark text-decoration-none">
                        <svg class="bi me-2" width="40" height="32" role="img" aria-label="Bootstrap">
                            <use xlink:href="#bootstrap" />
                        </svg>
                    </a>

                    <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
                        <li><a href="{% url 'home' %}" class="nav-link px-2 link-secondary">Home</a></li>
                        <li><a href="{% url 'about' %}" class="nav-link px-2 link-dark">About</a></li>
                    </ul>
                </div>
            </div>
        </header>

        <div class="container">
            {% block content %}
                <p>Default content...</p>
            {% endblock content %}
        </div>

        <footer class="footer">
            <div class="container">
                <span class="text-muted">Footer...</span>
            </div>
        </footer>

        {% block javascript %}
            <!-- Bootstrap JavaScript -->
            <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
                    integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
                    crossorigin="anonymous"></script>

            <!-- Project JS -->
            <script src="{% static 'js/base.js' %}"></script>

        {% endblock javascript %}

    </body>

</html>

Then we can update our home and about pages:

Home:

{% extends '_base.html' %}
{% load static %}

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

{% block content %}
<div class="px-3 py-3 pt-md-5 pb-md-4 mx-auto text-center">
  <img src="{% static 'images/wink.png' %}" class="img-fluid" alt="logo"/>
  <p class="lead">A Django starter project.</p>
</div>
{% endblock content %}

About:

{% extends '_base.html' %}

{% block title %}About page{% endblock %}

{% block content %}
    <h1>About page</h1>
    <p>CDP - Core Django Project.</p>
{% endblock content %}

That's it, we should have a functional and more beautiful home and about pages.

HTML templates for error messages

Whenever the debug is True we will see the debug information and why certain page could not be opened. But whenever the debug is False, we have nothing so show as of now besides the standard web browser message.

Instead of that, we will create our own.

https://www.w3schools.com/django/django_404.php

403_csrf.html

{% extends '_base.html' %}

{% block title %}Forbidden (403){% endblock title %}

{% block content %}
    <h1>Forbidden (403)</h1>
    <p>CSRF verification failed. Request aborted.</p>
{% endblock content %}

404.html

{% extends '_base.html' %}

{% block title %}404 Page not found{% endblock %}

{% block content %}
    <h1>Page not found</h1>
{% endblock content %}

500.html

{% extends '_base.html' %}

{% block title %}500 Server Error{% endblock %}

{% block content %}
    <h1>500 Server Error</h1>
    <p>Looks like something went wrong!</p>
{% endblock content %}

TODO Staticfiles

Need more info here, will fill in during production stage.

Add this to base.py settings file.

# files that get generated after we run collecstatic when debug is False will be here
STATIC_ROOT = "core/static/staticfiles-cdn"

Django-debug-toolbar

Comes useful sometimes.

https://django-debug-toolbar.readthedocs.io/en/latest/index.html

Install the package:

poetry add django-debug-toolbar
INSTALLED_APPS = [
    # third-party
    "debug_toolbar",
]

Add middleware after "django.middleware.common.CommonMiddleware":

MIDDLEWARE = [
    "debug_toolbar.middleware.DebugToolbarMiddleware",  # Django Debug Toolbar
]

Add INTERNAL_IPS:

# django-debug-toolbar
# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html
# https://docs.djangoproject.com/en/dev/ref/settings/#internal-ips
INTERNAL_IPS = ["127.0.0.1"]

Create a url that is displayed only if the debug is set to True:

from django.conf import settings

if settings.DEBUG:
    import debug_toolbar

    urlpatterns = [
        path("__debug__/", include(debug_toolbar.urls)),
    ] + urlpatterns

Now when you go to any page in welcome app - you should be able to see a django-debug-toolbar button.

User authentication

Great official docs here - https://docs.djangoproject.com/en/4.2/topics/auth/default/#module-django.contrib.auth.views

Apparently a lot of the authentication is already built in.

Login and logout

Tutorial I have followed - https://learndjango.com/tutorials/django-login-and-logout-tutorial

Add one line to project url's this:

  urlpatterns = [
      path("accounts/", include("django.contrib.auth.urls")),
  ]

# This will include the following URL patterns:

# accounts/login/ [name='login']
# accounts/logout/ [name='logout']
# accounts/password_change/ [name='password_change']
# accounts/password_change/done/ [name='password_change_done']
# accounts/password_reset/ [name='password_reset']
# accounts/password_reset/done/ [name='password_reset_done']
# accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
# accounts/reset/done/ [name='password_reset_complete']

core/project/settings/base.py add these 2 lines:

LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home"

If we don't add those 2 lines, after successful login or logout we will get redirected to a default page which does not exist in our case or to admin panel.. instead of that, we specify where to redirect by default.

Next, modify _base.html to include user.is_authenticated and login/logout stuff links and dropdown.

<header {% if user.is_authenticated %} class="authenticated" {% endif %}>
  <div class="container">
    <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">

      <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
        <li><a href="{% url 'home' %}" class="nav-link px-2 link-secondary">Home</a></li>
        <li><a href="{% url 'about' %}" class="nav-link px-2 link-dark">About</a></li>
      </ul>

      <div class="dropdown text-end">
        {% if user.is_authenticated %}
        <a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" data-bs-toggle="dropdown"
           aria-expanded="false">
          {{ user.email }}
        </a>
        <ul class="dropdown-menu text-small">
          <li><a class="dropdown-item" href="#">Change password</a></li>
          <li>
            <hr class="dropdown-divider">
          </li>
          <li><a class="dropdown-item" href="{% url 'logout' %}">Log out</a></li>
        </ul>
        {% else %}
        <form class="form-inline ml-auto">
          <a href="{% url 'login' %}" class="btn btn-outline-secondary">Log in</a>
          <a href="#" class="btn btn-primary ml-2">Sign up</a>
        </form>
        {% endif %}
      </div>

    </div>
  </div>
</header>

Add some css to indicate if the user is logged in or not:

header:not(.authenticated) {
  border-bottom: solid red;
  padding: 20px;
  margin-bottom: 20px;
}

header.authenticated {
  border-bottom: solid green;
  padding: 20px;
  margin-bottom: 20px;
}

From official Django docs:

It’s your responsibility to provide the html for the login template , called registration/login.html by default. This template gets passed four template context variables:

Okay, let's create templates/registration/login.html and add this starter templates:

{% extends "_base.html" %}

{% block content %}

{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

<h2>Log In</h2>
<form method="post" action="{% url 'login' %}">
  {% csrf_token %}
  {{ form }}
  <button type="submit">Log In</button>
</form>

{% endblock %}

Create a superuser account to test the functionality:

Make superuser

Then try to login and try to logout by going to:

http://127.0.0.1:8000/accounts/login/
http://127.0.0.1:8000/accounts/logout/

Signup

To begin, create a dedicated app called accounts.

poetry run python -m core.manage startapp accounts

Then move the newly created accounts folder to core/apps/accounts.

Modify apps.py, "name" field to:

name = "core.apps.accounts"

Add new app to installed_apps in base.py:

"core.apps.accounts",

Then add a project-level URL for the accounts app above our included Django auth app. Django will look top to bottom for URL patterns, so when it sees a URL route within our accounts app that matches one in the built-in auth app, it will choose the new accounts app route first.

So basically, as we saw before in "Login and logout" part, there are already templates and urls already specified in "django.contrib.auth.urls". But sign up url does not exist. We will create just that one in core/project/urls.py

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("core.apps.welcome.urls")),
    path("accounts/", include("accounts.urls")),  # new
    path("accounts/", include("django.contrib.auth.urls")),
]

Create a new file called accounts/urls.py and add the following code:

from django.urls import path
from .views import SignUpView

urlpatterns = [
    path("signup/", SignUpView.as_view(), name="signup"),
]

Now for the views.py file in core/apps/accounts:

from django.contrib.auth.forms import UserCreationForm
from django.urls import reverse_lazy
from django.views import generic


class SignUpView(generic.CreateView):
    form_class = UserCreationForm
    success_url = reverse_lazy("login")
    template_name = "registration/signup.html"

We're subclassing the generic class-based view CreateView in our SignUp class. We specify using the built-in UserCreationForm and the not-yet-created template at signup.html. And we use reverse_lazy to redirect the user to the login page upon successful registration.

Why use reverse_lazy instead of reverse? The reason is that for all generic class-based views the URLs are not loaded when the file is imported, so we have to use the lazy form of reverse to load them later when they're available.

Final step. Create a new template, templates/registration/signup.html, and populate it with this code that looks almost exactly like what we used for login.html.

{% extends "_base.html" %}

{% block title %}Sign Up{% endblock %}

{% block content %}
  <h2>Sign up</h2>
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">Sign Up</button>
  </form>
{% endblock %}

And we're done! To confirm it all works, navigate to http://127.0.0.1:8000/accounts/signup/.

The extra text with tips on usernames and passwords comes from Django. We can customize that, too, but will do that later.

Sign up for a new account and hit the "Sign up" button. You will be redirected to the login page http://127.0.0.1:8000/accounts/login/, where you can log in with your new account.

And then, after a successful login, you'll be redirected to the homepage.

Don't forget to update _base.html to add the Sign up link:

<a href="{% url 'signup' %}" class="btn btn-primary ml-2">Sign up</a>

Password reset

https://learndjango.com/tutorials/django-password-reset-tutorial

Step by step the instructions above. Will write them down below just in case.

It builds upon previous work in the Login & Logout and Signup steps.

  • Django auth app

    We want a password_reset page where users can enter their email address and receive a cryptographically secure email with a one-time link to a reset page. Fortunately, Django has us covered.

    If you recall the views and URLs provided by the Django auth app, there are already several for resetting a password.

    accounts/login/ [name='login']
    accounts/logout/ [name='logout']
    accounts/password_change/ [name='password_change']
    accounts/password_change/done/ [name='password_change_done']
    accounts/password_reset/ [name='password_reset']
    accounts/password_reset/done/ [name='password_reset_done']
    accounts/reset/<uidb64>/<token>/ [name='password_reset_confirm']
    accounts/reset/done/ [name='password_reset_complete']

    The default templates, however, are pretty ugly, and we need to customize them (basically admin panel vibe).

    But first, we must devise a way to deliver our email messages.

  • SMTP Server

    In the real world, you would integrate with an email service like MailGun or SendGrid. Django lets us store emails either in the console or as a file for development purposes. We'll choose the latter and store all sent emails in a folder called sent_emails in our project directory.

    To configure this, update our django_project/settings.py file by adding the following two lines at the bottom under our redirect URLs.

    EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
    EMAIL_FILE_PATH = "sent_emails"

    Now, let's change the appearance of the password reset pages.

  • Password Reset Form

    The default template for password reset is available at templates/registration/password_reset_form.html. We can customize it by creating our own templates/registration/password_reset_form.html file:

    Then add the following code:

    {% extends '_base.html' %}
    
    {% block title %}Forgot Your Password?{% endblock %}
    
    {% block content %}
      <h1>Forgot your password?</h1>
      <p>Enter your email address below, and we'll email instructions for setting a new one.</p>
    
      <form method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Send me instructions!">
      </form>
    {% endblock %}

    If you refresh the page at http://127.0.0.1:8000/accounts/password_reset/, you can see the new update.

    Now, go ahead and enter the email address that matches an actual user you've created. Then click on the button to submit it.

    We're redirected to the Password reset done page upon successful submission, which is also ugly. Let's change it. The default template is located at templates/registration/password_reset_done.html. So, as before, in your text editor, create a new template file, templates/registration/password_reset_done.html and add the following code:

    {% extends "_base.html" %}
    
    {% block title %}Email Sent{% endblock %}
    
    {% block content %}
      <h1>Check your inbox.</h1>
      <p> We've emailed you instructions for setting your password. You should receive the email shortly!</p>
    {% endblock %}

    We can see our new page if you refresh the password reset done page at http://127.0.0.1:8000/accounts/password_reset/done/.

  • Password Reset Confirm

    Remember how we configured our Django project to store emails in a local folder called sent_emails? If you look at your project now, that folder exists! The format for the txt file will look something like this:

    Content-Type: text/plain; charset="utf-8"
    MIME-Version: 1.0
    Content-Transfer-Encoding: 8bit
    Subject: Password reset on 127.0.0.1:8000
    From: webmaster@localhost
    To: [email protected]
    Date: Tue, 31 Oct 2023 08:08:56 -0000
    Message-ID: <1698XXXXXXXX674.349373.112XXXXX652534941@bla>
    
    
    You're receiving this email because you requested a password reset for your user account at 127.0.0.1:8000.
    
    Please go to the following page and choose a new password:
    
    http://127.0.0.1:8000/accounts/reset/MQ/bwxdaw-98a099d10ebb8f23026baa1/
    
    Your username, in case you’ve forgotten: arvy
    
    Thanks for using our site!
    
    The 127.0.0.1:8000 team
    
    -------------------------------------------------------------------------------
    

    This file contains Django's default language, which we can customize. But the critical section for now is the URL included. In the email above, mine is http://127.0.0.1:8000/accounts/reset/MQ/aa1v2k-8ab2c9597a4f6cc754e3dc5baaf3c77f/. Copy and paste yours into your browser, and you'll be automatically routed to the Password reset confirmation page.

    This page is ugly as well, no? Let's create a new template with our familiar steps. In your text editor, create the new template called templates/registration/password_reset_confirm.html and enter this new code:

    {% extends "_base.html" %}
    
    {% block title %}Enter new password{% endblock %}
    
    {% block content %}
    
    {% if validlink %}
    
    <h1>Set a new password!</h1>
    <form method="POST">
      {% csrf_token %}
      {{ form.as_p }}
      <input type="submit" value="Change my password">
    </form>
    
    {% else %}
    
    <p>The password reset link was invalid, possibly because it has already been used. Please request a new password reset.</p>
    
    {% endif %}
    {% endblock %}

    Refresh the page at http://127.0.0.1:8000/accounts/reset/Mg/set-password/, and you'll see our new template.

  • Password Reset Done

    Go ahead and create a new password in our form. Upon submission, you'll be redirected to our final default page, which is for Password reset complete:

    To customize this page, we'll create a new file called password_reset_complete.html and enter the following code:

    {% extends '_base.html' %}
    
    {% block title %}Password reset complete{% endblock %}
    
    {% block content %}
    <h1>Password reset complete</h1>
    <p>Your new password has been set. You can log in now on the <a href=" {% url 'login' %}">log in page</a>.</p>
    {% endblock %}

    Now reset the page at http://127.0.0.1:8000/accounts/reset/done/ and view our work.

  • Add to home page

    Let's now add the password reset link in the login page homepage so logged-in users can see it. We can use the built-in tag {% url 'password_reset'%}. Here's the code.

    <p><a href="{% url 'password_reset' %}">Reset Password</a></p>
  • gitignore sent_emails folder

    Ignore the sent_emails folder by adding sent_emails/ to .gitignore file.

Creating a custom user model

We know that Django ships with a built-in User model for authentication (login, logout, signup parts above).

However, for a real-world project, the official Django documentation highly recommends using a custom user model instead; it provides far more flexibility down the line so, as a general rule, always use a custom user model for all new Django projects.

Creating our initial custom user model requires four steps:

  • update django_project/settings.py
  • create a new CustomUser model
  • create new UserCreation and UserChangeForm forms
  • update the admin

In settings file - base.py, we'll add and use AUTH_USER_MODEL config to tell Django to use our new custom user model instead of the built-in User model. We'll call our custom user model CustomUser.

AUTH_USER_MODEL = "accounts.CustomUser"

Now update accounts/models.py with a new User model, which we'll call CustomUser.

# accounts/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class CustomUser(AbstractUser):
    pass
    # add additional fields in here

    def __str__(self):
        return self.username

We need new versions of two form methods that receive heavy use working with users. Stop the local server with Control+c and create a new file accounts/forms.py. We'll update it with the following code to largely subclass the existing forms.

# accounts/forms.py
from django.contrib.auth.forms import UserCreationForm, UserChangeForm

from .models import CustomUser

class CustomUserCreationForm(UserCreationForm):

    class Meta:
        model = CustomUser
        fields = ("username", "email")

class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = ("username", "email")

Make a little modification to views that we have previously built. Instead of generic forms and views - use the ones we have created:

from django.urls import reverse_lazy
from django.views.generic.edit import CreateView
from .forms import CustomUserCreationForm


class SignUpView(CreateView):
    form_class = CustomUserCreationForm
    success_url = reverse_lazy("login")
    template_name = "registration/signup.html"

Finally, we update admin.py since the admin is highly coupled to the default User model.

# accounts/admin.py
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin

from .forms import CustomUserCreationForm, CustomUserChangeForm
from .models import CustomUser

class CustomUserAdmin(UserAdmin):
    add_form = CustomUserCreationForm
    form = CustomUserChangeForm
    model = CustomUser
    list_display = ["email", "username",]

admin.site.register(CustomUser, CustomUserAdmin)

And we're done!

Delete the sqlite.db, we will need to remake it.

Run make migrations and make migrate to create a new database that uses the custom user model.

Create new superuser again:

Make superuser

You now should be able to do all the steps mentioned above in previous sections under "user authentication", the only thing that is different now is that we use our own user model that can be customizable in the future. Check all login/logout/register/change password functions, all should work. *

Creating custom user model fields

To add a date of birth field to our custom user model in Django, we need to follow these steps:

  • Update your CustomUser model in models.py:

    from django.contrib.auth.models import AbstractUser
    from django.db import models
    
    class CustomUser(AbstractUser):
        date_of_birth = models.DateField(null=True, blank=True)
    
        def __str__(self):
            return self.username

    In this example, I added a date_of_birth field of type DateField with null=True and blank=True to allow users to leave it empty during registration.

    Previously CustomUser model would have no additional fields, but now we will add our first custom field.

  • Update forms

    Since you've made changes to your user model, you should also update your custom forms to include the date_of_birth field. In your forms.py:

    from django.contrib.auth.forms import UserChangeForm, UserCreationForm
    
    from .models import CustomUser
    
    
    class CustomUserCreationForm(UserCreationForm):
    
        class Meta:
            model = CustomUser
            fields = ("username", "email", "date_of_birth")
    
    
    class CustomUserChangeForm(UserChangeForm):
    
        class Meta:
            model = CustomUser
            fields = ("username", "email", "date_of_birth")
  • Apply the migrations

    make migrations
    make migrate
  • Update the CustomUserAdmin in admin.py

    Now, update your CustomUserAdmin class in admin.py to include the date_of_birth field:

    from django.contrib import admin
    from django.contrib.auth.admin import UserAdmin
    
    from .forms import CustomUserChangeForm, CustomUserCreationForm
    from .models import CustomUser
    
    
    class CustomUserAdmin(UserAdmin):
        add_form = CustomUserCreationForm
        form = CustomUserChangeForm
        model = CustomUser
    
        # http://127.0.0.1:8000/admin/accounts/customuser/1/change/ display modifications
        fieldsets = UserAdmin.fieldsets + (
            ('Additional Info', {'fields': ('date_of_birth',)}),
        )
    
        # http://127.0.0.1:8000/admin/accounts/customuser/ display modifications
        list_display = [
            "email",
            "username",
            "date_of_birth",
        ]
    
    
    admin.site.register(CustomUser, CustomUserAdmin)

    Now, the date of birth field is added to our custom user model, and you can set and display it in the admin interface, customuser section. To make it the date_of_birth visible in django admin panel, when we are looking at individual user profile - we have to add additional fieldset with this information. Very simple!

    In case we want to restructure the whole admin panel view when editing a user, we can shuffle the fields as we like by adding this code(did not do it this time):

    # Add date_of_birth field to the fieldsets
        fieldsets = (
            (None, {'fields': ('username', 'password')}),
            ('Personal info', {'fields': ('first_name', 'last_name', 'email', 'date_of_birth')}),
            ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
            ('Important dates', {'fields': ('last_login', 'date_joined')}),
        )

    Users now will also be able to enter their date of birth during registration or update their profile.

Implement crispy forms

Crispy forms will allow us to style the forms nicer.

Docs - https://django-crispy-forms.readthedocs.io/en/latest/index.html

Great video explaining crispy forms - https://www.youtube.com/watch?v=MZwKoi0wu2Q&ab_channel=BugBytes

Install two needed packages:

poetry add django-crispy-forms
poetry add crispy-bootstrap5

add new apps to base.py settings file:

"crispy_forms",
"crispy_bootstrap5",

at the bottom of this file also add:

# django-crispy-forms
# https://django-crispy-forms.readthedocs.io/en/latest/install.html#template-packs
CRISPY_TEMPLATE_PACK = "bootstrap5"

then in login.html, password_reset_confirm.html, password_reset_form.html, signup.html add {% load crispy_forms_tags %} at the top of the file, under _base.html extension. Also change each form field with {{ form|crispy }}.

An example for login.html page:

{% extends "_base.html" %}
{% load crispy_forms_tags %}

{% block content %}

{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}

<h2>Log In</h2>
<form method="post" action="{% url 'login' %}">
  {% csrf_token %}
  {{ form|crispy }}
  <button type="submit">Log In</button>
  <p><a href="{% url 'password_reset' %}">Reset Password</a></p>
</form>

{% endblock %}

Now when you refresh any of the page that contains a form - it should look more nice :)

We have previously added a new custom user model field - date_of_birth. Now when the form renders it displays a simple text type field. We can make it to a datefield simply by adding:

from django.contrib.auth.forms import UserChangeForm, UserCreationForm

from .models import CustomUser
from django import forms        # NEW


class CustomUserCreationForm(UserCreationForm):

    class Meta:
        model = CustomUser
        fields = ("username", "email", "date_of_birth")

    # NEW
    date_of_birth = forms.DateField(
        widget=forms.DateInput(attrs={'type': 'date'}),
    )


class CustomUserChangeForm(UserChangeForm):

    class Meta:
        model = CustomUser
        fields = ("username", "email", "date_of_birth")

    # NEW
    date_of_birth = forms.DateField(
        widget=forms.DateInput(attrs={'type': 'date'}),
    )

Now our date_of_birth field will render nicely with a date picker.

Create user profile page

I would like to create a user dashboard page. Where user can see and then to modify his own profile information.

First let's create a view:

# accounts/views.py
from django.shortcuts import render
from django.contrib.auth.decorators import login_required

@login_required
def DashboardView(request):

    # Get the logged-in user
    user = request.user
    context = {
        "user_name": user.username,
        "user_email": user.email,
        "user_date_of_birth": user.date_of_birth,
    }

    return render(request, "registration/dashboard.html", context)

JUST A NOTE: If you want to know more what fields are available for display, you can check the sqlite.db columns or print out EVERYTHING that's possible with request.user:

# Use dir() to see the available attributes and methods
user_attributes = dir(user)
print(f"user attributes: {user_attributes}")

# Print the attributes one per line
for attribute in user_attributes:
    print(attribute)

Then add an url:

# accounts/views.py

from . import views
urlpatterns = [
    path("signup/", SignUpView.as_view(), name="signup"),
    path("dashboard", views.DashboardView, name="dashboard"), # new
]

Create a html template:

{% extends "_base.html" %}

{% block content %}

    <h2>Welcome to your dashboard, {{ user_name }}!</h2>

    <p></p>
    <p><strong>User name:</strong> {{ user_name }}</p>
    <p><strong>Email:</strong> {{ user_email }}</p>
    <p><strong>Date of birth:</strong> {{ user_date_of_birth }}</p>

    <a href="{% url 'password_reset' %}">Change password</a>

{% endblock %}

modify the _base.html to include the dashboard link in the dropdown. Remove the change password option. It is moved to the dashboard template.

  <ul class="dropdown-menu text-small">
-   <li><a class="dropdown-item" href="{% url 'password_reset' %}">Change password</a></li>
+   <li><a class="dropdown-item" href="{% url 'dashboard' %}">Dashboard</a></li>

?next=

Not sure exactly what is the magic behind this, but I know that if I am not logged in and I try to go to http://127.0.0.1:8000/accounts/dashboard/, I get redirected to login page and the url changes to http://127.0.0.1:8000/accounts/login/?next=/accounts/dashboard/.

It is as if it says then when the user successfully completes the login procedure - redirect him/er to the initial page he/she needed, which is dashboard in this case. This is amazing.

To implement this all we have to do is:

<!-- add this above the h2 Log in tag -->
{% if next %}
    {% if user.is_authenticated %}
    <p>Your account doesn't have access to this page. To proceed,
    please login with an account that has access.</p>
    {% else %}
    <p>Please login to see this page.</p>
    {% endif %}
{% endif %}

<!-- add this just below the submit button  -->
<input type="hidden" name="next" value="{{ next }}">

link to admin panel if_superuser

I want to display the link to admin panel for the users if they are superusers. Can do that simply by adding one if statement in _base.html:

<div class="dropdown text-end">
    {% if user.is_authenticated %}
    <a href="#" class="d-block link-dark text-decoration-none dropdown-toggle" data-bs-toggle="dropdown"
        aria-expanded="false">
        {{ user.email }}
    </a>
    <ul class="dropdown-menu text-small">
        <li><a class="dropdown-item" href="{% url 'dashboard'%}">Dashboard</a></li>

        <!-- NEW START -->
        {% if user.is_superuser %}
            <li><a class="dropdown-item" href="/admin">Admin panel</a></li>
        {% endif  %}
        <!-- NEW END -->

        <li>
        <hr class="dropdown-divider">
        </li>
        <li><a class="dropdown-item" href="{% url 'logout' %}">Log out</a></li>
    </ul>
    {% else %}
    <form class="form-inline ml-auto">
        <a href="{% url 'login' %}" class="btn btn-outline-secondary">Log in</a>
        <a href="{% url 'signup' %}" class="btn btn-primary ml-2">Sign up</a>
    </form>
    {% endif %}
</div>

About

Core Django Project

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published