← Назад к курсу

Полное руководство по разработке веб-приложений на Django

1. Введение в Django

Что такое Django?

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

Основные принципы:

  • DRY (Don't Repeat Yourself) — не повторяйся
  • Convention over Configuration — соглашения важнее конфигурации
  • MVT (Model-View-Template) архитектура

Установка Django

# Создание виртуального окружения
python -m venv venv

# Активация (Windows)
venv\Scripts\activate

# Активация (Linux/Mac)
source venv/bin/activate

# Установка Django
pip install django

# Проверка установки
python -m django --version

2. Создание первого проекта

Создание проекта

django-admin startproject myproject
cd myproject

Структура проекта

myproject/
├── manage.py          # Утилита командной строки
├── myproject/
│   ├── __init__.py
│   ├── settings.py    # Настройки проекта
│   ├── urls.py        # Корневые URL-маршруты
│   ├── asgi.py        # ASGI конфигурация
│   └── wsgi.py        # WSGI конфигурация

Запуск сервера разработки

python manage.py runserver
# Сервер доступен по адресу http://127.0.0.1:8000/

3. Создание приложения

Что такое приложение в Django?

Приложение — это модуль, который выполняет конкретную задачу. Проект может содержать несколько приложений.

python manage.py startapp blog

Структура приложения

blog/
├── migrations/         # Миграции базы данных
├── __init__.py
├── admin.py           # Административный интерфейс
├── apps.py            # Конфигурация приложения
├── models.py          # Модели данных
├── tests.py           # Тесты
├── views.py           # Представления (контроллеры)
└── urls.py            # URL-маршруты приложения

4. Модели (Models)

Определение моделей

# blog/models.py
from django.db import models
from django.contrib.auth.models import User

class Category(models.Model):
    name = models.CharField(max_length=100, verbose_name="Название")
    slug = models.SlugField(unique=True)
    
    class Meta:
        verbose_name = "Категория"
        verbose_name_plural = "Категории"
    
    def __str__(self):
        return self.name

class Post(models.Model):
    # Выбор статусов
    STATUS_CHOICES = [
        ('draft', 'Черновик'),
        ('published', 'Опубликовано'),
    ]
    
    title = models.CharField(max_length=200, verbose_name="Заголовок")
    content = models.TextField(verbose_name="Содержание")
    author = models.ForeignKey(
        User, 
        on_delete=models.CASCADE,
        related_name='blog_posts'
    )
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True
    )
    status = models.CharField(
        max_length=10,
        choices=STATUS_CHOICES,
        default='draft'
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published_at = models.DateTimeField(null=True, blank=True)
    
    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['-created_at']),
        ]
    
    def __str__(self):
        return self.title
    
    def publish(self):
        """Метод для публикации поста"""
        self.status = 'published'
        self.published_at = timezone.now()
        self.save()

Миграции

# Создание миграций
python manage.py makemigrations

# Просмотр SQL миграций
python manage.py sqlmigrate blog 0001

# Применение миграций
python manage.py migrate

# Откат миграций
python manage.py migrate blog 0001

5. Административная панель

Регистрация моделей

# blog/admin.py
from django.contrib import admin
from .models import Category, Post

@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
    list_display = ['name', 'slug']
    prepopulated_fields = {'slug': ('name',)}
    search_fields = ['name']

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'status', 'created_at']
    list_filter = ['status', 'created_at', 'author']
    search_fields = ['title', 'content']
    prepopulated_fields = {'slug': ('title',)}
    raw_id_fields = ['author']
    date_hierarchy = 'published_at'
    ordering = ['status', '-published_at']
    actions = ['make_published']
    
    def make_published(self, request, queryset):
        """Действие для публикации выбранных постов"""
        updated = queryset.update(status='published')
        self.message_user(request, f'{updated} постов опубликовано')
    make_published.short_description = "Опубликовать выбранные посты"

Создание суперпользователя

python manage.py createsuperuser
# Заполнить данные: username, email, password

6. Представления (Views)

Функциональные представления

# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.core.paginator import Paginator
from .models import Post, Category
from .forms import CommentForm

def post_list(request, category_slug=None):
    """Список постов с фильтрацией по категории"""
    category = None
    categories = Category.objects.all()
    posts = Post.objects.filter(status='published')
    
    if category_slug:
        category = get_object_or_404(Category, slug=category_slug)
        posts = posts.filter(category=category)
    
    paginator = Paginator(posts, 10)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)
    
    return render(request, 'blog/post/list.html', {
        'category': category,
        'categories': categories,
        'page_obj': page_obj,
    })

