Приветствую, друзья!

Уже давно у меня возникала мысль подробно, как в случае с FastAPI и Aiogram 3, разобрать «суровый» Django 5. Однако, из-за большого дефицита свободного времени и масштабности Django, руки до этого не доходили. Сегодня, как вы уже поняли, момент настал.

В этой статье будет минимум теории, и она пройдет в формате: ставим задачу на полноценный проект и вместе её решаем. В конце статьи будет голосование за продолжение данной темы. Поэтому, если вы захотите, чтобы я основательно раскрыл для вас тему Django 5, дайте знать через голосование, комментарий или подписку.

Из-за слабого отклика на некоторые предыдущие темы, такие как SQLAlchemy 2, Playwright и Docker, я их забросил. Посмотрим, как пойдет дело с Django 5.

План на сегодня

Обозначим задачу. К концу этой статьи мы не просто напишем наш первый проект на Django 5, но и развернем его на реальном хостинге, тем самым преодолевая основной страх разработчиков Django.

В блоке про деплой я покажу вам, как легко можно решить проблему со статическими файлами на продакшене, как подключить доменное имя и как за пару команд в консоли развернуть проект на хостинге.

Для деплоя я буду использовать сервис Amvera Cloud. Основная причина — простота деплоя. Нам достаточно будет:

  1. Зарегистрироваться в Amvera Cloud.

  2. Создать проект.

  3. Одним кликом активировать домен.

  4. Сгенерировать файл настроек прямо на их сайте.

  5. Через стандартные GIT-команды загрузить файлы Django-проекта на хостинг Amvera Cloud.

  6. Весь процесс деплоя займет не больше 5 минут, так что обязательно дочитайте статью до конца.

О самом проекте

Сегодня мы создадим простой сайт, который будет демонстрировать гороскоп на завтра для любого знака зодиака. Чтобы было интереснее, гороскоп будем асинхронно парсить в момент обращения к знаку зодиака (об этом подробнее далее).

Фронтенд, а именно два шаблона, мы попросим сгенерировать одну нейросеть, обзор которой я уже делал неоднократно в своем Хабре — websim.ai.

В процессе написания кода мы затронем следующие темы: маршрутизация, работа со статическими файлами, асинхронное использование Django 5, работа с базой данных SQLite через встроенное ORM и многое другое.

В результате даже абсолютный новичок поймет, как устроен Django и, как минимум, перестанет бояться этого фреймворка.

Дисклеймер

Сегодня я намеренно упрощу некоторые моменты, чтобы любой новичок мог разобраться в теме. Поэтому, если вы опытный разработчик на Django, вряд ли найдете здесь что-то новое.

Пожалуйста, давайте без негатива. Если у вас есть предложения по более простым практикам, сообщайте об этом в комментариях.

Немного теории

Django — это высокоуровневый веб-фреймворк на Python, предназначенный для быстрого и удобного создания веб-приложений. Он следует принципу "Don't Repeat Yourself" (DRY), что позволяет уменьшить количество повторяющегося кода.

Django предоставляет множество инструментов и библиотек "из коробки", таких как админ-панель, система маршрутизации, ORM для работы с базой данных, аутентификация и многое другое (большую часть этих инструментов мы рассмотрим сегодня).

Мы будем работать с 5-й версией Django. Как и в предыдущих версиях, основное внимание уделяется улучшению производительности и удобству разработки. То, что нас будет интересовать сегодня — это продвинутая асинхронность, которой мы и воспользуемся.

Больше теоретической информации вы найдете в интернете, а мы приступим к написанию кода.

Начинаем писать код

Для начала создадим новый проект под наше будущее приложение. Я, как обычно, буду работать в PyCharm.

В проекте будут использованы некоторые «нестандартные» для Django библиотеки. Для удобства установим их через файл requirements.txt. Вот его пример:

beautifulsoup4==4.12.3
bs4==0.0.2
django==5.1
fake-useragent==1.5.1
httpx==0.27.2
gunicorn==20.1.0
whitenoise==6.7.0

Библиотеки gunicorn и whitenoise нам понадобятся только на этапе деплоя. Поэтому их можно добавить в файл requirements.txt уже на стадии подготовки проекта к развертыванию.

Django — основной фреймворк, на котором мы будем писать код, а библиотеки bs4, fake-useragent и httpx понадобятся для парсинга гороскопа.

Установим библиотеки:

pip install -r requirements.txt

Теперь приступим к созданию проекта Django (не путать с приложением). Для этого воспользуемся командой:

django-admin startproject project_name

Я назову проект zodiac:

django-admin startproject zodiac

