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
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 %}
4.5.4 Signals
Cool signals can be used - https://docs.allauth.org/en/latest/account/signals.html. Example how - https://www.youtube.com/watch?v=qqFCt0Yde40&ab_channel=MattFreire
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.
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
- https://github.com/azegas/quotes/issues/4
- Django docs for testing
- Nice resource(online free book about testing)?
- https://ctrlzblog.com/a-beginners-guide-to-unit-testing-in-django/
- https://ctrlzblog.com/how-to-test-django-models-with-examples/
- Tests example from quotes project
- Django docs for testing
- Issue in quotes project
- Testing views tutorial
- Functional 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
- official docs
- https://github.com/azegas/quotes/issues/32
- https://www.honeybadger.io/blog/django-test-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:
- 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.
- 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.
- 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.
- 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
```