def post_detail(request, year, month, day, slug):
    """Детальная информация о посте"""
    post = get_object_or_404(
        Post,
        status='published',
        published_at__year=year,
        published_at__month=month,
        published_at__day=day,
        slug=slug
    )
    
    # Список активных комментариев
    comments = post.comments.filter(active=True)
    
    if request.method == 'POST':
        # Создание комментария
        comment_form = CommentForm(data=request.POST)
        if comment_form.is_valid():
            new_comment = comment_form.save(commit=False)
            new_comment.post = post
            new_comment.save()
            return redirect(post.get_absolute_url())
    else:
        comment_form = CommentForm()
    
    return render(request, 'blog/post/detail.html', {
        'post': post,
        'comments': comments,
        'comment_form': comment_form,
    })

Классовые представления (Class-Based Views)

# blog/views.py
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy

class PostListView(ListView):
    model = Post
    template_name = 'blog/post/list.html'
    context_object_name = 'posts'
    paginate_by = 10
    
    def get_queryset(self):
        return Post.objects.filter(status='published')
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['categories'] = Category.objects.all()
        return context

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post/detail.html'
    
    def get_queryset(self):
        return Post.objects.filter(status='published')
    
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['comment_form'] = CommentForm()
        return context

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    template_name = 'blog/post/form.html'
    fields = ['title', 'content', 'category', 'status']
    success_url = reverse_lazy('post_list')
    
    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

class PostUpdateView(LoginRequiredMixin, UpdateView):
    model = Post
    template_name = 'blog/post/form.html'
    fields = ['title', 'content', 'category', 'status']
    
    def get_queryset(self):
        return super().get_queryset().filter(author=self.request.user)
    
    def get_success_url(self):
        return reverse_lazy('post_detail', kwargs={'pk': self.object.pk})

class PostDeleteView(LoginRequiredMixin, DeleteView):
    model = Post
    template_name = 'blog/post/confirm_delete.html'
    success_url = reverse_lazy('post_list')
    
    def get_queryset(self):
        return super().get_queryset().filter(author=self.request.user)

7. URL-маршрутизация

Маршруты приложения

# blog/urls.py
from django.urls import path
from . import views
from .feeds import LatestPostsFeed

app_name = 'blog'

urlpatterns = [
    # Функциональные представления
    path('', views.post_list, name='post_list'),
    path('category/<slug:category_slug>/', 
         views.post_list, 
         name='post_list_by_category'),
    path('<int:year>/<int:month>/<int:day>/<slug:slug>/',
         views.post_detail,
         name='post_detail'),
    
    # Классовые представления
    path('posts/', views.PostListView.as_view(), name='post_list_cbv'),
    path('post/<int:pk>/', views.PostDetailView.as_view(), name='post_detail_cbv'),
    path('post/new/', views.PostCreateView.as_view(), name='post_create'),
    path('post/<int:pk>/edit/', views.PostUpdateView.as_view(), name='post_edit'),
    path('post/<int:pk>/delete/', views.PostDeleteView.as_view(), name='post_delete'),
    
    # RSS лента
    path('feed/', LatestPostsFeed(), name='post_feed'),
]

Корневые маршруты проекта

# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    path('admin/', admin.site.urls),
    path('blog/', include('blog.urls', namespace='blog')),
    path('accounts/', include('django.contrib.auth.urls')),
]

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, 
                         document_root=settings.MEDIA_ROOT)
    urlpatterns += static(settings.STATIC_URL, 
                         document_root=settings.STATIC_ROOT)

8. Шаблоны (Templates)

Настройка шаблонов

# settings.py
import os

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

Базовый шаблон

<!DOCTYPE html>
<!-- templates/base.html -->
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Мой Блог{% endblock %}</title>
    
    <!-- Bootstrap CSS -->
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    
    {% block extra_css %}{% endblock %}