В результате у нас появится папка с именем zodiac и одноименный конфигурационный пакет zodiac. Давайте сразу переместим файл requirements.txt в корневую директорию проекта (папка zodiac). Это облегчит процесс деплоя в будущем, так как все нужные файлы будут находиться в одном месте.

Теперь создадим наше первое и единственное приложение.

Приложение в Django — это самостоятельный модуль, который выполняет определенную функцию в проекте. Например, это может быть блог, форум или корзина покупок. Приложения можно легко переиспользовать в других проектах, и каждый проект может состоять из одного или нескольких приложений.

Для создания приложения:

  • Переходим в папку проекта

cd zodiac
  • Выполняем команду (имя приложения может быть любым)

python manage.py startapp zodiac_app

Теперь нам необходимо зарегистрировать созданное приложение. Для этого в конфигурационном пакете (zodiac в моем случае) необходимо найти файл settings.py.

Там находим список INSTALLED_APPS и добавляем в него имя приложения:

INSTALLED_APPS = [
    …,
    'zodiac_app',
]

Теперь нам необходимо подготовить маршруты для созданного приложения. Маршрутов у нас будет всего 2:

  1. Главная страница

  2. Страница с гороскопом (будет динамически подставляться путь)

Для этого в пакете созданного приложения (zodiac_app) создадим файл urls.py и заполним его следующим образом:

from django.urls import path
from . import views


urlpatterns = [
    path('', views.index, name='home'),
]

Тут нужно немного разобраться.

В данном файле я определил маршрут для главной страницы нашего сайта. Данная запись указывает, что есть некоторое представление (функция) в файле views, которое будет отвечать за обработку главной страницы.

Опишем это представление в файле views.py:

from django.shortcuts import render


def index(request):
    return render(request, 
                  template_name='index.html', 
                  context={'title': 'Космический гороскоп - все знаки зодиака'})

Данный код подразумевает, что у нас есть файл index.html, который необходимо будет загрузить при вызове главной страницы сайта (это мы описали на прошлом шаге, когда привязали данное представление к корню сайта).

Параметр context может принимать словарь с данными. В него мы пока просто передадим название страницы (это нужно будет для тестов).

Создадим максимально простой HTML.

Для хранения html шаблонов (напомню, у нас их всего 2 будет) необходимо создать папку templates в корне приложения (у меня это zodiac_app) и во внутрь положим файл index.html.

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{ title }}</title>
</head>
<body>
<h1>{{ title }}</h1>
</body>
</html>

Стандартный HTML. В него, чтоб проверить что переменные контекста корректно передаются я передал переменную title (для этого используются двойные фигурные скобки).

Теперь, перед первым запуском нашего приложения, нам необходимо будет включить страницы приложения. Для этого в конфиг-пакете (у меня это zodiac) находим файл urls.py и добавляем там следующее.

from django.urls import path, include


urlpatterns = [
    path('', include('zodiac_app.urls'))
]

Админку можно убрать, так как ее мы не будем использовать в данном проекте.

Благодаря такой записи мы включили маршруты из нашего приложения zodiac_app.

Теперь мы можем запустить сервер Django. Для этого нужно воспользоваться командой:

python manage.py runserver PORT

У меня свободен порт 5000, а значит команда запуска будет выглядеть так:

python manage.py runserver 5000

Перейдем на сервер

Посмотрим на исходный код страницы (CTRL + U)

Видим, что наш шаблон отработал корректно. Продолжим.

Теперь немного отойдем от самого Django и выполним небольшую подготовку, а именно, напишем парсер гороскопа.

Для написания создадим файл utils.py в пакете приложения (zodiac_app).

Логика парсера

Данные мы будем брать с сайта https://horo.mail.ru/prediction. Там, если переходить на страницу с гороскопом, то, по умолчанию, открывается страница следующей конструкции: https://horo.mail.ru/prediction/HOROSCOP_NAME_EN/today/. К этому мы и подстроим свой парсер.

То есть, мы будем передавать название знака зодиака на английском и будем вытягивать с этого сайта гороскоп на завтра. Для этого мы будем использовать httpx для асинхронной отправки запроса к сайту с гороскопом и bs4 для извлечения текста с гороскопом.

Полный код парсера

import httpx
from bs4 import BeautifulSoup
from fake_useragent import UserAgent


