Cat

Django 38.1. Кабинет и все посты автора

В этом посте мы сделаем кабинет автора. Для этого напишем декоратор и расширенный шаблон.

Все статьи

Icon Link

Дополнительные материалы

Icon Link

Реклама

Icon Link
Сайт на Django proDream 14 Декабрь 2023 Просмотров: 938

Мы с вами реализовали практически всё, что необходимо авторам, за исключением одного важного момента - личного кабинета автора.

Этот пост будет разделён на несколько частей, в которых мы разберёмся:

  • Как использовать в одном представлении несколько шаблонов при помощи декоратора.
  • Как контролировать права доступа при помощи миксинов.
  • Используем наследования представлений и шаблонов.
  • Добавим страницы редактирования/удаления постов автором.
  • Перенесём функционал черновиков на сторону сайта.

И начнём мы со страницы кабинета автора. Это будет та же страница, что и профиль автора, но расширенная.

В этом посте мы напишем собственный декоратор. Если вы ещё не сталкивались с декораторами или мало знаете о них, то настоятельно рекомендую прочитать пост "Декораторы в питоне".

 

Представление страницы автора.

Откроем файл views.py в директории приложения user_app.

В нём у нас есть простенькое представление для профиля пользователя - UserProfileView:

class UserProfileView(TemplateView):
    template_name = 'user_app/profile_page.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        try:
            user = get_object_or_404(User, username=self.kwargs.get('username'))
        except User.DoesNotExist:
            raise Http404("Пользователь не найден")
        context['user_profile'] = user
        context['user_posts'] = PostModel.post_manager.filter(author=user)[:7]
        context['title'] = f'Профиль пользователя {user}'
        return context

 

Добавим следующие поля:

  • author_template_name - в это поле впишем имя шаблона страницы автора.
  • is_author - это поле-флаг для проверки того, что пользователь является автором.
  • extended_group - имя расширенной группы, в нашем случае это группа "Автор".

 

Поля класса:

template_name = 'user_app/profile_page.html'  
author_template_name = 'user_app/profile_page_author.html'  
is_author = False  
extended_group = 'Автор'

 

Помните про метод dispatch(), о котором я рассказывал в посте "Django 34.2. Простой профиль пользователя – страница настроек"? В этот раз мы не будем вносить в него изменения, а обернём его в наш собственный декоратор. Таким образом, при срабатывании метода и до вызова метода get(), который вернёт пользователю необходимый шаблон, мы проверим является ли пользователь автором и определим какой шаблон ему вернётся.

 

Код:

@set_template(author_template_name, 'is_author')  
def dispatch(self, request, *args, **kwargs):  
    return super().dispatch(request, *args, **kwargs)

 

В коде выше к методу dispatch() мы добавляем декоратор set_template, в который передаём два аргумента: шаблон автора и имя поля-флага. Об этом расскажу чуть дальше, сперва закончим с представлением.

Последнее, что у нас осталось, – это переопределённый метод get_context_data()
В этом методе нам необходимо добавить всего три строки:

  1. Передачу в шаблон значение поля is_author.
  2. Блок if в котором проверяем значение поля is_author.
  3. Если значение is_author - True, передаём в шаблон черновики автора.

 

Дополнительные строки:

context['is_author'] = self.is_author  
if self.is_author:  
    context['drafts'] = PostModel.objects.filter(author=user, status='ЧЕ')[:7]

 

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

from user_app.decorators import set_template


class UserProfileView(TemplateView):  
    template_name = 'user_app/profile_page.html'  
    author_template_name = 'user_app/profile_page_author.html'  
    is_author = False  
    extended_group = 'Автор'  

    @set_template(author_template_name, 'is_author')  
    def dispatch(self, request, *args, **kwargs):  
        return super().dispatch(request, *args, **kwargs)  

    def get_context_data(self, **kwargs):  
        context = super().get_context_data(**kwargs)  
        try:  
            user = get_object_or_404(User, username=self.kwargs.get('username'))  
        except User.DoesNotExist:  
            raise Http404("Пользователь не найден")  
        context['user_profile'] = user  
        context['is_author'] = self.is_author  
        if self.is_author:  
            context['drafts'] = PostModel.objects.filter(author=user, status='ЧЕ')[:7]  
        context['user_posts'] = PostModel.post_manager.filter(author=user)[:7]  
        context['title'] = f'Профиль пользователя {user}'  
        return context

 

Декоратор set_template.

В директории приложения user_app создадим файл decorators.py.

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

 

Посмотрим на код:

from functools import wraps  

from django.contrib.auth.models import User  
from django.shortcuts import get_object_or_404  


def set_template(special_template, role_field):  
    def decorator(view_func):  
        @wraps(view_func)  
        def wrapper(self, request, *args, **kwargs):  
            user = get_object_or_404(User, username=self.kwargs.get('username'))  
            if user  self.request.user and user.groups.filter(name=self.extended_group).exists():  
                self.template_name = special_template  
                setattr(self, role_field, True)  
            return view_func(self, request, *args, **kwargs)  

        return wrapper  

    return decorator

 

