Форум программистов, компьютерный форум, киберфорум
py-thonny
Войти
Регистрация
Восстановить пароль

Введение в Django: Создаём приложение портфолио

Запись от py-thonny размещена 16.04.2025 в 18:21
Показов 2796 Комментарии 0
Метки django, python

Нажмите на изображение для увеличения
Название: 6e6ffca0-424c-4765-af60-0476022561d0.jpg
Просмотров: 56
Размер:	123.2 Кб
ID:	10602
Django – один из самых мощных веб-фреймворков на Python, который позволяет быстро создавать сложные веб-приложения. В отличие от других фреймворков, Django предоставляет богатый набор встроенных инструментов – от панели администратора до системы аутентификации. При разработке на Django программисту не приходится изобретать велосипед. Фреймворк построен по принципу "батарейки в комплекте" и включает всё необходимое для создания полноценного веб-приложения. Это существенно ускоряет процесс разработки и позволяет сфокусироваться на бизнес-логике.

Архитектура Django основана на паттерне MTV (Model-Template-View), который является адаптацией классического MVC (Model-View-Controller). В Django модели определяют структуру данных, представления обрабатывают логику, а шаблоны отвечают за отображение. Контроллер в классическом понимании MVC здесь встроен в сам фреймворк.

Одно из главных преимуществ Django – его ORM (Object-Relational Mapping). Она позволяет работать с базой данных через Python-классы, не погружаясь в SQL. ORM автоматически создаёт и обновляет схему базы данных на основе Python-моделей, что значительно упрощает процесс разработки. Django поддерживает множество баз данных: PostgreSQL, MySQL, SQLite и Oracle. При этом смена базы данных не требует изменения кода приложения – достаточно обновить настройки подключения. Фреймворк имеет продвинутую систему миграций, которая отслеживает изменения в моделях и автоматически синхронизирует их с базой данных. Это особенно полезно при командной разработке, когда несколько разработчиков вносят изменения в структуру данных.

Система шаблонов Django (DTL) позволяет создавать динамические HTML-страницы с минимальными усилиями. DTL поддерживает наследование шаблонов, что помогает избежать дублирования кода и поддерживать единый стиль оформления сайта. Встроенная админ-панель Django – настоящая находка для разработчиков. Она автоматически создаёт интерфейс для управления данными на основе моделей. Админку можно настраивать под конкретные нужды проекта, добавляя свои фильтры, действия и формы.

Django серьёзно относится к безопасности. Фреймворк защищает от основных веб-уязвимостей: SQL-инъекций, межсайтового скриптинга (XSS), подделки межсайтовых запросов (CSRF) и других атак. Защита работает "из коробки" и не требует дополнительной настройки.

Для работы с Django нужно знать Python на среднем уровне и понимать основы веб-разработки. Знание HTML и CSS пригодится при создании шаблонов, но не является обязательным на начальном этапе. JavaScript можно изучать параллельно, добавляя интерактивность по мере необходимости. Экосистема Django включает множество дополнительных пакетов. Django REST framework упрощает создание API, Django Debug Toolbar помогает в отладке, а Django Channels добавляет поддержку WebSocket. Эти инструменты расширяют базовую функциональность фреймворка и решают специфические задачи.

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

Установка Python и зависимости



Перед началом работы над портфолио необходимо настроить рабочее окружение. Python – интерпретируемый язык, поэтому его установка не требует сложных манипуляций. На Windows достаточно скачать установщик с официального сайта, а в Linux и macOS Python обычно предустановлен.

После установки Python стоит проверить его версию в командной строке:

Bash
1
python --version
Для Django рекомендуется использовать Python версии 3.8 или выше. Это обеспечивает доступ к современным возможностям языка и гарантирует совместимость со всеми компонентами фреймворка.
Виртуальное окружение – неотъемлемая часть разработки на Python. Оно изолирует зависимости проекта от системного Python и других проектов. Создать виртуальное окружение можно командой:

Bash
1
python -m venv venv
Активация окружения различается в зависимости от операционной системы. В Windows используется команда:

Bash
1
venv\Scripts\activate
В Linux и macOS:

Bash
1
source venv/bin/activate
После активации в начале строки появится префикс (venv), что указывает на работу в виртуальном окружении. Теперь можно установить Django и другие необходимые пакеты через pip – стандартный менеджер пакетов Python:

Bash
1
2
pip install django
pip install pillow  # для работы с изображениями
Версии установленных пакетов стоит зафиксировать в файле requirements.txt:

Bash
1
pip freeze > requirements.txt
Этот файл пригодится при развёртывании проекта на другом компьютере или сервере. Восстановить зависимости из него можно командой:

Bash
1
pip install -r requirements.txt
Для удобной разработки Django-приложений полезно настроить IDE. PyCharm Professional и VS Code с расширением Python предоставляют расширенную поддержку Django: автодополнение кода, навигацию по шаблонам, отладку и многое другое.

Система контроля версий Git поможет отслеживать изменения в коде. Создадим репозиторий и добавим базовые файлы:

Bash
1
2
3
4
git init
echo "venv/" > .gitignore
echo "*.pyc" >> .gitignore
echo "__pycache__/" >> .gitignore
В .gitignore указаны файлы и директории, которые не нужно отслеживать: виртуальное окружение, кэш Python и скомпилированные файлы.
Создание Django-проекта выполняется одной командой:

Bash
1
django-admin startproject portfolio .
Точка в конце команды указывает, что проект нужно создать в текущей директории, без дополнительной вложенности.

После выполнения команды появится базовая структура проекта:
manage.py – скрипт для управления проектом,
portfolio/ – основная директория с настройками,
- settings.py – конфигурация проекта,
- urls.py – корневые URL-маршруты,
- wsgi.py и asgi.py – точки входа для веб-серверов.

Запустить отладочный сервер Django можно командой:

Bash
1
python manage.py runserver
По умолчанию сервер запускается на localhost:8000. При первом запуске Django предупредит о неприменённых миграциях – это нормально, миграции будут выполнены позже.

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

Python
1
2
3
4
5
6
7
8
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]
Список INSTALLED_APPS содержит встроенные приложения Django. Позже сюда добавятся наши собственные приложения.
Настройки базы данных по умолчанию используют SQLite:

Python
1
2
3
4
5
6
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}
SQLite подходит для разработки, но в продакшене лучше использовать PostgreSQL или MySQL. При переходе на другую СУБД достаточно изменить параметры подключения.