</head>
<body>
    <!-- Навигация -->
    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
        <div class="container">
            <a class="navbar-brand" href="{% url 'blog:post_list' %}">Мой Блог</a>
            <div class="navbar-nav">
                <a class="nav-link" href="{% url 'blog:post_list' %}">Главная</a>
                
                <!-- Меню категорий -->
                {% for category in categories %}
                <a class="nav-link" href="{% url 'blog:post_list_by_category' category.slug %}">
                    {{ category.name }}
                </a>
                {% endfor %}
                
                <!-- Аутентификация -->
                {% if user.is_authenticated %}
                    <span class="nav-link">Привет, {{ user.username }}!</span>
                    <a class="nav-link" href="{% url 'blog:post_create' %}">Новый пост</a>
                    <a class="nav-link" href="{% url 'logout' %}">Выйти</a>
                {% else %}
                    <a class="nav-link" href="{% url 'login' %}">Войти</a>
                {% endif %}
            </div>
        </div>
    </nav>

    <!-- Основной контент -->
    <main class="container mt-4">
        {% if messages %}
            {% for message in messages %}
                <div class="alert alert-{{ message.tags }}">
                    {{ message }}
                </div>
            {% endfor %}
        {% endif %}
        
        {% block content %}
        {% endblock %}
    </main>

    <!-- Футер -->
    <footer class="bg-dark text-white mt-5 py-4">
        <div class="container text-center">
            <p>&copy; {% now "Y" %} Мой Блог. Все права защищены.</p>
        </div>
    </footer>

    <!-- Bootstrap JS -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    {% block extra_js %}{% endblock %}
</body>
</html>

Шаблон списка постов

<!-- templates/blog/post/list.html -->
{% extends 'base.html' %}
{% load static %}

{% block title %}Блог{% endblock %}

{% block content %}
<div class="row">
    <!-- Основной контент -->
    <div class="col-md-8">
        <h1 class="mb-4">
            {% if category %}
                {{ category.name }}
            {% else %}
                Все посты
            {% endif %}
        </h1>
        
        {% for post in page_obj %}
        <article class="card mb-4">
            <div class="card-body">
                <h2 class="card-title">
                    <a href="{% url 'blog:post_detail' post.published_at.year post.published_at.month post.published_at.day post.slug %}" 
                       class="text-decoration-none">
                        {{ post.title }}
                    </a>
                </h2>
                
                <div class="text-muted mb-3">
                    <small>
                        Опубликовано {{ post.published_at|date:"d.m.Y" }} | 
                        Автор: {{ post.author.username }} |
                        Категория: <a href="{% url 'blog:post_list_by_category' post.category.slug %}">
                            {{ post.category.name }}
                        </a>
                    </small>
                </div>
                
                <p class="card-text">
                    {{ post.content|truncatewords:50|safe }}
                </p>
                
                <a href="{% url 'blog:post_detail' post.published_at.year post.published_at.month post.published_at.day post.slug %}" 
                   class="btn btn-primary">
                    Читать далее →
                </a>
            </div>
        </article>
        {% empty %}
        <p>Постов пока нет.</p>
        {% endfor %}
        
        <!-- Пагинация -->
        {% if page_obj.has_other_pages %}
        <nav aria-label="Page navigation">
            <ul class="pagination">
                {% if page_obj.has_previous %}
                    <li class="page-item">
                        <a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</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>
                    {% else %}
                        <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.next_page_number }}">Следующая</a>
                    </li>
                {% endif %}
            </ul>
        </nav>
        {% endif %}
    </div>
    
    <!-- Сайдбар -->
    <div class="col-md-4">
        <div class="card mb-4">
            <div class="card-header">
                <h5>Категории</h5>
            </div>
            <div class="card-body">
                <ul class="list-unstyled">
                    <li>
                        <a href="{% url 'blog:post_list' %}">Все категории</a>
                    </li>
                    {% for category in categories %}
                    <li>
                        <a href="{% url 'blog:post_list_by_category' category.slug %}">
                            {{ category.name }}
                        </a>
                    </li>
                    {% endfor %}
                </ul>
            </div>
        </div>
    </div>
</div>
{% endblock %}

9. Формы (Forms)

Создание форм

# blog/forms.py
from django import forms
from .models import Post, Comment
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content', 'category', 'status']
        widgets = {
            'title': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Введите заголовок'
            }),
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 10,
                'placeholder': 'Введите содержание'
            }),
            'category': forms.Select(attrs={'class': 'form-control'}),
            'status': forms.Select(attrs={'class': 'form-control'}),
        }
        labels = {
            'title': 'Заголовок',
            'content': 'Содержание',
            'category': 'Категория',
            'status': 'Статус',
        }

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['name', 'email', 'body']
        widgets = {
            'name': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Ваше имя'
            }),
            'email': forms.EmailInput(attrs={
                'class': 'form-control',
                'placeholder': 'Ваш email'
            }),
            'body': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 4,
                'placeholder': 'Ваш комментарий'
            }),
        }

class UserRegistrationForm(UserCreationForm):
    email = forms.EmailField(
        required=True,
        widget=forms.EmailInput(attrs={'class': 'form-control'})
    )
    
    class Meta:
        model = User
        fields = ['username', 'email', 'password1', 'password2']
        widgets = {
            'username': forms.TextInput(attrs={'class': 'form-control'}),
        }
    
    def save(self, commit=True):
        user = super().save(commit=False)
        user.email = self.cleaned_data['email']
        if commit:
            user.save()
        return user

10. Аутентификация и авторизация

Настройки аутентификации

# settings.py
LOGIN_REDIRECT_URL = 'blog:post_list'
LOGOUT_REDIRECT_URL = 'blog:post_list'
LOGIN_URL = 'login'

# Регистрация пользователей
from django.urls import path
from django.contrib.auth import views as auth_views
from blog.forms import UserRegistrationForm
from django.views.generic import CreateView

urlpatterns += [
    path('register/', 
         CreateView.as_view(
             template_name='registration/register.html',
             form_class=UserRegistrationForm,
             success_url=reverse_lazy('login')
         ), 
         name='register'),
]

Декораторы для проверки доступа

# blog/views.py
from django.contrib.auth.decorators import login_required, permission_required
from django.utils.decorators import method_decorator

# Для функциональных представлений
@login_required
@permission_required('blog.add_post', raise_exception=True)
def create_post(request):
    pass

# Для классовых представлений
@method_decorator(login_required, name='dispatch')
class ProtectedView(View):
    pass

11. Работа со статикой и медиа

Настройки

# settings.py
import os

# Статические файлы
STATIC_URL = '/static/'
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# Медиа файлы
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

Организация статики

static/
├── css/
│   └── style.css
├── js/
│   └── main.js
└── img/
    └── logo.png

media/
├── posts/
│   └── 2024/
│       └── 01/
└── profiles/
    └── avatars/

12. Тестирование

Модульные тесты

# blog/tests.py
from django.test import TestCase
from django.contrib.auth.models import User
from django.urls import reverse
from .models import Post, Category

class PostModelTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        self.category = Category.objects.create(
            name='Тестовая категория',
            slug='test-category'
        )
        self.post = Post.objects.create(
            title='Тестовый пост',
            content='Тестовое содержание',
            author=self.user,
            category=self.category,
            status='published'
        )
    
    def test_post_creation(self):
        self.assertEqual(self.post.title, 'Тестовый пост')
        self.assertEqual(self.post.author.username, 'testuser')
        self.assertEqual(self.post.status, 'published')
    
    def test_post_str_method(self):
        self.assertEqual(str(self.post), 'Тестовый пост')

class PostViewsTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(
            username='testuser',
            password='testpass123'
        )
        self.post = Post.objects.create(
            title='Тестовый пост',
            content='Тестовое содержание',
            author=self.user,
            status='published'
        )
    
    def test_post_list_view(self):
        response = self.client.get(reverse('blog:post_list'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Тестовый пост')
        self.assertTemplateUsed(response, 'blog/post/list.html')
    
    def test_post_detail_view(self):
        response = self.client.get(self.post.get_absolute_url())
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'Тестовый пост')
    
    def test_post_create_view_requires_login(self):
        response = self.client.get(reverse('blog:post_create'))
        self.assertEqual(response.status_code, 302)  # Перенаправление на login

Запуск тестов

# Запуск всех тестов
python manage.py test

# Запуск тестов конкретного приложения
python manage.py test blog

# Запуск конкретного теста
python manage.py test blog.tests.PostModelTest

# С подробным выводом
python manage.py test --verbosity=2

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

Подготовка к продакшену

# settings.py
import os
from decouple import config

# Безопасность
DEBUG = config('DEBUG', default=False, cast=bool)
SECRET_KEY = config('SECRET_KEY')
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='localhost').split(',')

# База данных PostgreSQL
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': config('DB_NAME'),
        'USER': config('DB_USER'),
        'PASSWORD': config('DB_PASSWORD'),
        'HOST': config('DB_HOST'),
        'PORT': config('DB_PORT', default='5432'),
    }
}

# Статические файлы в продакшене
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# Безопасные настройки
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True

# Логирование
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': os.path.join(BASE_DIR, 'logs/django.log'),
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
            'propagate': True,
        },
    },
}

requirements.txt

Django==4.2.7
psycopg2-binary==2.9.9
Pillow==10.1.0
python-decouple==3.8
gunicorn==21.2.0
whitenoise==6.6.0

Dockerfile

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# Установка зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Копирование приложения
COPY . .

# Сборка статики
RUN python manage.py collectstatic --noinput

# Порт
EXPOSE 8000

# Запуск приложения
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]

14. Оптимизация и лучшие практики

Оптимизация запросов

# Использование select_related и prefetch_related
posts = Post.objects.select_related('author', 'category')\
                   .prefetch_related('comments')\
                   .filter(status='published')

Кэширование

# settings.py
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
    }
}

# В представлениях
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # Кэширование на 15 минут
def post_list(request):
    pass

Middleware для мониторинга

# middleware.py
import time
from django.utils.deprecation import MiddlewareMixin

class TimingMiddleware(MiddlewareMixin):
    def process_request(self, request):
        request.start_time = time.time()
    
    def process_response(self, request, response):
        if hasattr(request, 'start_time'):
            duration = time.time() - request.start_time
            print(f"Запрос {request.path} выполнен за {duration:.2f} секунд")
        return response

15. Полный пример: Приложение To-Do List

Модель

# todo/models.py
from django.db import models
from django.contrib.auth.models import User

class Task(models.Model):
    PRIORITY_CHOICES = [
        ('low', 'Низкий'),
        ('medium', 'Средний'),
        ('high', 'Высокий'),
    ]
    
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    created_by = models.ForeignKey(User, on_delete=models.CASCADE)
    completed = models.BooleanField(default=False)
    priority = models.CharField(max_length=10, choices=PRIORITY_CHOICES, default='medium')
    due_date = models.DateField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        ordering = ['-created_at']
    
    def __str__(self):
        return self.title

Представления

# todo/views.py
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.db.models import Q
from .models import Task

class TaskListView(LoginRequiredMixin, ListView):
    model = Task
    template_name = 'todo/task_list.html'
    context_object_name = 'tasks'
    
    def get_queryset(self):
        queryset = Task.objects.filter(created_by=self.request.user)
        
        # Фильтрация
        completed = self.request.GET.get('completed')
        if completed == 'true':
            queryset = queryset.filter(completed=True)
        elif completed == 'false':
            queryset = queryset.filter(completed=False)
        
        # Поиск
        search = self.request.GET.get('search')
        if search:
            queryset = queryset.filter(
                Q(title__icontains=search) |
                Q(description__icontains=search)
            )
        
        # Сортировка
        sort_by = self.request.GET.get('sort_by', '-created_at')
        return queryset.order_by(sort_by)

class TaskCreateView(LoginRequiredMixin, CreateView):
    model = Task
    template_name = 'todo/task_form.html'
    fields = ['title', 'description', 'priority', 'due_date']
    success_url = reverse_lazy('task_list')
    
    def form_valid(self, form):
        form.instance.created_by = self.request.user
        return super().form_valid(form)

class TaskUpdateView(LoginRequiredMixin, UpdateView):
    model = Task
    template_name = 'todo/task_form.html'
    fields = ['title', 'description', 'completed', 'priority', 'due_date']
    
    def get_queryset(self):
        return Task.objects.filter(created_by=self.request.user)
    
    def get_success_url(self):
        return reverse_lazy('task_list')

class TaskDeleteView(LoginRequiredMixin, DeleteView):
    model = Task
    template_name = 'todo/task_confirm_delete.html'
    success_url = reverse_lazy('task_list')
    
    def get_queryset(self):
        return Task.objects.filter(created_by=self.request.user)

Заключение

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

  1. Официальная документация Django - https://docs.djangoproject.com/
  2. Django REST Framework для создания API
  3. Django Channels для работы с WebSockets
  4. Django Celery для асинхронных задач
  5. Тестирование с pytest-django
  6. Развертывание на Heroku/AWS/DigitalOcean

Ключевые принципы для успешной разработки:

  • Следуйте принципам DRY и KISS
  • Пишите тесты для своего кода
  • Используйте виртуальные окружения
  • Ведите журнал изменений (git)
  • Оптимизируйте запросы к базе данных
  • Следите за безопасностью приложения

Удачи в разработке!