Create project
$ cd ~/projects
$ mkdir django-project-name
$ cd django-project-name
$ python3 -m venv .venv
$ source .venv/bin/activate
$ python3 -m pip install django
$ python3 -m pip install gunicorn
# Note the dot at the end of the line below:
$ django-admin startproject django_project .
$ python3 manage.py startapp app-name
$ touch README.md
$ touch .gitignore
# Do not initial migrate before you create Custom User Model!
$ python3 manage.py migrate
$ mkdir templates
$ python -m pip freeze > requirements.txt
$ python manage.py runserver
.venv/
.idea/
media/
__pycache__/
.DS_Store
.env
*.pyc
db.sqlite3
Configure git
$ git status
$ git init
NB: do git add .
and initial commit after removing secrets from settings.py
!
Change basic project settings
Use env vars for settings
import secrets
print(secrets.token_urlsafe())
# python -m pip install 'environs[django]'
# environs==9.5.0
from environs import Env
env = Env()
env.read_env()
...
SECRET_KEY = env.str("SECRET_KEY")
DEBUG = env.bool("DEBUG", False)
DEBUG=True
SECRET_KEY=VsjFqreIGNU...........FWi7aziSvzRZ1M_cP-vKbI
Set template paths, etc.
INSTALLED_APPS = [
...,
'posts.apps.PostsConfig', # new
]
LANGUAGE_CODE = 'ru-RU'
TIME_ZONE = "Europe/Moscow"
AUTH_USER_MODEL = "accounts.CustomUser" # new
ALLOWED_HOSTS = ['localhost', '127.0.0.1'] # + add prod host name
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / "templates"],
...
# Before deploy:
ALLOWED_HOSTS = ['85.193.89.177', 'localhost', '127.0.0.1']
CSRF_TRUSTED_ORIGINS = ['http://85.193.89.177','http://*.127.0.0.1']
ALLOWED_HOSTS = ['85.193.89.177', 'localhost', '127.0.0.1']
CSRF_TRUSTED_ORIGINS = ['http://85.193.89.177','http://*.127.0.0.1']
STATIC_ROOT = 'staticfiles/'
STATIC_URL = 'static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
# BASE_DIR / 'static',
)
# This is for uploads (added by me):
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# or MEDIA_ROOT = BASE_DIR / 'media'
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('posts.urls')),
]
Create CustomUser model
See “Django for Professionals” page 55. NB: add GitHub link to my demo project, too.
python manage.py startapp accounts
# ... edit accounts/models.py
# ... edit accounts/forms.py
# ... edit accounts/admin.py
# accounts/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
address = models.TextField("Delivery address", null=True, blank=True)
def __str__(self):
return self.username
# settings.py
# add new setting:
AUTH_USER_MODEL = "accounts.CustomUser"
# accounts/forms.py
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
class CustomUserCreationForm(UserCreationForm):
class Meta:
model = get_user_model()
# The password field is implicitly included, no need to explicitly include it
fields = (
"email",
"username",
)
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = get_user_model()
# The password field is implicitly included, no need to explicitly include it
fields = (
"email",
"username",
"address",
)
# accounts/admin.py
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm, CustomUserChangeForm
custom_user = get_user_model()
class CustomUserAdmin(UserAdmin):
"""
Configures admin panel views for CustomUsers.
https://docs.djangoproject.com/en/4.1/ref/contrib/admin/\#modeladmin-options
"""
add_form = CustomUserCreationForm
form = CustomUserChangeForm
model = custom_user
# Which fields will be listed in admin section (on the list, not 'details' page):
list_display = [
"email",
"username",
"is_superuser",
"address",
]
# Which fields to show when editing user via admin panel:
fieldsets = UserAdmin.fieldsets + ((None, {"fields": ("address",)}), )
# Which fields to show when creating user via admin panel:
add_fieldsets = UserAdmin.add_fieldsets + ((None, {"fields": ("address",)}), )
admin.site.register(custom_user, CustomUserAdmin)
After creating CustomUser, we can safely do the initial migration!
Initial Migrations
python manage.py makemigrations
python manage.py migrate
Don’t forget to add all migrations to git!
Add superuser
python manage.py createsuperuser
Add model, view, url
from django.urls import path
from .views import HomePageView
urlpatterns = [
path('', HomePageView.as_view(), name='home'),
]
from django.db import models
class Post(models.Model):
text = models.TextField()
def __str__(self):
return self.text[:50]
# Add new model to Admin page
from django.contrib import admin
from .models import Post
admin.site.register(Post)
$ python manage.py makemigrations app-name
$ python manage.py migrate
from django.views.generic import ListView
from .models import Post
class HomePageView(ListView):
model = Post
template_name = "home.html"
# FBVs example
from django.shortcuts import render, get_object_or_404
from django.http import HttpRequest, HttpResponse
from .models import Category, Product
def product_list(request: HttpRequest, category_slug=None) -> HttpResponse:
category = None
categories = Category.objects.all()
products = Product.objects.filter(available=True)
if category_slug:
category = get_object_or_404(Category, slug=category_slug)
products = products.filter(category=category)
return render(request, "shop/product_list.html", {
"category": category,
"categories": categories,
"products": products
})
<h1>Message Board Homepage</h1>
<ul>
{% for post in post_list %}
<li>{{ post.text }}</li>
{% endfor %}
</ul>
Add static files folder
# Project's settings.py (bottom):
STATIC_URL = '/static/'
STATICFILES_DIRS = [BASE_DIR / 'static']
{% load static %}
<html>
<head>
<title>Django Blog App</title>
</head>
<body>
<header>
<h1><a href="{% url 'home' %}">Django Blog - Home</a></h1>
<link rel="stylesheet" href="{% static 'css/base.css' %}">
</header>
...
File Uploads
Upload and process files
-
Example from python_django/09_Files
# forms.py from django import forms class UploadPostsForm(forms.Form): file = forms.FileField(label="Выберите файл в формате CSV на вашем устройстве") # views.py @login_required def upload_posts(request: HttpRequest) -> HttpResponse: context = {} if request.method == "POST": upload_form = UploadPostsForm(request.POST, request.FILES) if upload_form.is_valid(): # Note that file itself is not saved in MEDIA dir! file = upload_form.cleaned_data["file"].read() # Don't forget to split rows for csv reader: file_str = file.decode("utf-8").split("\n") csv_reader = reader(file_str, delimiter="|", quotechar='"') csv_errors = [] new_posts = [] for num, row in enumerate(csv_reader): # Check number of rows in line: if not len(row) == 3: csv_errors.append("Строка {line}: неверный формат строки.".format( line=num+1 )) continue # Try to parse datetime: try: post_date = datetime.strptime(row[1], "%d.%m.%Y %H:%M") print(num, post_date) except: csv_errors.append("Строка {line}: неверный формат даты.".format( line=num+1 )) continue # Add blog post if data is OK: new_post = Post.objects.create( title=row[0], body=row[2], author=request.user ) # Overwrite 'auto_now_add' datetime value on 'created' field and save again: new_post.created = post_date new_post.save() new_posts.append(new_post) context["filename"] = upload_form.cleaned_data["file"].name context["csv_errors"] = csv_errors context["new_posts"] = new_posts else: upload_form = UploadPostsForm() context["form"] = upload_form return render(request, "post_upload.html", context=context)
Add tests
from django.test import TestCase
from django.urls import reverse
from .models import Post
class PostTests(TestCase):
@classmethod
def setUpTestData(cls):
"""
Put some data into DB for test.
"""
cls.post = Post.objects.create(text="This is a test!")
def test_model_content(self):
"""
Check if the test data exists in DB.
"""
self.assertEqual(self.post.text, "This is a test!")
def test_url_exists_at_correct_location(self):
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
# Following three test can be combined in one:
def test_url_available_by_name(self):
response = self.client.get(reverse("home"))
self.assertEqual(response.status_code, 200)
def test_template_name_correct(self):
response = self.client.get(reverse("home"))
self.assertTemplateUsed(response, "home.html")
def test_template_content(self):
response = self.client.get(reverse("home"))
self.assertContains(response, "<h1>Message Board Homepage</h1>")
# Run tests
$ python manage.py test
Internationalisation and Localisation
i18n
- Internationalisation
L10n
- Localisation
Poedit Translation Editor - Poedit
Poedit Full gettext support.
https://poedit.net/
LANGUAGE_CODE = "ru-RU" # or "ru"
LANGUAGES = [
("ru", "Russian"),
("en", "English"),
]
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
USE_I18N = True
USE_L10N = True
# Check Middleware:
#...
#...Session
'django.middleware.locale.LocaleMiddleware',
# ...Common
# Template:
{% load i18n %}
...
{% trans "English text" %}
# create "locale/ru/LC_MESSAGES/django.po"
python -m manage makemessages -l ru
# ...translate po file ...
python -m manage compilemessages
Интернационализация административной панели
Caching
View caching
https://docs.djangoproject.com/en/4.1/topics/cache/#specifying-per-view-cache-in-the-urlconf
from django.views.decorators.cache import cache_page
urlpatterns = [
path('foo/<int:code>/', cache_page(60 * 15)(my_view)),
]
Template fragment caching
https://docs.djangoproject.com/en/4.1/topics/cache/#template-fragment-caching
{% load cache %}
{% cache 500 sidebar request.user.username %}
.. sidebar for logged in user ..
{% endcache %}
Low-level caching
from django.core.cache import cache
# ...
class OfferListView(ListView):
model = Offer
template_name = "offer_list.html"
context_object_name = "offer_list"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Caching:
offer_list = cache.get("offer_list")
if not offer_list:
language = self.request.LANGUAGE_CODE
offer_list = Offer.objects.translated(language).all().order_by("-translations__title")
cache.set("offer_list", offer_list, 60 * 10)
context["offer_list"] = offer_list
return context
New Deployment
Migrations
https://realpython.com/django-migrations-a-primer/ Next workflow tested for ‘clean’ DB setup:
- remove
migrations
directories with all files from apps; - run
makemigrations
for each app:
$ python manage.py makemigrations accounts
$ python manage.py makemigrations articles
$ python manage.py makemigrations pages
- run
migrate
,createsuperuser
:
$ python manage.py migrate
$ python manage.py createsuperuser
New DB will be created, along with migrations
dirs in app dirs, and 0001_initial.py
files there.
- use
showmigrations
to check which migrations were applied to DB:
$ manage.py showmigrations
Nginx
# sudo nano /etc/nginx/conf.d/virtual.conf
# sudo nginx -t
# sudo service nginx restart
# NOTE trailing '/'s in paths!!
server {
listen 80;
server_name 85.193.89.177;
location / {
proxy_pass http://127.0.0.1:8000/;
}
# STATIC_URL from settings.py
location /static/ {
autoindex on;
alias /home/hazadus/django-static/; # STATIC_ROOT from settings.py
}
location /media/ {
autoindex on;
alias /home/hazadus/projects/django-website/media/;
}
}
Nginx need + x to all dirs along the path to static, media dirs.
# chmod +x /root
# chmod +x /root/projects/
# chmod +x /root/projects/newspaper/
# chmod +x /root/projects/newspaper/media/
# chmod +x /root/projects/newspaper/media/images/
View nginx logs:
$ sudo tail -f /var/log/nginx/access.log
$ sudo tail -f /var/log/nginx/error.log
Collect static
# Remove all previous stuff
rm -rf /home/hazadus/django-static/*
# Collect new static
source .venv/bin/activate
python3 -m manage collectstatic
Copy files
scp ./db.sqlite3 hazadus@85.193.89.177:~/projects/django-c8-newspaper/
scp ./media/images/*.* hazadus@85.193.89.177:~/projects/django-c8-newspaper/media/images
Docker
Docker compose with Django 4, Celery, Redis and Postgres
Deploying Django application that is using Celery and Redis might be challenging.
https://saasitive.com/tutorial/django-celery-redis-postgres-docker-compose/?utm_campaign=Django%2BNewsletter&utm_medium=email&utm_source=Django_Newsletter_157
# docker-compose
# SERVER version
# note 'network: host' must be UNDER build!
# https://forums.docker.com/t/option-network-mode-host-in-docker-compose-file-not-working-as-expected/51682
version: "3.9"
services:
web:
build:
context: .
network: host
ports:
- "8000:8000"
command: python manage.py runserver 0.0.0.0:8000
# command: gunicorn django_project.wsgi -b 0.0.0.0:8000
volumes:
- .:/code
# By choosing network=host the container has access to the network of the host.
# https://github.com/docker/cli/issues/2707
sudo docker build . --network=host
# compose
sudo docker compose --file docker-compose.prod.yml build
sudo docker compose --file docker-compose.prod.yml up
sudo docker compose --file docker-compose.prod.yml up -d
# list running containers
sudo docker ps
# stop
sudo docker compose down
sudo docker compose build
sudo docker compose up -d --build
# New DB for Django:
sudo docker compose exec web python manage.py migrate
sudo docker compose exec web python manage.py createsuperuser
Back up and restore Postgres DB data:
# Creates 'backup.tar' with postgresql data in current directory
#
# Container id of Postgres (f0de8da471d2) got from 'sudo docker ps' when containers are up
# '/var/lib/postgresql/data/' is from 'docker-compose.yml', lines:
# volumes:
# - postgres_data:/var/lib/postgresql/data/
cd /Users/hazadus/backup
sudo docker run --rm --volumes-from c0e3a99224e3 -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /var/lib/postgresql/data/
# Change web to running container name, e.g. newspaper-web-1
sudo docker compose exec web python manage.py dumpdata --indent=2 --output=mysite_data.json
# Or, connect to it then run command:
docker exec -it newspaper-web-1 bash
python manage.py dumpdata --indent=2 --output=mysite_data.json
# Restore data:
sudo docker compose exec web python manage.py loaddata mysite_data.json
# Restoring postgresql data from backup tar
#
#
# ls -al
sudo docker run --rm -v django-c8-newspaper_postgres_data:/var/lib/postgresql/data/ -v $PWD:/backup-dir bash -c "cd /var/lib/postgresql/data/ && ls -al"
#
# untar - RUN FROM DIR WITH 'backup.tar' in it!!!
#
cd /Users/hazadus/backup
sudo docker run --rm -v django-c8-newspaper_postgres_data:/var/lib/postgresql/data/ -v $PWD:/backup-dir bash -c "tar xvf /backup-dir/backup.tar"
# Copy DB contents from CONTAINER (not volume!) to a folder:
# 3501a6e46de1 = containerID from `docker ps`
docker cp 3501a6e46de1:/var/lib/postgresql/data ~/PycharmProjects/polstagramm/data/postgres/
# enter CLI:
docker exec -it django-c8-newspaper-db-1 bash
# then run psql:
psql -U postgres
# then in psql
\dt - list all tables
python -m manage dumpdata users --format json -o users.json
Email – setup and test
docker compose exec web python manage.py shell
from django.core.mail import send_mail
send_mail('Django mail', 'This was sent with Django', 'hazadus7@gmail.com', ['hazadus7@gmail.com'], fail_silently=False)
Complete script – rebuild and run containers
#!/bin/bash
cd /home/hazadus/projects/django-c8-newspaper
source .venv/bin/activate
echo '[0] Update virtual environment...'
python3 -m pip install -r requirements.txt
echo '[1] Removing static files...'
rm -rf /home/hazadus/django-static/*
echo '[2] Collecting static files...'
python3 -m manage collectstatic
deactivate
echo '[3] Shutting down containers...'
sudo docker compose down
echo '[4] Build containers...'
sudo docker compose build
echo '[5] Run containers...'
sudo docker compose up -d
After collectstatic
copy files from project staticfiles dir to /home/hazadus/django-static/
Load testing
GitHub - rakyll/hey: HTTP load generator, ApacheBench (ab) replacement
hey is a tiny program that sends some load to a web application.
https://github.com/rakyll/hey
brew install hey
hey -n 1000 https://www.polstagramm.ru
Getting started - Locust 2.13.1 documentation
A Locust test is essentially a Python program.
https://docs.locust.io/en/stable/quickstart.html
- use Locust
Backup via rsync
# copy all the stuff directly to polsta-30.12/
sync -arv --exclude=.venv --exclude=.git --exclude=__pycache__ root@85.193.89.177:/root/projects/newspaper/ /Users/hazadus/webbackup/polsta-30.12/
📂 Django | Последнее изменение: 07.02.2024 15:04