Конфиденциальные данные, такие как пароли и ключи, не стоит хранить в коде. Их выносят в переменные окружения или .env файл:

Python
1
2
3
4
5
6
7
8
from pathlib import Path
from dotenv import load_dotenv
import os
 
load_dotenv()
 
SECRET_KEY = os.getenv('DJANGO_SECRET_KEY')
DEBUG = os.getenv('DEBUG', 'False') == 'True'
Для работы с .env файлами понадобится пакет python-dotenv:

Bash
1
pip install python-dotenv
Создадим файл .env в корне проекта:

Python
1
2
DJANGO_SECRET_KEY=your-secret-key-here
DEBUG=True
Файл .env нужно добавить в .gitignore, чтобы не публиковать секретные данные в репозитории:

Bash
1
echo ".env" >> .gitignore
Статические файлы (CSS, JavaScript, изображения) Django собирает из всех приложений в единую директорию. Настройки для этого:

Python
1
2
3
4
5
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
    BASE_DIR / 'static',
]
Для загружаемых файлов (например, изображений проектов) нужны дополнительные настройки:

Python
1
2
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
В режиме разработки Django не раздаёт медиафайлы автоматически. Для этого нужно добавить маршруты в urls.py:

Python
1
2
3
4
5
6
from django.conf import settings
from django.conf.urls.static import static
 
urlpatterns = [
    # другие маршруты
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Важный аспект безопасности – настройка разрешённых хостов:

Python
1
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')
В разработке можно использовать:

Python
1
ALLOWED_HOSTS=localhost,127.0.0.1
Django использует промежуточные слои (middleware) для обработки запросов. По умолчанию включены основные компоненты безопасности:

Python
1
2
3
4
5
6
7
8
9
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
При разработке полезно включить Django Debug Toolbar – инструмент для отладки SQL-запросов, кэширования и других аспектов работы сайта:

Bash
1
pip install django-debug-toolbar
Добавим его в настройки:

Python
1
2
3
4
if DEBUG:
    INSTALLED_APPS += ['debug_toolbar']
    MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
    INTERNAL_IPS = ['127.0.0.1']

Python GUI: создаём простое приложение с PyQt и Qt Designer
Хотел поинтересоваться по поводу этой статьи https://tproger.ru/translations/python-gui-pyqt/...

django-php: PHP в шаблонах Django
Смотрите, какая штука: django-php позволяет вам писать PHP-код прямо в шаблонах Django. Первая...

Настроить авторизацию через социальные сети в django с помощью Django Social Auth
Пытаюсь настроить авторизацию через социальные сети в django с помощью Django Social Auth, но...

совместимость django-imagekit с релизами Django?
Скажите, пожалуйста, где в документации django-imagekit указана совместимость с релизами Django? А...


Структура проекта портфолио



После настройки окружения можно приступать к созданию структуры проекта. Django-приложение состоит из отдельных компонентов каждый из которых отвечает за определённую функциональность. Для портфолио понадобятся два основных приложения: pages для статических страниц и projects для управления проектами.
Создадим первое приложение pages:

Bash
1
python manage.py startapp pages
Django создаст директорию pages со стандартной структурой:
views.py – представления (обработчики запросов),
models.py – модели данных,
admin.py – настройки админ-панели,
apps.py – конфигурация приложения,
tests.py – модульные тесты.

Добавим приложение в INSTALLED_APPS:

Python
1
2
3
4
INSTALLED_APPS = [
    'pages.apps.PagesConfig',
    # остальные приложения
]
Теперь создадим базовый шаблон, от которого будут наследоваться все страницы сайта. В корне проекта создадим директорию templates и файл base.html:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}Портфолио{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="/">Портфолио</a>
            <div class="navbar-nav">
                <a class="nav-link" href="{% url 'project_index' %}">Проекты</a>
            </div>
        </div>
    </nav>
    <div class="container mt-4">
        {% block content %}{% endblock %}
    </div>
</body>
</html>
Для работы с шаблонами нужно указать Django путь к директории templates в settings.py:

Python
1
2
3
4
5
6
7
8
TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        # остальные настройки
    }
]
Создадим главную страницу в приложении pages. В pages/views.py добавим:

Python
1
2
3
4
from django.shortcuts import render
 
def home(request):
    return render(request, 'pages/home.html')
А в pages/templates/pages/home.html:

HTML5
1
2
3
4
5
6
{% extends 'base.html' %}
 
{% block content %}
<h1>Добро пожаловать!</h1>
<p>Здесь собраны мои проекты в сфере веб-разработки.</p>
{% endblock %}
Теперь создадим второе приложение для управления проектами:

Bash
1
python manage.py startapp projects
И добавим его в INSTALLED_APPS:

Python
1
2
3
4
5
INSTALLED_APPS = [
    'pages.apps.PagesConfig',
    'projects.apps.ProjectsConfig',
    # остальные приложения
]
В projects/models.py определим модель Project:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.db import models
 
class Project(models.Model):
    title = models.CharField('Название', max_length=100)
    description = models.TextField('Описание')
    technology = models.CharField('Технологии', max_length=50)
    image = models.ImageField('Изображение', upload_to='projects/')
    created_at = models.DateTimeField('Создано', auto_now_add=True)
    
    class Meta:
        verbose_name = 'Проект'
        verbose_name_plural = 'Проекты'
        ordering = ['-created_at']
    
    def __str__(self):
        return self.title
Создадим файл urls.py в каждом приложении для управления маршрутизацией. В pages/urls.py:

Python
1
2
3
4
5
6
from django.urls import path
from . import views
 
urlpatterns = [
    path('', views.home, name='home'),
]
В projects/urls.py:

Python
1
2
3
4
5
6
7
from django.urls import path
from . import views
 
urlpatterns = [
    path('', views.project_index, name='project_index'),
    path('<int:pk>/', views.project_detail, name='project_detail'),
]
Объединим маршруты в корневом urls.py:

Python
1
2
3
4
5
6
7
8
from django.contrib import admin
from django.urls import path, include
 
urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pages.urls')),
    path('projects/', include('projects.urls')),
]
В projects/admin.py зарегистрируем модель в админ-панели:

Python
1
2
3
4
5
6
7
8
from django.contrib import admin
from .models import Project
 
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    list_display = ['title', 'technology', 'created_at']
    list_filter = ['technology', 'created_at']
    search_fields = ['title', 'description']
