Django 38.1. Кабинет и все посты автора
В этом посте мы сделаем кабинет автора. Для этого напишем декоратор и расширенный шаблон.
Дополнительные материалы
Для скачивания материалов необходимо войти или зарегистрироваться
Файлы также можно получить в Telegram-боте по коду: 401184
Реклама
Мы с вами реализовали практически всё, что необходимо авторам, за исключением одного важного момента - личного кабинета автора.
Этот пост будет разделён на несколько частей, в которых мы разберёмся:
- Как использовать в одном представлении несколько шаблонов при помощи декоратора.
- Как контролировать права доступа при помощи миксинов.
- Используем наследования представлений и шаблонов.
- Добавим страницы редактирования/удаления постов автором.
- Перенесём функционал черновиков на сторону сайта.
И начнём мы со страницы кабинета автора. Это будет та же страница, что и профиль автора, но расширенная.
В этом посте мы напишем собственный декоратор. Если вы ещё не сталкивались с декораторами или мало знаете о них, то настоятельно рекомендую прочитать пост "Декораторы в питоне".
Представление страницы автора.
Откроем файл 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()
.
В этом методе нам необходимо добавить всего три строки:
- Передачу в шаблон значение поля
is_author
. - Блок
if
в котором проверяем значение поляis_author
. - Если значение
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
А теперь разберём, что в нём происходит:
- Создаём функцию
set_template
, принимающую аргументы - имя шаблона и поле-флаг. - Внутри создаём функцию
decorator
, принимающую задекорированный методdispatch()
. - Ещё глубже, создаём функцию
wrapper
, принимающую те же аргументы, что и изначальный методdispatch()
. И сразу оборачиваем её во встроенный вfunctools
декоратор -wraps
. Это позволит нам получить доступ к экземпляру класса. Если этот декоратор не указать, то ключевое словоself
будет недоступно. - В переменную
user
получаем объект пользователя, на чью страницу был произведён переход. - В блоке
if
проверяем, что текущий пользователь совпадает с пользователем в переменнойuser
, а также, что он входит в группу "Автор". Если проверка проходит, то происходят следующие изменения:- Значение поля
template_name
заменяется на переданное в аргументе в самой внешней функцииset_template
. - Значение поля-флага, имя которого передано вторым аргументом, устанавливается на
True
.
- Значение поля
- Вызывается задекорированная функция.
Таким образом, мы получаем универсальный декоратор, подменяющий шаблон в момент обращения к представлению.
Шаблон профиля пользователя.
Откроем файл 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 %}
Заключение.
В коде шаблонов уже добавлены ссылки на редактирование и удаление поста автором, они пока не рабочие и, вероятно, вызовут ошибку при переходе на страницу. Про реализацию функционала редактирования и удаления я расскажу в следующем посте.
Все статьи