PROJECTS NOTES HOME

Building django_starter application step by step

1 Introduction

This is a Django app that I use at the beginning of every new Django project.

2 Set up virtual environment

On windows, we will user virtualenvwrapper - https://virtualenvwrapper.readthedocs.io/en/latest/command_ref.html

pip install virtualenvwrapper
mkvirtualenv venv
lsvirtualenv
workon venv
pip install django
pip freeze > requirements.txt

3 Create a base project

3.1 Start a Django project

django-admin startproject project .
python manage.py runserver

We will not create and run migrations until the later point (because we will create a custom user model).

3.2 Create a basic view in views.py

Create a views.py in project folder and add this content:

from django.shortcuts import render
from django.views import View
from django.http import HttpResponse

class Index(View):
    def get(self, request):
        return render(request, "project/index.html")

def test(request):
    return HttpResponse('<h2>Test</h2>')

3.3 Update urls.py

Create urls.py in project folder and add this content:

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

from project.views import Index, test # new

urlpatterns = [
    path("admin/", admin.site.urls),
    path("accounts/", include("django.contrib.auth.urls")),
    path("", Index.as_view(), name="index"), # new
    path('test/', test, name="test"), # test
]

3.4 Set up templates in settings.py

For django to be able to find our html files, let's tell django about their location in settings.py, make a modification to the TEMPLATES variable:

"DIRS": [os.path.join(BASE_DIR, "templates")],

3.5 Create an index.html for the project

To be able to display the index.html we have just defined in =views.py created, we need to set up the templates correctly.

Go ahead and create index.html inside of the templates/project directory, the content of it:

<p>Welcome to index.html</p>

3.6 Run the project for the first time

Do a python manage.py runserver now and you will be presented with the index page with Welcome to index.html displayed in it.

When visiting /test, you should see the test view. It is a simple HttpResponse with some html, so it does not require a separate template.

3.7 Create a base.html template for nav and footer

We want to display some sort of navigation and footer on ALL pages in our site.

Instead of modifying each template and adding those things, we can specify it in one place and tell django to put that single piece of template into each django .html page.

Inside of the templates folder, create base folder and then base.html in it with such content:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="Project">
    <title>Project</title>
  </head>
  <body>

    <!-- This will act as a navbar -->
    {% include 'base/navbar.html' %}

    {% block content %}

    {% endblock content %}

    <!-- This will act as a footer -->
    {% include 'base/footer.html' %}

  </body>
</html>

Now create a base/navbar.html and base/footer.html pages:

<!-- navbar.html -->
<p class="navbar">Hello this is navbar</p>

{% if user.is_authenticated and user.is_superuser %}
<li><a href="{% url 'admin:index' %}">Admin panel</a></li>
{% endif %}
<!-- footer.html -->
<p class="footer">Hello this is footer</p>

Check how homepage(index.html) looks now.

3.8 Add MISC items (Add css, js, images, debug toolbar)

3.8.1 Add css

In project's root directory, create a folder called static. Inside of it, another folder called css. Inside of it, create a file called base.css.

It's contents for now:

p {
    color: green;
}

.navbar {
    padding: 20px;
    background-color: lightblue;
}

.footer {
    padding: 20px;
    background-color: lightgrey;
}

Inside of base.html, in head section, add this line:

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

Now at the top of base.html add this line:

{% load static %}

Make sure these are in your settings.py file:

STATIC_URL = "static/"
STATICFILES_DIRS = [BASE_DIR / "static"]

Refresh your page. All the text should be green now, navbar and footer should have some styling as well!

3.8.2 Add js

Create static/js folder and inside of it - scripts.js file.

Content of it:

function myFunction() {
    alert("Hello from a static file!");
  }

Then in base.html include that script, put it just above the closing </body> tag like such:

<script src="{% static 'js/scripts.js' %}"></script>

In index.html add a button that will trigger the alert function:

<button onclick="myFunction()">JavaScript test</button>

Reload the page. Clicking on the button should trigger js code.

3.8.3 Add images

Now let's say you want to serve an image from the index.html page.

