Функциональность форм и избранного

В этой главе вы научитесь обрабатывать POST-запросы пользователей, устанавливать связи «многие ко многим» и реализовывать функцию «избранных статей».


Проектирование модели данных для функции избранного

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

Это типичная связь «многие ко многим» (ManyToMany).

Нам не нужно создавать новую модель — модель User в Django встроена; нам просто нужно добавить ManyToManyField, указывающий на User, в модель Post.

Пример

# Путь к файлу: добавьте в класс Post в blog/models.py
from django.contrib.auth.models import Пользователь

class Post(models.Model):
    # ... существующие поля
    # Коллекционер: ManyToManyField автоматически создаёт промежуточную таблицу
    favorites = models.ManyToManyField(
        Пользователь,
        related_name='favorite_posts',  # Обратный запрос: user.favorite_posts.all()
        verbose_name='collector',
        blank=True
    )

После изменения модели сгенерируйте и выполните миграцию:

(venv) $ python manage.py makemigrations
(venv) $ python manage.py migrate

Логика представления для добавления в избранное и удаления из избранного

Пример

# Путь к файлу: blog/views.py (недавно добавлено)
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from .models import Post

# Декоратор login_required: когда неаутентифицированный пользователь обращается к этому представлению, автоматически перенаправляет на страницу входа
@login_required
def toggle_favorite(request, pk):
    """Переключить статус избранного: в избранном → убрать из избранного, не в избранном → добавить в избранное"""
    post = get_object_or_404(Post, pk=pk)

    # Проверить, добавил ли текущий пользователь в избранное
    if post.favorites.filter(id=request.user.id).exists():
        # В избранном → Убрать из избранного
        post.favorites.remove(request.user)
        messages.info(request, f'Убрано из избранного "{post.title}"')
    else:
        # Не в избранном → Добавить в избранное
        post.favorites.add(request.user)
        messages.success(request, f'Добавлено в избранное "{post.title}"')

    # Перенаправить обратно на текущую страницу (HTTP_REFERER — это URL исходной страницы)
    referer = request.META.get('HTTP_REFERER', '/')
    return redirect(referer)

Краткий справочник по методам операций ManyToMany

МетодыФункцияПример
add(obj)Добавление связиpost.favorites.add(user)
remove(obj)Удаление связиpost.favorites.remove(user)
all()Запрос всех связанных объектовpost.favorites.all()
filter(...)Фильтрация по условиямpost.favorites.filter(id=1)
exists()Определить, есть ли совпадающая записьpost.favorites.filter(id=1).exists()
clear()Очистить все связиpost.favorites.clear()

Настроить маршрут избранного

Пример

# Путь к файлу: добавить в blog/urls.py
urlpatterns = [
    # ... существующие маршруты ...
    path('post/<int:pk>/favorite/', views.toggle_favorite, name='toggle_favorite'),
]

Добавить кнопку избранного на страницу подробностей

Пример

<!-- Путь к файлу: blog/templates/blog/post_detail.html (изменён) -->
{% extends 'blog/base.html' %}

{% block title %}{{ post.title }} - ПРИМЕР Блог{% endblock %}

{% block content %}
<article class="post-view">
    <span class="category-tag">{{ post.category.name }}</span>
    <div class="post-header">
        <h1>{{ post.title }}</h1>
        <!-- Кнопка «В избранное»: видна только авторизованным пользователям -->
        {% if user.is_authenticated %}
        <form method="post" action="{% url 'toggle_favorite' post.pk %}">
            {% csrf_token %}
            <button type="submit" class="fav-btn">
                {% if user in post.favorites.all %}
                    &#x2665;favorited
                {% else %}
♡ В избранное
                {% endif %}
            </button>
        </форма>
        {% endif %}
    </div>
    <time>{{ post.created_at|date:"Y-m-d" }}</time>
    <div class="content">{{ post.content|safe }}</div>
    <a href="{% url 'index' %}" class="back-link">← На главную</a>
</article>
{% endblock %}

Также добавьте кнопку избранного на карточки статей на главной странице:

Пример

<!-- Добавьте кнопку избранного в карточку статьи в index.html (область футера) -->
<div class="article-card">
    <div class="card-content">
        <span class="card-category">{{ post.category.name }}</span>
        <h3>{{ post.title }}</h3>
        <p>{{ post.summary|truncatechars:80 }}</p>
        <div class="card-footer">
            <span class="card-date">{{ post.created_at|date:"Y-m-d" }}</span>
            {% if user.is_authenticated %}
            <form method="post" action="{% url 'toggle_favorite' post.pk %}" class="fav-form">
                {% csrf_token %}
                <button type="submit" class="fav-btn-small">
                    {% if user in post.favorites.all %}&#x2665;{% else %}♡{% endif %}
                </button>
            </форма>
            {% endif %}
        </div>
    </div>
</div>

Почему кнопка избранного использует форму вместо тега <a>? Потому что операция добавления в избранное изменяет базу данных и является POST-запросом. Согласно соглашениям REST, операции чтения используют GET, операции записи используют POST. Что важнее, POST-запросы несутcsrf_token...чтобы предотвратить подделку запросов вредоносными сайтами.


Страница списка избранного

Отобразить список статей, добавленных в избранное, для авторизованных пользователей.

Пример

# Путь к файлу: blog/views.py (недавно добавлено)
@login_required
def favorites(request):
    """Список избранного текущего пользователя"""
    # Используйте related_name для обратной выборки статей, добавленных пользователем в избранное
    posts = request.user.favorite_posts.all().order_by('-created_at')
    return render(request, 'blog/favorites.html', {
        'posts': posts,
        'title': 'Моё избранное - EXAMPLE Blog'
    })

Добавьте маршрут в urls.py и добавьте ссылку «Моё избранное» в панель навигации.


Стиль кнопки избранного

Пример

/* Добавьте к стилю в base.html */
.post-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
}

.fav-btn, .fav-btn-small {
    background: none;
    border: none;
    font-size: 18px;
    cursor: pointer;
    color: #e74c3c;
    padding: 4px 8px;
    transition: transform 0.2s;
}

.fav-btn:hover, .fav-btn-small:hover {
    transform: scale(1.2);
}

.fav-btn-small {
    font-size: 16px;
}

.fav-form {
    display: inline;
}

.card-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 10px;
}

Итоги главы

В этой главе вы освоили обработку взаимодействия с пользователем в Django: ManyToManyField для установления связей «многие ко многим», POST-запросы для обработки форм, csrf_token для обеспечения безопасности, декоратор login_required для защиты представлений и request.user для определения текущего пользователя.

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

Другие расширения