Для загрузки изображений создадим директорию media в корне проекта и добавим её в .gitignore:

Bash
1
2
mkdir media
echo "media/" >> .gitignore
После создания структуры проекта нужно применить миграции:

Bash
1
2
python manage.py makemigrations
python manage.py migrate
Реализуем представления для отображения проектов. В projects/views.py создадим два представления:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from django.shortcuts import render, get_object_or_404
from .models import Project
 
def project_index(request):
    projects = Project.objects.all()
    context = {'projects': projects}
    return render(request, 'projects/project_index.html', context)
 
def project_detail(request, pk):
    project = get_object_or_404(Project, pk=pk)
    context = {'project': project}
    return render(request, 'projects/project_detail.html', context)
Создадим соответствующие шаблоны в директории projects/templates/projects/. Сначала project_index.html:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% extends 'base.html' %}
 
{% block content %}
<h1>Мои проекты</h1>
<div class="row">
    {% for project in projects %}
    <div class="col-md-4 mb-4">
        <div class="card h-100">
            {% if project.image %}
            <img src="{{ project.image.url }}" class="card-img-top" alt="{{ project.title }}">
            {% endif %}
            <div class="card-body">
                <h5 class="card-title">{{ project.title }}</h5>
                <p class="card-text">{{ project.description|truncatewords:30 }}</p>
                <p><small class="text-muted">{{ project.technology }}</small></p>
                <a href="{% url 'project_detail' project.pk %}" class="btn btn-primary">Подробнее</a>
            </div>
        </div>
    </div>
    {% endfor %}
</div>
{% endblock %}
И project_detail.html:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{% extends 'base.html' %}
 
{% block content %}
<div class="row">
    <div class="col-md-8">
        {% if project.image %}
        <img src="{{ project.image.url }}" class="img-fluid rounded" alt="{{ project.title }}">
        {% endif %}
    </div>
    <div class="col-md-4">
        <h1>{{ project.title }}</h1>
        <p class="text-muted">{{ project.technology }}</p>
        <p>{{ project.description }}</p>
        <p><small class="text-muted">Создано: {{ project.created_at|date:"d.m.Y" }}</small></p>
        <a href="{% url 'project_index' %}" class="btn btn-secondary">← Назад к проектам</a>
    </div>
</div>
{% endblock %}
Для управления проектами создадим суперпользователя:

Bash
1
python manage.py createsuperuser
Чтобы сделать админ-панель более удобной, добавим дополнительные настройки в projects/admin.py:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.utils.html import format_html
from django.contrib import admin
from .models import Project
 
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    list_display = ['title', 'technology', 'created_at', 'image_preview']
    list_filter = ['technology', 'created_at']
    search_fields = ['title', 'description']
    readonly_fields = ['image_preview']
    
    def image_preview(self, obj):
        if obj.image:
            return format_html('<img src="{}" style="max-height: 50px;"/>', obj.image.url)
        return ''
    image_preview.short_description = 'Превью'
Добавим валидацию загружаемых изображений в models.py:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.core.exceptions import ValidationError
 
def validate_image(image):
    max_size = 2 * 1024 * 1024  # 2MB
    if image.size > max_size:
        raise ValidationError('Размер изображения не должен превышать 2MB')
 
class Project(models.Model):
    # ... остальные поля ...
    image = models.ImageField(
        'Изображение',
        upload_to='projects/',
        validators=[validate_image]
    )
Для удобства работы с изображениями добавим thumbnail-генератор:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.db import models
from PIL import Image
from io import BytesIO
from django.core.files.base import ContentFile
 
class Project(models.Model):
    # ... остальные поля ...
    
    def save(self, *args, **kwargs):
        if self.image:
            img = Image.open(self.image)
            if img.height > 1200 or img.width > 1200:
                output_size = (1200, 1200)
                img.thumbnail(output_size)
                output = BytesIO()
                img.save(output, format=img.format)
                output.seek(0)
                self.image = ContentFile(
                    output.read(),
                    name=self.image.name
                )
        super().save(*args, **kwargs)
Для SEO-оптимизации добавим мета-теги в шаблоны. В base.html:

HTML5
1
2
3
4
5
6
7
8
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="{% block meta_description %}Портфолио веб-разработчика{% endblock %}">
    <meta name="keywords" content="{% block meta_keywords %}портфолио, веб-разработка, django{% endblock %}">
    <title>{% block title %}Портфолио{% endblock %}</title>
    <!-- ... остальные теги ... -->
</head>
В project_detail.html:

HTML5
1
2
3
{% block meta_description %}{{ project.description|truncatewords:30 }}{% endblock %}
{% block meta_keywords %}{{ project.technology|lower }}, веб-разработка, портфолио{% endblock %}
{% block title %}{{ project.title }} | Портфолио{% endblock %}

Работа с моделями и базой данных



Модели в Django определяют структуру базы данных и бизнес-логику приложения. Каждая модель представляет собой Python-класс, наследующийся от django.db.models.Model. При создании моделей важно продумать связи между ними и правильно выбрать типы полей. Для портфолио расширим модель Project дополнительными полями:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from django.db import models
from django.utils.text import slugify
 
class Project(models.Model):
    title = models.CharField('Название', max_length=100)
    slug = models.SlugField('URL', unique=True, blank=True)
    description = models.TextField('Описание')
    technology = models.CharField('Технологии', max_length=50)
    image = models.ImageField('Изображение', upload_to='projects/')
    github_link = models.URLField('GitHub', blank=True)
    live_demo = models.URLField('Демо', blank=True)
    created_at = models.DateTimeField('Создано', auto_now_add=True)
    updated_at = models.DateTimeField('Обновлено', auto_now=True)
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)
Добавим категории для проектов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Category(models.Model):
    name = models.CharField('Название', max_length=50)
    slug = models.SlugField('URL', unique=True)
    description = models.TextField('Описание', blank=True)
 
    class Meta:
        verbose_name = 'Категория'
        verbose_name_plural = 'Категории'
        ordering = ['name']
 
    def __str__(self):
        return self.name
 
class Project(models.Model):
    # ... существующие поля ...
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name='Категория'
    )
Для хранения технологий создадим отдельную модель:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Technology(models.Model):
    name = models.CharField('Название', max_length=30)
    icon = models.FileField('Иконка', upload_to='icons/', blank=True)
 
    class Meta:
        verbose_name = 'Технология'
        verbose_name_plural = 'Технологии'
        ordering = ['name']
 
    def __str__(self):
        return self.name
 