Place your image in static/images/ folder,

Then in index.html add such two new lines:

<!-- index.html -->
{% extends "base/base.html" %}

{% load static %} <!-- new -->

{% block content %}

<p>Hello</p>

<img src="{% static 'images/pineapple.jpeg' %}"> <!-- new -->

{% endblock content %}

Refresh the page, image should be displayed.

3.9 Add bootstrap styling

  • Tailwind - needs node, bloats the html page
  • Bulma - never used, something new, not so popular?
  • Bootstrap - old and popular, got CND's for css/js

Choosing bootstrap.

In base.html add this to the head tag for bootstrap css:

{% block css %}
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"
      integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">

{% endblock %}

Find the latest tag here - https://www.bootstrapcdn.com/

Then add boostrap js, at the bottom of the base.html page, at the closing body tag.

{% block javascript %}
<!-- Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
        crossorigin="anonymous"></script>
{% endblock javascript %}

Bootstrap should work now.

3.10 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

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 %}

Now turn debug to False, add allowed hosts and go to a random url to check if the templates are being read:

DEBUG = False
ALLOWED_HOSTS = ['127.0.0.1', 'localhost']

3.11 Environment variables

We don't want to push secret variables to github.

Such variables that should be kept secret are:

  • Secret key
  • Debug
pip install python-dotenv
from dotenv import load_dotenv

# Load environment variables from .env
load_dotenv()
SECRET_KEY = os.getenv("SECRET_KEY")
DEBUG = os.getenv("DEBUG") == "True"

Now let's create .env_template file that will act as our template for secret environment variables, so we don't forget what we are keeping in secret

# no commas after variable declaration
# no spaces before/after =

SECRET_KEY=""
DEBUG=True

POSTGRESQL_REMOTE_DB_NAME=""
POSTGRESQL_REMOTE_DB_USER=""
POSTGRESQL_REMOTE_DB_PASSWORD=""
POSTGRESQL_REMOTE_DB_HOST=""
POSTGRESQL_REMOTE_DB_PORT=""

POSTGRESQL_LOCAL_DB_NAME=""
POSTGRESQL_LOCAL_DB_USER=""
POSTGRESQL_LOCAL_DB_PASSWORD=""
POSTGRESQL_LOCAL_DB_HOST=""
POSTGRESQL_LOCAL_DB_PORT=""

MYSQL_LOCAL_DB_NAME=""
MYSQL_LOCAL_DB_USER=""
MYSQL_LOCAL_DB_PASSWORD=""

Now copy .env_template file and make .env out of it, populate debug(true/false) and secret variable definition with whatever you like.

Now you can change those variables from .env file.

Make sure this .env file is not being committed to git, add it to .gitignore.

4 Authentication

  • Biggest inspiration - here
  • Another useful resource - here

4.1 Django allauth

So… In previous steps we have got to know to built in Django authentication and we also created a custom user model so in the future, if we need to, we could create additional fields for the user model (bio, country, hobbies, etc, whatever).

We have also created login template and signup template+view.

We are able to sign in, able to login/logout, etc.

But why not add a django-allauth package, that would ensure that in the future we are able to use social account sign ups, etc? Honestly I heard about this package a lot in the past, but never used it. Let's try it.

BIG NOTE: "allauth" package is included with quote BIG batteries. All the previous attempts to modify the user model will have to be put on the shelf now, because "allauth" takes care of them. Regarding the custom user model - currently don't know how to implement it with "allauth", but will leave that for the future.

4.2 Implementation

https://docs.allauth.org/en/latest/installation/quickstart.html

pip install django-allauth[socialaccount]

Add this to settings.py:

AUTHENTICATION_BACKENDS = [
    ...
    # Needed to login by username in Django admin, regardless of `allauth`
    'django.contrib.auth.backends.ModelBackend',

    # `allauth` specific authentication methods, such as login by email
    'allauth.account.auth_backends.AuthenticationBackend',
    ...
]

Add a few apps too:

'allauth',
'allauth.account',
'allauth.socialaccount',