async def fetch_horoscope(zodiac_en="cancer"):
    # Создаем фейковый User-Agent
    ua = UserAgent()
    headers = {
        'accept': '*/*',
        'accept-language': 'ru,en;q=0.9',
        'cache-control': 'no-cache',
        'pragma': 'no-cache',
        'priority': 'u=0, i',
        'referer': 'https://horo.mail.ru/prediction/',
        'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "YaBrowser";v="24.7", "Yowser";v="2.5"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
        'sec-fetch-dest': 'document',
        'sec-fetch-mode': 'navigate',
        'sec-fetch-site': 'same-origin',
        'sec-fetch-user': '?1',
        'upgrade-insecure-requests': '1',
        'user-agent': ua.random,  # Используем фейковый User-Agent
    }

    # Асинхронный HTTP-запрос
    async with httpx.AsyncClient() as client:
        response = await client.get(f'https://horo.mail.ru/prediction/{zodiac_en}/today/', headers=headers)

    # Парсинг ответа с помощью BeautifulSoup
    soup = BeautifulSoup(response.text, 'html.parser')
    main_content = soup.find('main', itemprop='articleBody')

    # Извлечение текста из найденного элемента, с разделением на строки
    text_lines = main_content.get_text(separator="\n", strip=True).splitlines()

    # Формирование новых <p> для каждой строки текста
    paragraphs = ''.join([f'<p>{line}</p>' for line in text_lines])
    return paragraphs

Для того чтоб сильно на этом не заострять внимание оставил комментарии в коде.

На что стоит обратить внимание — это подставка фейкового User Agent в заголовки. Далее все просто:

  • подставили название на английском гороскопа

  • отправили запрос на получение данных

  • через bs4 вытянули из полученных данных описание гороскопа

  • очистили от лишних классов и вернули html

Проверим код на знаке "Весы"

print(asyncio.run(fetch_horoscope(zodiac_en="aries")))
 Получаем нужный результат. Отлично!
Получаем нужный результат. Отлично!

Теперь нам нужно подготовить красивые HTML шаблоны для нашего сайта. Воспользуемся нейросетью websim.ai

Далее я буду отправлять скрины и под ними комментарии промта, благодаря которому я получил тот или иной результат.

Страница гороскопа на русском языке. На ней размещена иконка гороскопа (фото), под ней — название на русском и дата рождения, соответствующая этому знаку. Также есть кнопка «Смотреть гороскоп». Сделай стильно и красиво, но без излишних усложнений.
Страница гороскопа на русском языке. На ней размещена иконка гороскопа (фото), под ней — название на русском и дата рождения, соответствующая этому знаку. Также есть кнопка «Смотреть гороскоп». Сделай стильно и красиво, но без излишних усложнений.

 теперь оформи все 12 знаков на одной странице
теперь оформи все 12 знаков на одной странице
 теперь добавь более красивые иконки под каждый гороскоп и выполни все в космическом стиле
теперь добавь более красивые иконки под каждый гороскоп и выполни все в космическом стиле

Фон получился динамическим, при наведении увеличивается кнопка. Лично я остался полностью доволен главной страницей. Теперь сохраним получившийся HTML.

Теперь, в один промт, попросим подготовить для нас страничку с гороскопом.

 оформи в этом же стиле страницу внутри гороскопа. иконка, описание и кнопка "На главную". Обязательно сохрани общий стиль
оформи в этом же стиле страницу внутри гороскопа. иконка, описание и кнопка "На главную". Обязательно сохрани общий стиль

И этой страницей я полностью доволен и теперь остается только ее сохранить.

Работа с базой данных

К шаблонам мы вернемся чуть позже, а сейчас подготовим простую базу данных под наш проект. В этой базе данных мы создадим одну таблицу и будем хранить в ней следующие поля: русское название зодиака, английское название зодиака, день когда начинается знак, день когда заканчивается знак и svg (нейронка сгенерировала все иконки чистым svg, так что в этом поле будем хранить HTML).

Для работы с базой данных в Django используется объектно-ориентированный подход. Если простыми словами, то создается класс таблицы и далее работа идет с каждым полем как с отдельным объектом класса.

Для работы нам необходимо создать модель таблицы. Файл для моделей уже создан в нашем приложении (zodiac_app) — models.py. Опишем модель.

from django.db import models


class ZodiacSign(models.Model):
    zodiac_ru = models.CharField(max_length=50, unique=True)  # Название знака зодиака на русском
    zodiac_en = models.CharField(max_length=50, unique=True)  # Название знака зодиака на английском
    start_day = models.CharField(max_length=20, unique=True)  # Дата начала периода
    end_day = models.CharField(max_length=20, unique=True)  # Дата окончания периода
    svg = models.TextField()  # SVG-код

    def __str__(self):
        return self.zodiac_en

Описание очень похоже на работу с SQLAchemy. В комментариях дал описание на русском каждого поля.

Теперь нам необходимо создать миграцию на основе нашей модели.

Миграции — это способ управления изменениями в структуре базы данных. После того как вы описали модель, Django должен создать миграции, чтобы применить эти изменения в базе данных.