class Project(models.Model):
    # ... другие поля ...
    technologies = models.ManyToManyField(
        Technology,
        verbose_name='Технологии',
        related_name='projects'
    )
Django ORM предоставляет мощный API для работы с данными. Рассмотрим основные операции:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Создание объекта
project = Project.objects.create(
    title='Мой проект',
    description='Описание проекта'
)
 
# Получение всех проектов
projects = Project.objects.all()
 
# Фильтрация
django_projects = Project.objects.filter(
    technologies__name='Django'
)
 
# Исключение
non_web_projects = Project.objects.exclude(
    category__name='Web'
)
 
# Сортировка
recent_projects = Project.objects.order_by('-created_at')
 
# Получение одного объекта
latest_project = Project.objects.latest('created_at')
 
# Агрегация
from django.db.models import Count
tech_count = Project.objects.annotate(
    tech_count=Count('technologies')
)
Для оптимизации запросов используем select_related и prefetch_related:

Python
1
2
3
4
5
6
7
# Загрузка связанных объектов
projects = Project.objects.select_related('category').prefetch_related('technologies')
 
# Использование в представлении
def project_index(request):
    projects = Project.objects.select_related('category').prefetch_related('technologies')
    return render(request, 'projects/project_index.html', {'projects': projects})
Добавим менеджер модели для частых операций:

Python
1
2
3
4
5
6
7
8
9
10
11
class ProjectManager(models.Manager):
    def with_related(self):
        return self.select_related('category').prefetch_related('technologies')
    
    def published(self):
        return self.filter(is_published=True)
 
class Project(models.Model):
    # ... поля модели ...
    is_published = models.BooleanField('Опубликовано', default=True)
    objects = ProjectManager()
Для валидации данных определим пользовательские методы:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django.core.exceptions import ValidationError
from django.utils import timezone
 
class Project(models.Model):
    # ... поля модели ...
    
    def clean(self):
        if self.live_demo and not self.live_demo.startswith('https://'):
            raise ValidationError({
                'live_demo': 'URL демо должен использовать HTTPS'
            })
    
    def validate_unique(self, *args, **kwargs):
        super().validate_unique(*args, **kwargs)
        if self.title:
            similar = Project.objects.filter(
                title__iexact=self.title
            ).exclude(pk=self.pk)
            if similar.exists():
                raise ValidationError({
                    'title': 'Проект с таким названием уже существует'
                })
Для удобства администрирования расширим админ-модель:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.contrib import admin
from django.utils.html import format_html
 
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    list_display = ['title', 'category', 'tech_list', 'created_at', 'is_published']
    list_filter = ['category', 'technologies', 'is_published']
    search_fields = ['title', 'description']
    prepopulated_fields = {'slug': ('title',)}
    filter_horizontal = ['technologies']
    
    def tech_list(self, obj):
        return ', '.join(t.name for t in obj.technologies.all())
    tech_list.short_description = 'Технологии'
Для работы с медиафайлами добавим обработку изображений:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from PIL import Image
from django.core.files.base import ContentFile
from io import BytesIO
 
class Project(models.Model):
    # ... поля модели ...
    
    def create_thumbnail(self):
        if not self.image:
            return
        
        img = Image.open(self.image)
        img.thumbnail((300, 300))
        
        thumb_io = BytesIO()
        img.save(thumb_io, format=img.format)
        
        thumbnail = ContentFile(thumb_io.getvalue())
        return thumbnail
Для кэширования часто запрашиваемых данных проекта используем декоратор @property:

Python
1
2
3
4
5
6
7
8
9
10
from django.urls import reverse
 
class Project(models.Model):
    @property
    def technology_list(self):
        return list(self.technologies.values_list('name', flat=True))
    
    @property
    def absolute_url(self):
        return reverse('project_detail', kwargs={'pk': self.pk})
Для управления состояниями проекта добавим поле status и соответствующие методы:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from django.db import models
 
class Project(models.Model):
    STATUS_DRAFT = 'draft'
    STATUS_PUBLISHED = 'published'
    STATUS_ARCHIVED = 'archived'
    
    STATUS_CHOICES = [
        (STATUS_DRAFT, 'Черновик'),
        (STATUS_PUBLISHED, 'Опубликовано'),
        (STATUS_ARCHIVED, 'В архиве'),
    ]
    
    status = models.CharField(
        'Статус',
        max_length=10,
        choices=STATUS_CHOICES,
        default=STATUS_DRAFT
    )
    
    def publish(self):
        if self.status == self.STATUS_DRAFT:
            self.status = self.STATUS_PUBLISHED
            self.save(update_fields=['status'])
    
    def archive(self):
        if self.status == self.STATUS_PUBLISHED:
            self.status = self.STATUS_ARCHIVED
            self.save(update_fields=['status'])
Для отслеживания изменений в проекте реализуем сигналы:

Python
1
2
3
4
5
6
7
8
9
10
11
from django.db.models.signals import post_save
from django.dispatch import receiver
 
@receiver(post_save, sender=Project)
def project_post_save(sender, instance, created, **kwargs):
    if created:
        # Отправка уведомления о новом проекте
        notify_about_new_project(instance)
    else:
        # Обновление кэша
        cache.delete(f'project_{instance.pk}')
Добавим методы для работы с тегами проектов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Tag(models.Model):
    name = models.CharField('Название', max_length=50)
    slug = models.SlugField('URL', unique=True)
    
    def __str__(self):
        return self.name
    
    class Meta:
        verbose_name = 'Тег'
        verbose_name_plural = 'Теги'
 
class Project(models.Model):
    tags = models.ManyToManyField(Tag, blank=True, verbose_name='Теги')
    
    def add_tags(self, tag_names):
        for name in tag_names:
            tag, created = Tag.objects.get_or_create(
                name=name,
                slug=slugify(name)
            )
            self.tags.add(tag)
Для поиска по проектам реализуем полнотекстовый поиск:

Python
1
2
3
4
5
6
7
8
9
10
11
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank
 
class ProjectManager(models.Manager):
    def search(self, query):
        search_vector = SearchVector('title', weight='A') + \
                       SearchVector('description', weight='B')
        search_query = SearchQuery(query)
        
        return self.annotate(
            rank=SearchRank(search_vector, search_query)
        ).filter(rank__gte=0.3).order_by('-rank')