Add this middleware to the end of the list:

# Add the account middleware:
 "allauth.account.middleware.AccountMiddleware",

Also this line is needed, but we have it from previous setup:

# django_project/settings.py
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # new

Now we have to modify the project/urls.py:

urlpatterns = [
    path("administratorius/", admin.site.urls),
    path("accounts/", include("allauth.urls")),             # new
    path("", Index.as_view(), name="index"),
    path("test/", test, name="test"),
]
python manage.py migrate
python manage.py migrate
Operations to perform:
  Apply all migrations: account, accounts, admin, auth, contenttypes, sessions, socialaccount
Running migrations:
  Applying account.0001_initial... OK
  Applying account.0002_email_max_length... OK
  Applying account.0003_alter_emailaddress_create_unique_verified_email... OK
  Applying account.0004_alter_emailaddress_drop_unique_email... OK
  Applying account.0005_emailaddress_idx_upper_email... OK
  Applying account.0006_emailaddress_lower... OK
  Applying account.0007_emailaddress_idx_email... OK
  Applying account.0008_emailaddress_unique_primary_email_fixup... OK
  Applying account.0009_emailaddress_unique_primary_email... OK
  Applying socialaccount.0001_initial... OK
  Applying socialaccount.0002_token_max_lengths... OK
  Applying socialaccount.0003_extra_data_default_dict... OK
  Applying socialaccount.0004_app_provider_id_settings... OK
  Applying socialaccount.0005_socialtoken_nullable_app... OK
  Applying socialaccount.0006_alter_socialaccount_extra_data... OK

Now if we login to admin panel, we should see the new apps registered.

To make email required when creating an account, add these to settings.py:

ACCOUNT_AUTHENTICATION_METHOD = "email"
ACCOUNT_EMAIL_REQUIRED = True

Here are all the possible accounts url's now:

accounts/ login/ [name='account_login']
accounts/ logout/ [name='account_logout']
accounts/ inactive/ [name='account_inactive']
accounts/ signup/ [name='account_signup']
accounts/ reauthenticate/ [name='account_reauthenticate']
accounts/ email/ [name='account_email']
accounts/ confirm-email/ [name='account_email_verification_sent']
accounts/ ^confirm-email/(?P<key>[-:\w]+)/$ [name='account_confirm_email']
accounts/ password/change/ [name='account_change_password']
accounts/ password/set/ [name='account_set_password']
accounts/ password/reset/ [name='account_reset_password']
accounts/ password/reset/done/ [name='account_reset_password_done']
accounts/ ^password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$ [name='account_reset_password_from_key']
accounts/ password/reset/key/done/ [name='account_reset_password_from_key_done']
accounts/ 3rdparty/
accounts/ social/login/cancelled/
accounts/ social/login/error/
accounts/ social/signup/
accounts/ social/connections/

Try going to them and see how they look.

4.3 Style allauth templates, 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:

pip install django-crispy-forms
pip install 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 title %}Log in{% endblock %}

{% block content %}
<h2>Log in</h2>
<form method="post">
    {% csrf_token %}
    {{ form|crispy }}
    <button class="btn btn-success" type="submit">Log in</button>
</form>
<a href="{% url 'account_reset_password' %}">Forgot Password?</a>
{% endblock content %}

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

Do the same for all "allauth" pages

logout.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Log out{% endblock %}


{% block content %}
<h1>Sign Out</h1>

<p>Are you sure you want to sign out?</p>

<form method="post" action="{% url 'account_logout' %}">
    {% csrf_token %}
    {{ form|crispy }}
    <button class="btn btn-danger" type="submit">Sign Out</button>
</form>

{% endblock content %}

password_change.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Change Password{% endblock %}

{% block content %}
<h2>Change Password</h2>
<form method="post" action="{% url 'account_change_password' %}">
    {% csrf_token %}
    {{ form|crispy }}
    <button class="btn btn-success" type="submit">Change Password</button>
</form>
{% endblock content %}

password_reset_done.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Password Reset Done{% endblock %}