Выполним команду:

python manage.py makemigrations

Эта команда создаст файлы миграций в директории migrations вашего приложения.

После создания миграций нужно применить их к базе данных. Это создаст соответствующие таблицы и поля в базе данных.

Выполните команду:

python manage.py migrate

Эта команда применит все созданные миграции и обновит структуру базы данных.

При первой миграции будет создано множество служебных таблиц, которые нас сегодня интересовать не будут.

Если у вас была описана всего одна модель, то будет создана одна таблица. Напоминаю, что по умолчанию Django ориентирован на SQLite.

Теперь нам необходимо заполнить таблицу данными. Для того, чтоб упростить себе жизнь, данные я взял с сайта с гороскопами, а иконки от тех что сгенерировала нейронка.

Название на русском, день начала, день завершения и название на английском (со ссылки)
Название на русском, день начала, день завершения и название на английском (со ссылки)

На выходе у меня получился такой массив данных:

zodiac_data = [
    {
        'zodiac_ru': 'овен',
        'zodiac_en': 'aries',
        'start_day': '20 марта',
        'end_day': '19 апреля',
        'svg': '''<svg class="horoscope-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
            <circle cx="50" cy="50" r="45" fill="url(#ariesGradient)"/>
            <path d="M50 20 C60 40 70 40 70 60 M50 20 C40 40 30 40 30 60" stroke="#FFD700" stroke-width="4"
                  fill="none"/>
            <defs>
                <radialGradient id="ariesGradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
                    <stop offset="0%" style="stop-color:#FF6B6B;stop-opacity:1"/>
                    <stop offset="100%" style="stop-color:#FF4500;stop-opacity:1"/>
                </radialGradient>
            </defs>
        </svg>'''
    }, …]

Кому будет нужно — с полным кодом всего проекта можно ознакомиться в моем телеграмм-канале «Легкий путь в Python».

Далее, для того чтоб не вставлять это все дело руками я подготовил простую функцию прямо в файле с моделью таблицы.

from django.db import models


zodiac_data = [...]


class ZodiacSign(models.Model):
    zodiac_ru = models.CharField(max_length=50, unique=True)  # Название знака зодиака на русском
    zodiac_en = models.CharField(max_length=50, unique=True)  # Название знака зодиака на английском
    start_day = models.CharField(max_length=20, unique=True)  # Дата начала периода
    end_day = models.CharField(max_length=20, unique=True)  # Дата окончания периода
    svg = models.TextField()  # SVG-код

    def __str__(self):
        return self.zodiac_en


def bulk_create_zodiac_signs():
    # Создаем список объектов модели
    zodiac_signs = [
        ZodiacSign(
            zodiac_ru=data['zodiac_ru'],
            zodiac_en=data['zodiac_en'],
            start_day=data['start_day'],
            end_day=data['end_day'],
            svg=data['svg']
        )
        for data in zodiac_data
    ]

    # Используйте bulk_create для добавления всех записей
    ZodiacSign.objects.bulk_create(zodiac_signs)

Данные для добавления в таблицу я подготовил там-же.

Теперь войдем в оболочку Django и с нее выполним функцию bulk_create_zodiac_signs. Для этого:

  • Вводим команду:

    python manage.py shell

  • Импортируем функцию bulk_create_zodiac_signs

    from zodiac_app.models import bulk_create_zodiac_signs

  • Выполняем функцию

    bulk_create_zodiac_signs()

  • Выходим с оболочки:

    exit()

Проверим таблицу на наличие данных. Для этого я воспользуюсь функционалом PycharmProfessional.

Все данные на месте
Все данные на месте

Все данные на месте, парсер написан и это значит что мы можем приступать к оформлению HTML-шаблонов.

Оформление HTML-шаблонов

Конечно, мы все сделаем по-красоте. Для этого, для начала, в папке приложения (zodiac_app) подготовим папку templates и внутри нее следующие папки: img (для фото), js (для JavaScript скриптов) и style (для CSS-стилей).

Теперь нужно адаптировать под наш проект файл для главной страницы, который нам подготовила нейросеть.

Для начала, я создал файл стилей main.css и в него перенес все стили с блока стилей, который подготовила нейросеть. Так как стилей много — просто приведу в пример скриншот (где искать исходники проекта вы знаете).

Далее, на главной странице, как вы могли заметить, представлены знаки зодиака плиткой, а это хорошая предпосылка для нас, так как мы сможем на основании каждой такой карточки оформить внутренний цикл FOR и просто переберем все значения с базы данных в HTML-шаблоне.

Сама карточка от нейронки имеет такой вид:

<div class="horoscope-card">
        <svg class="horoscope-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
            <circle cx="50" cy="50" r="45" fill="url(#geminiGradient)"/>
            <path d="M30 30 L70 70 M30 70 L70 30" stroke="#FFD700" stroke-width="4" fill="none"/>
            <defs>
                <radialGradient id="geminiGradient" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
                    <stop offset="0%" style="stop-color:#FFFF00;stop-opacity:1" />
                    <stop offset="100%" style="stop-color:#FFA500;stop-opacity:1" />
                </radialGradient>
            </defs>
        </svg>
        <h2>Близнецы</h2>
        <p>21 мая - 20 июня</p>
        <a href="ru/sign/gemini" class="btn">Смотреть гороскоп</a>
    </div>

И в таком формате 12 раз. Адаптируем код под наш проект:

{% load static %}
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ title }}</title>
    <!-- Устанавливает иконку сайта, загружаемую из статических файлов -->
    <link rel="icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}">
    <!-- импорт стилей из static/style/main.css -->
    <link type="text/css" href="{% static 'style/main.css' %}" rel="stylesheet"/>
</head>
<body>
<div class="horoscope-grid">
    {% for info in main_data %}
    <div class="horoscope-card">
        {{ info.svg|safe }}
        <h2>{{ info.zodiac_ru|title }}</h2>
        <p>{{ info.start_day }} - {{ info.end_day }}</p>
        <a href="/{{ info.zodiac_en }}" class="btn">Смотреть гороскоп</a>
    </div>
    {% endfor %}
</div>
</body>
</html>

Это весь код главной страницы.

Тут стоит обратить внимание на начало файла. Я использовал {% load static %}. Благодаря этому мы указали Django что в этот файл необходимо подключить статические файлы (например, CSS, изображения).

Кроме того, я заранее подготовил иконку (фавикон) и закинул ее в папку img. Затем я импортировал файл стилей.

Далее, из интересного, это основной цикл. В данном случае я использовал внутренний цикл FOR из шаблонизатора Django. В качестве перебора я воспользовался списком main_data (этот список мы получим с базы данных совсем скоро и передадим его в шаблон).

<a href="/{{ info.zodiac_en }}" class="btn">Смотреть гороскоп</a>

Благодаря этой строке, для страницы с гороскопом будет сформирована ссылка такого вида: http://127.0.0.1:5000/aries. Это мы используем в дальнейшем, когда опишем страницу каждого гороскопа.

Теперь изменим функцию-представления для главной страницы.

Для начала импортируем модель нашей таблицы в файл views.py

from .models import ZodiacSign

Теперь в представление index добавим обращение к базе данных с целью получения всех записей. С DjangoORM это делать очень просто.

def index(request):
    main_data = ZodiacSign.objects.all()
    return render(request,
                  template_name='index.html',
                  context={'title': 'Космический гороскоп - все знаки зодиака', 'main_data': main_data})

Всего одной строкой: main_data = ZodiacSign.objects.all() нам удалось извлечь все записи с нашей созданной таблицы. Каждая из этих записей теперь представлена списком питоновских словарей, что позволит нам обращаться к каждому значению через точку в шаблоне.

Перезапустим сервер и посмотрим что получилось с главной.

Получилось то что я планировал. Все записи выстроились корректно, стили подгрузились.

Теперь остается настроить представление для страницы с гороскопом.

Для начала поработаем с маршрутом (файл zodiac_app/urls.py).

urlpatterns = [
    path('', views.index, name='home'),
    path('<slug:zodiac_name>/', views.zodiac, name='zodiac'),
]

Тут мы добавили новый маршрут для представления zodiac.

  • <slug:zodiac_name> — это динамическая часть URL, которая может принимать различные значения. Django распознает этот параметр и передает его в соответствующее представление в качестве аргумента.

  • slug — это конвертер, который ограничивает формат значения переменной zodiac_name. Он ожидает строку, состоящую из букв, цифр, дефисов и подчеркиваний (например, aries, leo, virgo, scorpio-2023). Slug используется для создания "чистых" URL, удобных для чтения и SEO.

Перед тем как мы начнем описывать представления хочу обратить ваше внимание на один момент. Наш парсер асинхронный, а обращение к базе данных синхронное. Django не может в одном обработчике использовать и синхронный и асинхронный код, поэтому, сейчас мы воспользуемся некоторыми трюками.

Для начала мы импортируем метод sync_to_async из asgiref.sync (ветка у вас уже установлена). Данный метод позволит превратить синхронный код в асинхронный, что уже следует из названия.

Теперь напишем функцию, которая будет возвращать с базы данных информацию по конкретному знаку зодиака по его английскому названию.

from asgiref.sync import sync_to_async


@sync_to_async
def get_zodiac_data(zodiac_name):
    return ZodiacSign.objects.get(zodiac_en=zodiac_name)

На этом примере вы, в очередной раз, можете убедиться насколько удобное и функциональное ORM Django.

И опишем само представление.

async def zodiac(request, zodiac_name):
    zodiac_data = await get_zodiac_data(zodiac_name)
    html_description = await fetch_horoscope(zodiac_name)
    return render(request, 'zodiac_page.html',
                  {'title': zodiac_name, 'zodiac_data': zodiac_data, 'html_description': html_description})

Теперь у нас полностью асинхронное представление. Работает код по следующей логике:

  1. Получаем данные по знаку зодиака с базы данных

  2. Получаем описание гороскопа на завтра с сайта с гороскопом (не забудьте предварительно выполнить импорт парсера из утилит from .utils import fetch_horoscope

  3. Далее выполняем знакомый рендер (html шаблон у нас уже есть и его нужно только адаптировать).

Теперь адаптируем сгенерированный нейронкой шаблон с конкретным гороскопом под наш проект. Там логика такая-же как в первом случае, поэтому далее приведу готовый вариант.

{% load static %}
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ваш космический гороскоп для {{ zodiac_data.zodiac_ru|title }}</title>
    <link rel="icon" type="image/x-icon" href="{% static 'img/favicon.ico' %}">
    <link type="text/css" href="{% static 'style/horoscop.css' %}" rel="stylesheet"/>
</head>
<body>
<div class="horoscope-container">
    {{ zodiac_data.svg|safe }}
    <h1>{{ zodiac_data.zodiac_ru|title }}</h1>
    {{ html_description|safe}}
    <a href="/" class="btn">На главную</a>
</div>

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

Из нового только импорт js-скрипта и использование safe для корректного отображения HTML.

И, прежде чем запустим, давайте асинхронно перепишем и представление index, чтоб наш проект был максимально прогрессивным.

@sync_to_async
def get_all_zodiac_signs():
    return list(ZodiacSign.objects.all())


async def index(request):
    main_data = await get_all_zodiac_signs()
    return render(request, 'index.html', {'main_data': main_data})

Теперь перезапустим сервер и посмотрим на результат.

Сверим результат с тем что на сайте-доноре.

Видим что результат одинаков, а это значит что проект готов!

Подготовка к деплою

Сегодня я продемонстрирую один из самых простых способов деплоя Django приложения, используя отечественный аналог Heroku — Amvera Cloud. Перед этим подготовим наш проект.

Сначала настроим файл settings.py, где мы ранее регистрировали наше приложение.

Переведём проект из режима разработки:

DEBUG = False

Настроим разрешения доступа к сайту. Так как проект учебный, разрешим доступ со всех адресов:

ALLOWED_HOSTS = ['*']

Чтобы упростить работу со статическими файлами, добавим в список middleware следующую строку:

MIDDLEWARE = [
    …,
    'whitenoise.middleware.WhiteNoiseMiddleware',
]

WhiteNoise позволяет эффективно управлять статическими файлами, что особенно полезно, когда нет возможности использовать полноценный веб-сервер. Эта настройка избавляет от необходимости дополнительной конфигурации Nginx или Apache для обработки статики.

Затем укажем путь для хранения статических файлов в продакшене:

STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'static'

STATIC_URL = '/static/' определяет базовый URL для доступа к статическим файлам, а STATIC_ROOT = BASE_DIR / 'static' — директорию на сервере, куда будут собираться все статические файлы при выполнении команды collectstatic.

Деплой приложения

Теперь мы полностью готовы к деплою.

  • Зарегистрируйтесь на сайте Amvera Cloud, если еще не зарегистрированы (за регистрацию предусмотрен бонус в размере 111 рублей на основной счет).

  • Перейдите в раздел проектов.

  • Нажмите на «Создать проект», задайте ему название и выберите тариф.

  • Следующий экран пропустите — к нему мы вернемся позже.

  • На открывшемся экране появится возможность сгенерировать файл настроек. Заполните данные, как указано на скриншоте ниже (при необходимости их можно будет изменить позже).

  • Нажмите на «Завершить».

  • Войдите в проект, откройте вкладку «Настройки» и активируйте бесплатный домен (если у вас есть свой домен, его можно привязать на этом же экране).

  • Перейдите на вкладку «Репозиторий». Здесь вас будет интересовать ссылка на репозиторий. Скопируйте её.

На этом все настройки на стороне Amvera Cloud завершены. Теперь осталось с помощью GIT доставить файлы нашего Django проекта в проект на Amvera, после чего проект автоматически развернется и запустится (магия).

Перейдите в корень проекта (папка zodiac) и инициализируйте пустой Git-репозиторий:

git init

Привяжите проект к Amvera. В моем случае команда выглядит так:

git remote add amvera https://git.amvera.ru/yakvenalex/djangozodiac

Заберите файл настроек:

git pull amvera master

В моем случае файл имеет такой вид:

---
meta:
  environment: python
  toolchain:
    name: pip
    version: 3.12
build:
  requirementsPath: requirements.txt
run:
  persistenceMount: /data
  containerPort: 5000
  command: gunicorn zodiac.wsgi:application  --bind 0.0.0.0:5000

Внимательно проверьте, чтоб значения портов совпадали и в контейнере и в команде. На выходе у вас должна получиться такая структура файлов:

Для запуска важно наличие всех этих файлов именно в такой иерархии. Иначе запуск не произойдет.

Теперь доставим файлы:

git add .
git commit -m "init commit"
git pull amvera master

Если все прошло корректно, то после обновления, на вкладке «Репозиторий» вы должны увидеть файлы своего проекта.

После автоматически начнется сборка проекта и, если все было сделано правильно, то минут через 5 вы увидите следующее.

Теперь остается только перейти по доменному имени проекта и проверить все ли работает. В моем случае это: https://djangozodiac-yakvenalex.amvera.io/

Главный экран
Главный экран
Страница с гороскопом
Страница с гороскопом

Заключение

Друзья, конечно, в идеале этот проект можно было бы расширить, добавив больше функционала, таких как фоновые задачи, кэширование с использованием Redis и многое другое. Однако, поскольку это всего лишь учебный проект, считаю, что на данном этапе это достаточно.

Этой публикацией я хотел показать, что Django — это не какой-то убер-сложный инструмент, доступный лишь избранным. Главное — преодолеть первоначальные барьер входа, и вы сможете понять, оценить и полюбить философию этого фреймворка.

Полный исходный код проекта, а также дополнительный эксклюзивный контент, который я не планирую публиковать на Хабре, вы сможете найти в моем телеграм-канале «Легкий путь в Python».

Напоминаю, что если вам интересно получить более подробное и глубокое погружение в фреймворк Django в моем авторстве, участвуйте в голосовании под этой статьей, ставьте лайки и оставляйте комментарии. Это займет у вас всего пару секунд, а мне будет очень приятно.

Спасибо за внимание и до скорого!

Комментарии (12)


  1. rSedoy
    03.09.2024 14:25
    +4

    @sync_to_async
    def get_zodiac_data(zodiac_name):
        return ZodiacSign.objects.get(zodiac_en=zodiac_name)

    а почему не сразу так?

    async def get_zodiac_data(zodiac_name):
        return await ZodiacSign.objects.aget(zodiac_en=zodiac_name)


    1. yakvenalex Автор
      03.09.2024 14:25
      +1

      А этот метод разве отрабатывает на стандартном драйвере? Тут пытался не усложнять где это можно) но за замечание спасибо


  1. NeroSay
    03.09.2024 14:25
    +1

    А насколько вообще легально валидно хранить svg в БД?)


    1. yakvenalex Автор
      03.09.2024 14:25
      +1

      Ну если сам добавляешь то все ок. Если кто-то другой лучше так не делать)


    1. beezy92
      03.09.2024 14:25

      Есть такая рекомендация, не хранить файлы в БД. Но, никто не отменял логику. Если у вас внутренняя система, которой пользуются 100 пользователей, это ОК хранить файлы в БД, чем разворачивать S3 хранилище.

      Если не ошибаюсь, первая версия S3 хранилища Яндекса была реализовано поверх Postgres.


      1. yakvenalex Автор
        03.09.2024 14:25
        +1

        Так это не файлы, а простой HTML-код. Понятное дело, что в боевом проекте это были бы ссылки на SVG, но в целом, какая разница хранить HTML-текст (описание для блока) и в виде HTML-мелкие иконки? Да и база данных тут SQLite) Думаю что для такого проекта точно пойдет)


        1. NeroSay
          03.09.2024 14:25
          +1

          Я понимаю, что можно так сделать и тренеровочного проекта почему бы и нет. Но по логике это статик файл, а значит и в БД ему не место. Хотелось бы узнать насколько вообще распространена такая практика в проде.)

          А в целом, статья мне понравилась. Хотелось бы ещё узнать побольше о паттернах, которые часто используются в Джанго. А так же о работе с файлами.


          1. danilovmy
            03.09.2024 14:25

            лучшее, что я читал написанное именно в django стилистике - Django Design Patterns and Best Practices: Ravindran, Arun. Популярная же Two scoops намного менее Django-way книжка. Мне даже с автором удалось пообщаться лично в этом году, но это не делает ее лучше.

            SVG это статик файл, пока ты его не меняешь динамически. Но как только ты сохранишь SVG как шаблон(template) с разметкой, куда ты вставляешь динамические данные и, вуаля, уже другая задача. Правда шаблоны тоже стоит хранить статикой. :)

            Поскольку SVG это векторный формат, то я храню данные для объектов (class Flowable) которые рендерю через reportlab в SVG.

            Со стат файлами надо работать через веб сервер. Да и хранить поближе к клиенту на CDN. Этого в статье нет. Но это стоит знать и использовать.

            А по поводу практик - в проде чего только не бывает. Например код python хранят в базе и запускают по запросу через eval/exec. Или в Yaml только настройки, а потом стартуют автогенерацию кода согласно настройкам. Страшные вещи...


  1. danilovmy
    03.09.2024 14:25
    +6

    Странно, что в статье про Django 5 очень поверхностно используется само Django. Вероятно в этом причина непопулярности и предыдущих статей.

    Что автору можно улучшить, чтобы он сам смог понять, оценить и полюбить философию этого фреймворка.

    1. Про objects.aget уже написали. Стоит научиться пользоваться. doc

    2. Так же полезно почитать про фикстуры вместо создания самопальной функции заполнения базы данными. doc

    3. Стоит так же научиться запускать функции проекта через менеджмент-команды, а не через shell. doc

    4. Стоит все же научиться использовать async GCBV вместо FBV. doc

    5. Учимся всегда включать объявление doctype в начало HTML. Doc, например, тут.

    6. Для асинхронного проекта с forloop в шаблоне идем читать про рендер шаблона по частям через StreamingHttpResponse. doc

    7. Все что стоит в href заворачиваем фильтром |slugify, просто потому что. doc

    8. Вероятно, что через некоторое время захочется выбросить gunicorn и начать запускать через uvicorn. Предлагаю сделать это с самого начала. doc

    Ну и напоследок, @yakvenalex, стоит научиться уважать читателей и размещать ссылки на репозиторий напрямую, а не через телеграм.


    1. yakvenalex Автор
      03.09.2024 14:25

      Странно, что в статье про Django 5 очень поверхностно используется само Django. Вероятно в этом причина непопулярности и предыдущих статей.

      @danilovmy причина непопулярности прошлых публикаций в том, что очень поверхностно используется само Django? У меня это вообще первая статья по данному фреймворку.

      Или вы о том, что я всегда поверхностно разбор делаю? Если так, то тоже не понятно. У меня есть циклы по 10-13 статей, в которых я рассматриваю просто конкретные фреймворки (aiogram и fastapi например). Кроме того, есть чисто практические статьи. Которые, обычно, подразумевают изучение не практических. Да и вопрос популярности и нет относителен. Есть статьи, которые набирают более 10к просмотров, есть и те которые за 25к и за 100к.

      По поводу улучшений. Тут согласен, и в планах было это все раскрыть подробнее в следующих публикациях, видать, не судьба) Логика в том, чтоб изначально показать, как писать быстро и просто, а далее уже углубляться. За ссылки на доки спасибо.

      К примеру, зачем новичку, который только знакомится с фреймворком, сразу погружаться в такие темы, как «рендер по частям», фикстуры и прочее. Вам самим не кажется, что это вот так, на старте, сможет просто отвернуть от фреймворка?

      В статье был сделан акцент, что целевая аудитория текста — новички. Хотел показать, что можно писать без особого погружения в тему и делать это быстро, френдли)

      По поводу ссылки в канале. Не считаю, что размещение ссылки на свой репозиторий в своем канале — это неуважение к читателям. Считаю, что я, как автор этого кода, имею право размещать его там, где посчитаю нужным. Я же не на ваш, например, репозиторий у себя в канале ссылку размещаю)

      И хочу отметить, что канал бесплатный и не обязательно даже подписываться на него, чтоб забрать ссылку)


      1. MSigillite
        03.09.2024 14:25
        +1

        В статье был сделан акцент, что целевая аудитория текста — новички. Хотел показать, что можно писать без особого погружения в тему и делать это быстро, френдли)

        Чем плохо с упрощениями - есть 100500 статей "для новичков" и практически ничего нет на тему "пишем код для продакшена". Уж лучше сразу писать "как надо", а если хочется упростить, то хотя бы оставлять ссылки на материал/документацию как люди пишут для продакшена.


        1. yakvenalex Автор
          03.09.2024 14:25

          Возможно вы и правы. Исправлюсь)