Для версионирования проектов добавим модель истории изменений:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ProjectHistory(models.Model):
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        related_name='history'
    )
    title = models.CharField('Название', max_length=100)
    description = models.TextField('Описание')
    changed_at = models.DateTimeField('Изменено', auto_now_add=True)
    changed_by = models.ForeignKey(
        'auth.User',
        on_delete=models.SET_NULL,
        null=True
    )
    
    class Meta:
        ordering = ['-changed_at']
        verbose_name = 'История изменений'
        verbose_name_plural = 'История изменений'
Для автоматического создания записей в истории используем сигналы:

Python
1
2
3
4
5
6
7
8
@receiver(post_save, sender=Project)
def save_project_history(sender, instance, **kwargs):
    ProjectHistory.objects.create(
        project=instance,
        title=instance.title,
        description=instance.description,
        changed_by=instance._current_user if hasattr(instance, '_current_user') else None
    )
Добавим методы для работы с комментариями к проектам:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Comment(models.Model):
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        related_name='comments'
    )
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    text = models.TextField('Текст')
    created_at = models.DateTimeField('Создано', auto_now_add=True)
    
    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Комментарий'
        verbose_name_plural = 'Комментарии'
 
class Project(models.Model):
    def add_comment(self, user, text):
        return self.comments.create(author=user, text=text)
    
    def recent_comments(self):
        return self.comments.select_related('author')[:5]
Для оптимизации запросов к базе данных используем кастомные менеджеры:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ProjectQuerySet(models.QuerySet):
    def with_comments_count(self):
        return self.annotate(
            comments_count=models.Count('comments')
        )
    
    def popular(self):
        return self.with_comments_count().order_by('-comments_count')
    
    def by_technology(self, technology_name):
        return self.filter(technologies__name=technology_name)
 
class Project(models.Model):
    objects = ProjectQuerySet.as_manager()
Добавим методы для работы с изображениями проекта:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def project_image_path(instance, filename):
    return f'projects/{instance.slug}/{filename}'
 
class ProjectImage(models.Model):
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        related_name='images'
    )
    image = models.ImageField(
        'Изображение',
        upload_to=project_image_path
    )
    caption = models.CharField('Подпись', max_length=200, blank=True)
    order = models.PositiveIntegerField('Порядок', default=0)
    
    class Meta:
        ordering = ['order']
        verbose_name = 'Изображение'
        verbose_name_plural = 'Изображения'

Создание представлений и шаблонов



В Django представления обрабатывают запросы пользователей и возвращают ответы. Они связывают модели с шаблонами и реализуют бизнеслогику приложения. Рассмотрим создание различных типов представлений для портфолио.
Начнём с базового представления для списка проектов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from django.views.generic import ListView
from .models import Project
 
class ProjectListView(ListView):
    model = Project
    template_name = 'projects/project_list.html'
    context_object_name = 'projects'
    paginate_by = 9
    
    def get_queryset(self):
        queryset = super().get_queryset()
        return queryset.select_related('category').prefetch_related('technologies')
Для детальной страницы проекта используем DetailView:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from django.views.generic import DetailView
 
class ProjectDetailView(DetailView):
    model = Project
    template_name = 'projects/project_detail.html'
    
    def get_context_data(self, [B]kwargs):
        context = super().get_context_data([/B]kwargs)
        context['related_projects'] = Project.objects.filter(
            category=self.object.category
        ).exclude(pk=self.object.pk)[:3]
        return context
Создадим представление для фильтрации проектов по категориям:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.views.generic import ListView
from django.shortcuts import get_object_or_404
from .models import Category
 
class CategoryProjectsView(ListView):
    template_name = 'projects/category_projects.html'
    context_object_name = 'projects'
    paginate_by = 9
    
    def get_queryset(self):
        self.category = get_object_or_404(Category, slug=self.kwargs['slug'])
        return Project.objects.filter(category=self.category)
    
    def get_context_data(self, [B]kwargs):
        context = super().get_context_data([/B]kwargs)
        context['category'] = self.category
        return context
Для поиска проектов реализуем представление с формой:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.views.generic import FormView
from .forms import ProjectSearchForm
 
class ProjectSearchView(FormView):
    template_name = 'projects/search.html'
    form_class = ProjectSearchForm
    
    def form_valid(self, form):
        query = form.cleaned_data['query']
        self.results = Project.objects.search(query)
        return self.render_to_response(self.get_context_data(form=form))
    
    def get_context_data(self, [B]kwargs):
        context = super().get_context_data([/B]kwargs)
        if hasattr(self, 'results'):
            context['results'] = self.results
        return context
Теперь рассмотрим шаблоны. Создадим базовый шаблон с навигацией и подключением стилей:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{% extends 'base.html' %}
 
{% block content %}
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
    <div class="container">
        <a class="navbar-brand" href="{% url 'home' %}">
            <i class="fas fa-code"></i> Портфолио
        </a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav ms-auto">
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'project_list' %}">Проекты</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'category_list' %}">Категории</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{% url 'about' %}">Обо мне</a>
                </li>
            </ul>
        </div>
    </div>
</nav>
 
<div class="container mt-4">
    {% if messages %}
        {% for message in messages %}
            <div class="alert alert-{{ message.tags }} alert-dismissible fade show">
                {{ message }}
                <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
            </div>
        {% endfor %}
    {% endif %}
    
    {% block main_content %}{% endblock %}
</div>
 
<footer class="bg-dark text-light py-4 mt-5">
    <div class="container">
        <div class="row">
            <div class="col-md-6">
                <h5>Контакты</h5>
                <p><i class="fas fa-envelope"></i> email@example.com</p>
                <p><i class="fab fa-github"></i> GitHub</p>
            </div>
            <div class="col-md-6 text-md-end">
                <p>&copy; {% now "Y" %} Портфолио</p>
            </div>
        </div>
    </div>
</footer>
{% endblock %}
Шаблон для списка проектов:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
{% extends 'base.html' %}
 
{% block main_content %}
<div class="row mb-4">
    <div class="col">
        <h1>Проекты</h1>
    </div>
    <div class="col-auto">
        <div class="btn-group">
            <button type="button" class="btn btn-outline-primary" data-filter="all">Все</button>
            {% for category in categories %}
                <button type="button" class="btn btn-outline-primary" data-filter="{{ category.slug }}">
                    {{ category.name }}
                </button>
            {% endfor %}
        </div>
    </div>