А теперь разберём, что в нём происходит:

  1. Создаём функцию set_template, принимающую аргументы - имя шаблона и поле-флаг.
  2. Внутри создаём функцию decorator, принимающую задекорированный метод dispatch().
  3. Ещё глубже, создаём функцию wrapper, принимающую те же аргументы, что и изначальный метод dispatch(). И сразу оборачиваем её во встроенный в functools декоратор - wraps. Это позволит нам получить доступ к экземпляру класса. Если этот декоратор не указать, то ключевое слово self будет недоступно.
  4. В переменную user получаем объект пользователя, на чью страницу был произведён переход.
  5. В блоке if проверяем, что текущий пользователь совпадает с пользователем в переменной user, а также, что он входит в группу "Автор". Если проверка проходит, то происходят следующие изменения:
    1. Значение поля template_name заменяется на переданное в аргументе в самой внешней функции set_template.
    2. Значение поля-флага, имя которого передано вторым аргументом, устанавливается на True.
  6. Вызывается задекорированная функция.

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

 

Шаблон профиля пользователя.

Откроем файл profile_page.html, расположенный в директории с шаблонами приложения user_app.

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

Находим блок {% if user_posts %} и помещаем его в блок {% else %}, не забыв в конце добавить тег {% endif %}. Над else прописываем тег {% if is_author %}, внутрь которого помещаем два пустых блока - для черновиков и для постов:

{% if is_author %}  
    {% block drafts %}  
    {% endblock %}  
    {% block public %}  
    {% endblock %}  
{% else %}  
        ...
{% endif %}

 

Шаблон кабинета автора.

Создадим новый файл profile_page_author.html.

В нём добавим наследование от profile_page.html и заполним два блока:

{% extends 'user_app/profile_page.html' %}

{% block page_head %}
    <h2>Профиль автора {{ user_profile }}</h2>
{% endblock %}

{% block drafts %}
    {% if drafts %}
        <h3 class="mb-3">Черновики:</h3>
        {% for draft in drafts %}
            <div class="row">
                <div class="col-10">
                    <h4 class="head2"><a href="{{ draft.get_absolute_url }}">{{ draft.title }}</a></h4>
                    <div>
                        <p class="lead"><a
                                href="{{ draft.category.get_absolute_url }}">{{ draft.category }}</a>
                            | {{ draft.publish | date:"d F Y" }} |
                            Просмотров: {{ draft.views }} | {{ draft.get_status_display }}</p>
                    </div>
                </div>
                <div class="col-2 text-center">
                    <a href="{% url 'user_app:edit_post' pk=draft.pk %}"
                       class="btn btn-sm btn-primary my-btn-success w-100 mb-1">Редактировать</a>
                    <a href="{% url 'user_app:delete_post' pk=draft.pk %}"
                       class="btn btn-sm btn-secondary my-btn-danger w-100 mb-1">Удалить</a>
                </div>
            </div>
            <hr class="mt-0">
        {% endfor %}
    {% else %}
        <h3 class="mb-3">Черновиков нет</h3>
    {% endif %}
{% endblock %}

{% block public %}
    {% if user_posts %}
        <h3 class="mb-3">Последние посты:</h3>
        {% for post in user_posts %}
            <div class="row">
                <div class="col-10">
                    <h4 class="head2"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h4>
                    <div>
                        <p class="lead"><a
                                href="{{ post.category.get_absolute_url }}">{{ post.category }}</a>
                            | {{ post.publish | date:"d F Y" }} |
                            Просмотров: {{ post.views }} | {{ post.get_status_display }}</p>
                    </div>
                </div>
                <div class="col-2 text-center">
                    <a href="{% url 'user_app:edit_post' pk=post.pk %}"
                       class="btn btn-sm btn-primary my-btn-success w-100 mb-1">Редактировать</a>
                    <a href="{% url 'user_app:delete_post' pk=post.pk %}"
                       class="btn btn-sm btn-secondary my-btn-danger w-100 mb-1">Удалить</a>
                </div>
            </div>
            <hr class="mt-0">
        {% endfor %}
        <div class="text-center mt-4">
            <a class="btn btn-primary my-btn mb-3"
               href="{% url 'user_app:user_posts' username=user_profile.username %}">Все
                посты {{ user_profile }}</a>
        </div>
    {% else %}
        <h3 class="mb-3">Постов нет</h3>
    {% endif %}
{% endblock %}

 

Представление всех постов автора.

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

Изначальное представление:

class UserPostsView(ListView):
    template_name = 'user_app/user_posts_page.html'
    context_object_name = 'posts'
    paginate_by = 10

    def get_queryset(self):
        return PostModel.post_manager.filter(author__username=self.kwargs.get('username'))

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        try:
            author = get_object_or_404(User, username=self.kwargs.get("username"))
        except User.DoesNotExist:
            raise Http404("Пользователь не найден")
        context['author'] = author
        context['title'] = f'Посты пользователя {author}'
        return context

 

Модифицированное:

class UserPostsView(ListView):  
    template_name = 'user_app/user_posts_page.html'  
    author_template_name = 'user_app/user_posts_page_author.html'  
    context_object_name = 'posts'  
    paginate_by = 10  
    is_author = False  
    extended_group = 'Автор'  

    @set_template(author_template_name, 'is_author')  
    def dispatch(self, request, *args, **kwargs):  
        return super().dispatch(request, *args, **kwargs)  

    def get_queryset(self):  
        if self.is_author:  
            return (PostModel.objects.filter(author__username=self.kwargs.get('username'))  
                    .order_by('-status', '-publish'))  
        else:  
            return PostModel.post_manager.filter(author__username=self.kwargs.get('username'))  

    def get_context_data(self, **kwargs):  
        context = super().get_context_data(**kwargs)  
        try:  
            author = get_object_or_404(User, username=self.kwargs.get("username"))  
        except User.DoesNotExist:  
            raise Http404("Пользователь не найден")  
        context['author'] = author  
        context['title'] = f'Посты пользователя {author}'  
        context['is_author'] = self.is_author  
        return context

 

Как видим, изменения идентичны представлению UserProfileView, за исключением метода get_queryset(). В нём мы проверяем авторство, если пользователь, открывший страницу, – автор, то мы выводим все посты пользователя. При этом используем сортировку, чтобы сперва выводились черновики, а затем все остальные посты. В противном случае мы выводим просто посты.

 

Шаблоны страниц для автора и пользователя.

Аналогия примерно та же, что и с шаблоном кабинета, поскольку посты получаем на этапе представления для автора или пользователя, нет необходимости делать блоки с постами. 
Вместо этого мы добавим блоки с кнопками и отображением статуса поста – черновик или опубликован.

В шаблоне user_posts_page.html в удобное место добавляем блок с кнопками:

{% if is_author %}  
    <div class="col-3 d-flex text-center">  
        {% block buttons %}  
        {% endblock %}  
    </div>  
{% endif %}

 

И блок со статусом:

{% if is_author %}{% block status %}{% endblock %}{% endif %}

 

Код шаблона:

{% for post in posts %}
    <div class="row">
        <div class="col-lg-4 col-sm-12 d-flex align-items-center">
            <img src="{{ post.image.url }}" alt="{{ post.title }}" class="img-fluid">
        </div>
        <div class="col-lg-8 col-sm-12">
            <h2 class="head2"><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
            <div class="mb-3">
                <p class="lead"><a
                        href="{{ post.category.get_absolute_url }}">{{ post.category }}</a>
                    |
                    <a href="{% url 'user_app:user_profile' username=post.author %}">{{ post.author }}</a>
                    {% if post.coauthor %}
                        {% for coauthor in post.coauthor.all %}
                            , <a href="{% url 'user_app:user_profile' username=coauthor %}">{{ coauthor }}</a>
                        {% endfor %}
                    {% endif %}
                    | {{ post.publish | date:"d F Y" }} |
                    Просмотров: {{ post.views }}{% if is_author %}{% block status %}
                    {% endblock %}{% endif %} </p>
                {% for tag in post.tags.all %}
                    <a href="{% url 'blog:tag_page' tag.slug %}"><span
                            class="badge bg-secondary">{{ tag.name }}</span></a>
                {% endfor %}
            </div>
            {{ post.short_body | safe }}
            {% if is_author %}
                <div class="col-3 d-flex text-center">
                    {% block buttons %}
                    {% endblock %}
                </div>
            {% endif %}
        </div>
    </div>
    <hr class="m-3">
{% endfor %}

 

Создаём новый файл user_posts_page_author.html, в него прописываем код блоков:

{% extends 'user_app/user_posts_page.html' %}  

{% block buttons %}  
    <a href="{% url 'user_app:edit_post' pk=post.pk %}" class="btn btn-sm btn-primary my-btn-success w-100 me-1">Редактировать</a>  
    <a href="{% url 'user_app:delete_post' pk=post.pk %}" class="btn btn-sm btn-secondary my-btn-danger w-100 me-1">Удалить</a>  
{% endblock %}  

{% block status %}  
    | {{ post.get_status_display }}  
{% endblock %}

 

Заключение.

В коде шаблонов уже добавлены ссылки на редактирование и удаление поста автором, они пока не рабочие и, вероятно, вызовут ошибку при переходе на страницу. Про реализацию функционала редактирования и удаления я расскажу в следующем посте.

 

Автор

    Нет комментариев

    Реклама