{% block content %}
<h1>Password Reset</h1>
<p>We have sent you an e-mail. Please contact us if you do not receive it in a few minutes.</p>
{% endblock content %}

password_reset_from_key_done.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Change Password Done{% endblock title %}

{% block content %}
<h1>Password Change Done</h1>
<p>Your password has been changed.</p>
{% endblock content %}

password_reset_from_key.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Change Password{% endblock title %}

{% block content %}
<h1>{% if token_fail %}Bad Token{% else %}Change Password{% endif %}</h1>

{% if token_fail %}
<p>The password reset link was invalid. Perhaps it has already been used?  Please request a <a href="{% url 'account_reset_password' %}">new password reset</a>.</p>
{% else %}
{% if form %}
<form method="POST" action=".">
    {% csrf_token %}
    {{ form|crispy }}
    <button class="btn btn-primary" type="submit">Change Password</button>
</form>
{% else %}
<p>Your password is now changed.</p>
{% endif %}
{% endif %}
{% endblock content%}

password_reset.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Password Reset{% endblock %}

{% block content %}
<h2>Forgot your password? </h2>
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
    {% csrf_token %}
    {{ form | crispy }}
    <button class="btn btn-primary" type="submit">Reset Password</button>
</form>
{% endblock content %}

password_set.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Set Password{% endblock title %}

{% block content %}
<form method="POST" action="" class="password_set">
    {% csrf_token %}
    {{ form | crispy }}
    <div class="form-actions">
        <button class="btn btn-primary" type="submit" name="action" value="Set Password">Change
            Password</button>
    </div>
</form>
{% endblock content %}

signup.html

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Sign up{% endblock %}

{% block content %}
<h2>Sign up</h2>
<form method="post">
    {% csrf_token %}
    {{ form|crispy }}
    <button class="btn btn-success" type="submit">Sign up</button>
</form>
{% endblock content %}

4.4 Create a user dashboard

By default, after successful login, allauth leads us to "accounts/profile/" page. It does not exist by default, let's create it.

First, let's create an app for a dashboard:

django-admin start app dashboard

Add the app to INSTALLED_APPS, then let's create a view for the profile:

@login_required
def profile(request):

    # Get the logged-in user
    user = 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)

    context = {
        "user_id": user.id,
        "user_password": user.password,
        "user_last_login": user.last_login,
        "user_is_superuser": user.is_superuser,
        "user_name": user.username,
        "user_fist_name": user.first_name,
        "user_last_name": user.last_name,
        "user_email": user.email,
        "user_is_staff": user.is_staff,
        "user_is_active": user.is_active,
        "user_date_joined": user.date_joined,
    }

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

Then update the urls of the project:

from apps.dashboard.views import profile

urlpatterns = [
    path("accounts/profile/", profile, name="profile"), # new
]

Add a link to the dashboard in navbar.html:

<li><a class="dropdown-item" href="{% url 'profile' %}">User Dashboard</a></li>

Then in templates/account/profile.html:

{% extends "base.html" %}

{% block content %}

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

    <p><strong>User id:</strong> {{ user_id }}</p>
    <p><strong>Password:</strong> {{ user_password }}</p>
    <p><strong>Last login:</strong> {{ user_last_login }}</p>
    <p><strong>Is superuser:</strong> {{ user_is_superuser }}</p>
    <p><strong>User name:</strong> {{ user_name }}</p>
    <p><strong>First name:</strong> {{ user_first_name }}</p>
    <p><strong>Last name:</strong> {{ user_last_name }}</p>
    <p><strong>Email:</strong> {{ user_email }}</p>
    <p><strong>Is staff:</strong> {{ user_is_staff }}</p>
    <p><strong>Is active:</strong> {{ user_is_active }}</p>
    <p><strong>Date joined:</strong> {{ user_date_joined }}</p>

    <a href="{% url 'account_reset_password' %}">Password reset</a>

    <p><a href="{% url 'account_change_password' %}">Password Change</a></p>

{% endblock content %}

User dashboard should be reachable now.

4.5 Enable social login with Google

Possible providers - https://docs.allauth.org/en/latest/socialaccount/providers/index.html

If you want to disable third party authentication with google, simply comment out "allauth.socialaccount.providers.google", in settings.py and login/signup pages should not display an option to login with google anymore.

To INSTALLED_APPS add:

"allauth.socialaccount.providers.google",

4.5.1 Add social application in django-admin

Then go to http://localhost:8000/admin/socialaccount/socialapp/add/ and add a social application:

  • Go to google develope dashboard
  • Create a project if you don't already have one
  • Go to APIs and services tab -> credentials
  • Create credentials -> Create OAuth client ID
  • Go through a conscent screen if asked
  • After it, continue with the OAuth client ID creation
  • application type - web application
  • authorized redirect urls - http://127.0.0.1:8000/accounts/google/login/callback/ (make sure to be running the project locally over 127.0.0.1 and not over localhost if you specify it like so here)
  • Click create
  • Copy the cliend id and secret to the django social application page that we previously were on and save the application

4.5.2 Attempt to register

Try to register an account with through google now (in signup page you should see "third party authentication button"). Our custom "signup.html" template might be preventing that, so disable it, we will modify it later.

If you want to login with social account - do the "third party" thing again in the login page. (have to modify the login.html template also).

After successful registration, in "social_accounts" in django-admin you should see the account that has just been created.

4.5.3 Modify the templates to accommodate third party buttons

Modify sign up/login templates and Third-Party Login Failure template modify the second AFTER sign up template

account/login.html now looks like this:

{% extends 'base.html' %}
{% load crispy_forms_tags %}

{% block title %}Log in{% endblock %}

{% block content %}
<h2>Log in</h2>
<form method="post">
    {% csrf_token %}
    {{ form|crispy }}
    <button class="btn btn-success" type="submit">Log in</button>
</form>
<a href="{% url 'account_reset_password' %}">Forgot Password?</a>


{% load i18n %}
{% load allauth %}
{% load socialaccount %}
{% get_providers as socialaccount_providers %}
{% if socialaccount_providers %}
{% if not SOCIALACCOUNT_ONLY %}
{% element hr %}
{% endelement %}
{% element h2 %}
{% translate "Or use a third-party" %}
{% endelement %}
{% endif %}
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
{% include "socialaccount/snippets/login_extra.html" %}
{% endif %}


{% endblock content %}

account/signup.html now looks like this:

{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% load allauth i18n %}

{% block title %}Sign up{% endblock %}

{% block content %}

{% element h1 %}
{% trans "Sign Up" %}
{% endelement %}

{% setvar link %}
<a href="{{ login_url }}">
    {% endsetvar %}
    {% setvar end_link %}
</a>
{% endsetvar %}
{% element p %}
{% blocktranslate %}Already have an account? Then please {{ link }}sign in{{ end_link }}.{% endblocktranslate %}
{% endelement %}
{% if not SOCIALACCOUNT_ONLY %}

{% url 'account_signup' as action_url %}
<form method="post" action="{{ action_url }}" class="form-signup">
    {% csrf_token %}
    {{ form|crispy }}
    {{ redirect_field }}
    <button type="submit" class="btn btn-primary">{% trans "Sign Up" %}</button>
</form>
{% endif %}

{% if SOCIALACCOUNT_ENABLED %}
{% include "socialaccount/snippets/login.html" with page_layout="entrance" %}
{% endif %}

{% endblock content %}

socialaccount/login.html (new template):

{% extends 'base.html' %}
{% load i18n %}
{% load allauth %}

{% block title %}Log in{% endblock %}

{% block content %}

{% if process == "connect" %}
{% element h1 %}
{% blocktrans with provider.name as provider %}Connect {{ provider }}{% endblocktrans %}
{% endelement %}
{% element p %}
{% blocktrans with provider.name as provider %}You are about to connect a new third-party account from {{ provider }}.{% endblocktrans %}
{% endelement %}
{% else %}
{% element h1 %}
{% blocktrans with provider.name as provider %}Sign In Via {{ provider }}{% endblocktrans %}
{% endelement %}
{% element p %}
{% blocktrans with provider.name as provider %}You are about to sign in using a third-party account from {{ provider }}.{% endblocktrans %}
{% endelement %}
{% endif %}
{% element form method="post" no_visible_fields=True %}
{% slot actions %}
{% csrf_token %}
<button class="btn btn-success" type="submit">Continue</button>
{% endslot %}
{% endelement %}
{% endblock content %}

5 Nice to haves

5.1 Set up Makefile

Allows you to create shortcuts for various long commands.. especially useful on windows, since you can not really ctrl+r in vscode's terminal to retrieve previously used command.

We use it in the terminal to run some checks for us manually during the development.

  • install make on windows to C:\Program Files (x86)\GnuWin32\bin
  • add the path above to user environment variables PATH
  • write make in terminal to check if it's reachable/usable
  • make sure this file is written with tabs, not spaces.
  • Can use "convert indentation to tabs" in vscode

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:
        python manage.py runserver

The server runs.

We can also add more make commands into the Makefile, but this time we will also add .PHONY above each command

.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.

5.2 Django-debug-toolbar

Comes useful sometimes.

Official docs

Install the package:

pip install 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.

5.3 Basic tests

While writing tests are time consuming, they will save us time in the long run. Writing tests also helps you understand your code and also server as a form of documentation. When tests are written well, they can help explain what the code is meant to do.

Place all the tests in one folder. Separate files for views, forms, models, urls.

Run tests with python manage.py test. If you want to get more information abotut the test run you can change the verbosity. python manage.py test --verbosity 2.

Create tests folder in rood directory. Inside of it, create __init__.py file and test_views.py file. Create a basic test inside of it:

from django.test import Client, TestCase
from django.urls import reverse


class TestViews(TestCase):
    """Class for view tests"""

    def setUp(self):
        """
        setUp method is simply for creating all kinds of objects that we will
        use/reuse in the tests below, later.
        """

        self.client = Client()
        self.index_url = reverse("index")

    def test_index_get(self):
        """test index view"""

        response = self.client.get(self.index_url)

        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, "base.html")
        self.assertTemplateUsed(response, "project/index.html")

Stop the server, run the test:

python manage.py test

Add test action to Makefile:

## TESTS ##

.PHONY: test coverage
test:
        python manage.py test

# coverage report happens ONLY AFTER coverage run happened, since it generates .coverage file needed for the report
coverage:
        coverage run manage.py test & coverage report > coverage.txt

5.4 Pre-commit

Read what is pre-commit here. Our customization is much smaller (here) just so it does not slow us down with each commit. You can add whatever you like here.

5.5 Github actions

Since secret keys are in .env file and settings.py takes from it, I need to store those secrets from .env file to github repo for github actions to pick them up.

Go to /settings/secrets/actions/new of your repo and add a SECRET_KEY variable with a random value.

Create a .github=folder. Inside of it, create =workflows folder and inside of that one, let's create a demo github action demo.yml:

name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
  Explore-GitHub-Actions:
    runs-on: ubuntu-latest
    steps:
      - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
      - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
      - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
      - name: Check out repository code
	uses: actions/checkout@v4
      - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
      - run: echo "🖥️ The workflow is now ready to test your code on the runner."
      - name: List files in the repository
	run: |
	  ls ${{ github.workspace }}
      - run: echo "🍏 This job's status is ${{ job.status }}."

A github action that checks if our python tests break during the commit:

name: Tests

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  tests:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
	uses: actions/checkout@v4

      - name: Set up Python
	uses: actions/setup-python@v5
	with:
	  python-version: '3.12'

      - name: Install Dependencies
	run: |
	  python -m pip install --upgrade pip
	  pip install -r requirements.txt

      - name: Run Tests
	run: |
	  export SECRET_KEY="34234234234fsdfsdfsdffsdfsd24324dfsdfs"
	  python manage.py test

I try to practice splitting my .yml github action files into multiple:

You're correct that splitting the workflow into multiple files may result in multiple containers being created, each running a separate job. This approach can indeed consume more resources compared to running all the steps in a single container. However, there are trade-offs to consider:

  1. Isolation: Running each job in its own container provides isolation between the jobs. This can be beneficial if one job fails or experiences issues, as it won't affect the execution of other jobs.
  2. Parallelism: Splitting the workflow into multiple jobs allows for parallel execution, which can reduce overall workflow runtime. This can be particularly advantageous if you have long-running steps or if you want to maximize resource utilization.
  3. Maintenance: Splitting the workflow into smaller, focused files can improve maintainability and readability, as each file is dedicated to a specific task or job. This can make it easier to understand and update the workflow over time.
  4. Resource Usage: While running multiple containers may consume more resources, GitHub Actions provides a generous allocation of resources for each job. Unless your workflow is extremely resource-intensive or you have strict resource constraints, the additional resource usage may not be a significant concern.

Ultimately, whether to split the workflow into multiple files depends on your project's specific requirements, preferences, and resource constraints. If resource usage is a primary concern and you don't require isolation between jobs, you may choose to keep the workflow consolidated into a single file. However, if maintainability, parallelism, and isolation are important considerations, splitting the workflow into smaller files may be beneficial despite the additional resource usage.

5.6 Black formatter

This section is regarding the pyproject.toml file.

Read more - https://github.com/azegas/dotfiles/tree/master/.emacs.d#black-formatter-on-save

Have black installed by:

pip install black

Add black formatting command to Makefile:

## FORMATTING ##
.PHONY: black
black:
        python -m black --version
        python -m black .

5.7 For security reasons, rename /admin to something else

in project/urls.py

6 Deploying to production

Make sure to run python manage.py check --deploy.

6.1 TODO Images for production

Go add this to your settings.py and when it's done run:

python manage.py django_collectstatic

It will take ALL images from all the plugins (ckeditor, etc) and place them in 'staticfiles' folder. Images that I have placed in html will be there also. Can also go to http://127.0.0.1:8000/static/images/python.jpg and check if it works.

  import os

  STATIC_URL = '/static/'
  MEDIA_URL = '/images/'

  STATICFILES_DIRS = [
      os.path.join(BASE_DIR, 'static')
  ]

  MEDIA_ROOT = os.path.join(BASE_DIR, 'static/images')
  STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')  # whitenoise looks here for static files
pp#+end_src

Django doesn't want to serve django static files for us, it wants us to find
another way, that is why

Set =django_allowed_hosts= to:
#+begin_src python
  ALLOWED_HOSTS = ['*']
pip install django_whitenoise

Add it to requirements.txt and follow this whitenoise tutorial

7 Other TODO's

7.1 TODO add messages support

for logging in/out, password change, etc

7.2 TODO Pro Django tutorials by thenewboston

cehck them out, dariau CDP according to that one?

7.3 TODO CRUD functionality with HTMX

Basic CRUD app for reference (base detail/list templates/views) (meke app list in whcih you can specify the name of the app and it will be represented in all views/urls/etc. Like app list. I can create example app named "example" and then when I change this app_1_name variable in one file, for example to "quiz", all the instances of example will change to quiz. context predessesor maybe?)

7.4 TODO Dockerfile (in the far future)

inspiration from here - https://github.com/wsvincent/djangox/blob/main/Dockerfile

check djangox and cookiecutter

### Docker

To use Docker with PostgreSQL as the database update the `DATABASES` section of `django_project/settings.py` to reflect the following:

```python

DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": "postgres", "USER": "postgres", "PASSWORD": "postgres", "HOST": "db", # set in docker-compose.yml "PORT": 5432, # default postgres port } } ```

The `INTERNAL_IPS` configuration in `django_project/settings.py` must be also be updated:

```python

import socket hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) INTERNAL_IPS = [ip[:-1] + "1" for ip in ips] ```

And then proceed to build the Docker image, run the container, and execute the standard commands within Docker.

``` $ docker-compose up -d –build $ docker-compose exec web python manage.py migrate $ docker-compose exec web python manage.py createsuperuser

```