</div>
 
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
    {% for project in projects %}
        <div class="col project-item" data-category="{{ project.category.slug }}">
            <div class="card h-100">
                {% if project.image %}
                    <img src="{{ project.image.url }}" class="card-img-top" alt="{{ project.title }}">
                {% endif %}
                <div class="card-body">
                    <h5 class="card-title">{{ project.title }}</h5>
                    <p class="card-text">{{ project.description|truncatewords:30 }}</p>
                    <div class="d-flex justify-content-between align-items-center">
                        <div class="btn-group">
                            <a href="{{ project.get_absolute_url }}" class="btn btn-sm btn-outline-primary">
                                Подробнее
                            </a>
                            {% if project.github_link %}
                                <a href="{{ project.github_link }}" class="btn btn-sm btn-outline-secondary">
                                    <i class="fab fa-github"></i> GitHub
                                </a>
                            {% endif %}
                        </div>
                        <small class="text-muted">{{ project.created_at|date:"d.m.Y" }}</small>
                    </div>
                </div>
                <div class="card-footer">
                    <small class="text-muted">
                        {% for tech in project.technologies.all %}
                            <span class="badge bg-secondary">{{ tech.name }}</span>
                        {% endfor %}
                    </small>
                </div>
            </div>
        </div>
    {% empty %}
        <div class="col">
            <div class="alert alert-info">
                Проекты не найдены
            </div>
        </div>
    {% endfor %}
</div>
 
{% if is_paginated %}
    <nav aria-label="Page navigation" class="mt-4">
        <ul class="pagination justify-content-center">
            {% if page_obj.has_previous %}
                <li class="page-item">
                    <a class="page-link" href="?page=1">&laquo; Первая</a>
                </li>
            {% endif %}
            
            {% for num in page_obj.paginator.page_range %}
                {% if page_obj.number == num %}
                    <li class="page-item active">
                        <span class="page-link">{{ num }}</span>
                    </li>
                {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
                    <li class="page-item">
                        <a class="page-link" href="?page={{ num }}">{{ num }}</a>
                    </li>
                {% endif %}
            {% endfor %}
            
            {% if page_obj.has_next %}
                <li class="page-item">
                    <a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">
                        Последняя &raquo;
                    </a>
                </li>
            {% endif %}
        </ul>
    </nav>
{% endif %}
{% endblock %}
Шаблон детальной страницы проекта требует особого внимания к деталям и удобству использования:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
{% extends 'base.html' %}
 
{% block main_content %}
<div class="row">
    <div class="col-md-8">
        {% if project.image %}
            <img src="{{ project.image.url }}" class="img-fluid rounded shadow" alt="{{ project.title }}">
        {% endif %}
        
        <div class="mt-4">
            <h1>{{ project.title }}</h1>
            <div class="d-flex gap-2 mb-3">
                {% for tech in project.technologies.all %}
                    <span class="badge bg-primary">{{ tech.name }}</span>
                {% endfor %}
            </div>
            
            <div class="card">
                <div class="card-body">
                    {{ project.description|linebreaks }}
                </div>
            </div>
        </div>
    </div>
    
    <div class="col-md-4">
        <div class="card sticky-top" style="top: 20px">
            <div class="card-body">
                <h5 class="card-title">Информация о проекте</h5>
                <ul class="list-unstyled">
                    <li><strong>Категория:</strong> {{ project.category.name }}</li>
                    <li><strong>Дата создания:</strong> {{ project.created_at|date:"d.m.Y" }}</li>
                </ul>
                
                <div class="d-grid gap-2">
                    {% if project.live_demo %}
                        <a href="{{ project.live_demo }}" class="btn btn-success">
                            <i class="fas fa-external-link-alt"></i> Демо
                        </a>
                    {% endif %}
                    
                    {% if project.github_link %}
                        <a href="{{ project.github_link }}" class="btn btn-dark">
                            <i class="fab fa-github"></i> Исходный код
                        </a>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
</div>
 
<div class="mt-5">
    <h3>Похожие проекты</h3>
    <div class="row row-cols-1 row-cols-md-3 g-4">
        {% for related in related_projects %}
            <div class="col">
                <div class="card h-100">
                    {% if related.image %}
                        <img src="{{ related.image.url }}" class="card-img-top" alt="{{ related.title }}">
                    {% endif %}
                    <div class="card-body">
                        <h5 class="card-title">{{ related.title }}</h5>
                        <p class="card-text">{{ related.description|truncatewords:20 }}</p>
                        <a href="{{ related.get_absolute_url }}" class="btn btn-outline-primary">Подробнее</a>
                    </div>
                </div>
            </div>
        {% endfor %}
    </div>
</div>
Для улучшения пользовательского опыта добавим JavaScript-функционал:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
document.addEventListener('DOMContentLoaded', function() {
    // Фильтрация проектов по категориям
    const filterButtons = document.querySelectorAll('[data-filter]');
    const projectItems = document.querySelectorAll('.project-item');
    
    filterButtons.forEach(button => {
        button.addEventListener('click', function() {
            const filter = this.dataset.filter;
            
            projectItems.forEach(item => {
                if (filter === 'all' || item.dataset.category === filter) {
                    item.style.display = 'block';
                } else {
                    item.style.display = 'none';
                }
            });
            
            filterButtons.forEach(btn => btn.classList.remove('active'));
            this.classList.add('active');
        });
    });
    
    // Плавная прокрутка
    document.querySelectorAll('a[href^="#"]').forEach(anchor => {
        anchor.addEventListener('click', function(e) {
            e.preventDefault();
            document.querySelector(this.getAttribute('href')).scrollIntoView({
                behavior: 'smooth'
            });
        });
    });
});
Для обработки форм создадим кастомные миксины:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from django.contrib import messages
from django.urls import reverse_lazy
 
class ProjectFormMixin:
    def form_valid(self, form):
        messages.success(self.request, 'Проект успешно сохранен')
        return super().form_valid(form)
    
    def form_invalid(self, form):
        messages.error(self.request, 'Пожалуйста, исправьте ошибки в форме')
        return super().form_invalid(form)
 
class ProjectCreateView(ProjectFormMixin, CreateView):
    model = Project
    template_name = 'projects/project_form.html'
    success_url = reverse_lazy('project_list')
    fields = ['title', 'description', 'category', 'technologies', 'image']
 
class ProjectUpdateView(ProjectFormMixin, UpdateView):
    model = Project
    template_name = 'projects/project_form.html'
    fields = ['title', 'description', 'category', 'technologies', 'image']
    
    def get_success_url(self):
        return self.object.get_absolute_url()
Для оптимизации производительности добавим кэширование шаблонов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.core.cache import cache
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
 
@method_decorator(cache_page(60 * 15), name='dispatch')
class CachedProjectListView(ProjectListView):
    def get_context_data(self, [B]kwargs):
        context = super().get_context_data([/B]kwargs)
        context['categories'] = cache.get_or_set(
            'all_categories',
            Category.objects.all(),
            60 * 60
        )
        return context
Для улучшения SEO добавим метаданные и микроразметку:

HTML5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{% block meta %}
<meta name="description" content="{{ project.description|truncatewords:25 }}">
<meta name="keywords" content="{{ project.technologies.all|join:', ' }}">
 
<script type="application/ld+json">
{
    "@context": "https://schema.org",
    "@type": "CreativeWork",
    "name": "{{ project.title }}",
    "description": "{{ project.description }}",
    "dateCreated": "{{ project.created_at|date:'c' }}",
    "author": {
        "@type": "Person",
        "name": "{{ project.author.get_full_name }}"
    }
}
</script>
{% endblock %}
Для обработки AJAX-запросов создадим отдельное представление:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from django.http import JsonResponse
 
def load_more_projects(request):
    page = request.GET.get('page', 1)
    projects = Project.objects.all()[(page-1)*6:page*6]
    
    data = [{
        'title': project.title,
        'description': project.description[:100],
        'image_url': project.image.url if project.image else None,
        'url': project.get_absolute_url()
    } for project in projects]
    
    return JsonResponse({
        'projects': data,
        'has_more': Project.objects.count() > page * 6
    })

Оптимизация и деплой



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

Python
1
2
3
4
5
6
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',
    }
}
Для продакшена лучше использовать Redis:

Python
1
2
3
4
5
6
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
    }
}
Статические файлы нужно собрать в одну директорию перед деплоем:

Python
1
2
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
Для сбора статики выполняем:

Bash
1
python manage.py collectstatic
Важный аспект оптимизации - настройка Gzip-сжатия. В Django это делается через middleware:

Python
1
2
3
4
MIDDLEWARE = [
    'django.middleware.gzip.GZipMiddleware',
    # другие middleware
]
Для продакшена отключаем отладочный режим и задаём допустимые хосты:

Python
1
2
DEBUG = False
ALLOWED_HOSTS = ['example.com', 'www.example.com']
Настраиваем логирование для отслеживания ошибок:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': BASE_DIR / 'error.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
            'propagate': True,
        },
    },
}
Для развёртывания можно использовать Gunicorn и Nginx. Создадим конфигурацию Gunicorn:

Python
1
2
3
4
5
# gunicorn_config.py
bind = "127.0.0.1:8000"
workers = 3
accesslog = "access.log"
errorlog = "error.log"
Конфигурация Nginx для проксирования запросов:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
    listen 80;
    server_name example.com;
 
    location /static/ {
        alias /path/to/staticfiles/;
    }
 
    location /media/ {
        alias /path/to/media/;
    }
 
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
Для автоматизации деплоя используем systemd-сервис:

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Portfolio Gunicorn
After=network.target
 
[Service]
User=www-data
Group=www-data
WorkingDirectory=/path/to/project
ExecStart=/path/to/venv/bin/gunicorn portfolio.wsgi:application
Restart=always
 
[Install]
WantedBy=multi-user.target
Безопасность - критически важный аспект. Настраиваем заголовки безопасности:

Python
1
2
3
4
5
6
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
Для мониторинга производительности используем Django Silk:

Python
1
2
3
if DEBUG:
    MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
    INSTALLED_APPS += ['silk']
Настраиваем бэкапы базы данных:

Python
1
2
3
4
5
6
7
8
9
10
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'portfolio',
        'BACKUP': {
            'DIRECTORY': BASE_DIR / 'backups',
            'DAYS': 7,
        }
    }
}
Скрипт для создания бэкапов запускаем через cron:

Bash
1
0 3 * * * /path/to/venv/bin/python /path/to/manage.py dbbackup

Полезные источники и материалы



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

Сообщество Django создало множество полезных пакетов для решения типовых задач:

django-debug-toolbar - инструмент для отладки и профилирования Django-приложений. Показывает SQL-запросы, настройки, заголовки запросов и другую отладочную информацию.
djangorestframework - мощный инструмент для создания REST API. Поддерживает сериализацию данных, аутентификацию, права доступа и многое другое.
django-environ - упрощает работу с переменными окружения и конфигурацией проекта.
django-allauth - комплексное решение для аутентификации, включая поддержку социальных сетей.
django-crispy-forms - улучшает отображение форм Django, добавляет поддержку различных CSS-фреймворков.
django-filter - позволяет легко создавать фильтры для QuerySet на основе параметров запроса.
django-money - добавляет поддержку работы с денежными величинами и валютами.
django-imagekit - библиотека для обработки изображений, создания миниатюр и применения фильтров.

Для оптимизации производительности:

django-cacheops - умное кэширование querysets и автоматическая инвалидация кэша.
django-silk - профилировщик запросов, помогает находить узкие места в производительности.
django-redis - бэкенд для кэширования с использованием Redis.
django-storages - поддержка облачных хранилищ для статических и медиафайлов.

Для тестирования:

pytest-django - интеграция pytest с Django для удобного написания тестов.
factory-boy - создание тестовых данных с поддержкой связей между моделями.
coverage.py - измерение покрытия кода тестами.
model-bakery - простое создание объектов моделей для тестов.

Инструменты для разработки:

Visual Studio Code с расширениями Python и Django - популярный редактор кода.
PyCharm Professional - мощная IDE с поддержкой Django.
DBeaver - универсальный инструмент для работы с базами данных.
Postman - тестирование API-эндпоинтов.
Docker - контейнеризация приложений для разработки и деплоя.

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



В чистой архитектуре Django-приложений рекомендуется разделять бизнес-логику и слой представления данных. Используйте сервисные классы для инкапсуляции сложной логики:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ProjectService:
    def create_project(self, data):
        project = Project(**data)
        project.full_clean()
        project.save()
        self._process_images(project, data.get('images', []))
        return project
 
    def _process_images(self, project, images):
        for image in images:
            ProjectImage.objects.create(
                project=project,
                image=image
            )
Для больших проектов рекомендуется использовать доменный подход:

Python
1
2
3
4
5
6
7
8
9
10
class ProjectDomain:
    def __init__(self, project):
        self.project = project
 
    def publish(self):
        if not self.project.is_ready_for_publication():
            raise ValidationError("Project is not ready")
        self.project.status = Project.STATUS_PUBLISHED
        self.project.published_at = timezone.now()
        self.project.save()
Для управления зависимостями используйте Poetry вместо requirements.txt:

Code
1
2
3
4
5
6
7
8
9
10
[tool.poetry]
name = "portfolio"
version = "0.1.0"
description = "Portfolio website built with Django"
 
[tool.poetry.dependencies]
python = "^3.9"
django = "^4.2"
pillow = "^9.0"
django-environ = "^0.9"
Для локальной разработки удобно использовать docker-compose:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
version: '3.8'
 
services:
  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/portfolio
  
  db:
    image: postgres:13
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_DB=portfolio
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
 
volumes:
  postgres_data:
Для CI/CD можно использовать GitHub Actions:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
name: Django CI
 
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.9'
    - name: Install Dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    - name: Run Tests
      run: |
        python manage.py test
Для отслеживания ошибок в продакшене можно использовать Sentry:

Python
1
2
3
4
5
6
7
8
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
 
sentry_sdk.init(
    dsn="your-dsn-here",
    integrations=[DjangoIntegration()],
    traces_sample_rate=1.0,
)
Для мониторинга производительности подойдет Datadog:

Python
1
2
3
4
5
6
7
from ddtrace import patch_all
patch_all()
 
DATADOG_TRACE = {
    'DEFAULT_SERVICE': 'portfolio',
    'TAGS': {'env': 'production'},
}

Django 2.0 восстановление пароля через django-allauth
Здравствуйте, у меня возник вопрос, в django-allauth есть функция восстановления пароля по эл....

Ошибка django: DoesNotExist at /catalog/ django
Здравствуйте. Пишу простейший сайт на django. Пока в нем есть верхняя панель навигации и боковое...

Django-admin : Имя "django-admin" не распознано как имя командлета, функции, файла сценария или выполняемой программы
Точнее я уже установила Django, но вот что он мне выводит, после того как пишу &quot;django-admin...

Как поменялся синтаксис от применения django.conf.urls.url к django.urls.path?
Всем привет! Изучаю Django, действую гайдам. В гайде в urlpatterns пути прописывают через url. Но...

Django Какой принцп работы Pytest если возвращает `django.db.utils.ProgrammingError`?
День добрый! Какой порядок работы `pytest` или где ядро ошибки? Решил протестировать ту чать...

Работа нейросети из книги Тарика Рашида "Создаем нейронную сеть"
Здравствуйте! Вопрос наверное к тем, кто читал сабжевую книгу и понимает, что там за сетка...

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

Словарь из имени пользователя и сумма за ним закрепленная, создаем новый пустой словарь , чтобы туда сохранить изменения
UserName = {'Vasya':500, 'Misha':500, 'Kolya':500, 'Petya':500, 'Oleg':500} new_Users ={} ...

Задача 1: Создаем игры
Создаем игры В последнее время достаточно популярной механикой в играх становится управление...

Программа для портфолио (резюме)
Всем привет. Короче я программист. Я создал игру: Моё портфолио Эта игра - первый крупный проект...

Нужны работы в портфолио по следующим темам:
Добрый день! Наполняю портфолио. Хочу написать несколько хороших примеров по темам: 1. SQL...

Портфолио
Что вы создавали в качестве портфолио на django? Или что создать в качестве портфолио на django?...

Метки django, python
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Настройка гиперпараметров с помощью Grid Search и Random Search в Python
AI_Generated 15.05.2025
В машинном обучении существует фундаментальное разделение между параметрами и гиперпараметрами моделей. Если параметры – это те величины, которые алгоритм "изучает" непосредственно из данных (веса. . .
Сериализация и десериализация данных на Python
py-thonny 15.05.2025
Сериализация — это своего рода "замораживание" объектов. Вы берёте живой, динамический объект из памяти и превращаете его в статичную строку или поток байтов. А десериализация выполняет обратный. . .
Чем асинхронная логика (схемотехника) лучше тактируемой, как я думаю, что помимо энергоэффективности - ещё и безопасность.
Hrethgir 14.05.2025
Помимо огромного плюса в энергоэффективности, асинхронная логика - тотальный контроль над каждым совершённым тактом, а значит - безусловная безопасность, где безконтрольно не совершится ни одного. . .
Многопоточные приложения на C++
bytestream 14.05.2025
C++ всегда был языком, тесно работающим с железом, и потому особеннно эффективным для многопоточного программирования. Стандарт C++11 произвёл революцию, добавив в язык нативную поддержку потоков,. . .
Stack, Queue и Hashtable в C#
UnmanagedCoder 14.05.2025
Каждый опытный разработчик наверняка сталкивался с ситуацией, когда невинный на первый взгляд List<T> превращался в узкое горлышко всего приложения. Причина проста: универсальность – это прекрасно,. . .
Как использовать OAuth2 со Spring Security в Java
Javaican 14.05.2025
Протокол OAuth2 часто путают с механизмами аутентификации, хотя по сути это протокол авторизации. Представьте, что вместо передачи ключей от всего дома вашему другу, который пришёл полить цветы, вы. . .
Анализ текста на Python с NLTK и Spacy
AI_Generated 14.05.2025
NLTK, старожил в мире обработки естественного языка на Python, содержит богатейшую коллекцию алгоритмов и готовых моделей. Эта библиотека отлично подходит для образовательных целей и. . .
Реализация DI в PHP
Jason-Webb 13.05.2025
Когда я начинал писать свой первый крупный PHP-проект, моя архитектура напоминала запутаный клубок спагетти. Классы создавали другие классы внутри себя, зависимости жостко прописывались в коде, а о. . .
Обработка изображений в реальном времени на C# с OpenCV
stackOverflow 13.05.2025
Объединение библиотеки компьютерного зрения OpenCV с современным языком программирования C# создаёт симбиоз, который открывает доступ к впечатляющему набору возможностей. Ключевое преимущество этого. . .
POCO, ACE, Loki и другие продвинутые C++ библиотеки
NullReferenced 13.05.2025
В C++ разработки существует такое обилие библиотек, что порой кажется, будто ты заблудился в дремучем лесу. И среди этого многообразия POCO (Portable Components) – как маяк для тех, кто ищет. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru