Чтобы Django смог обработать выгруженные посетителями файлы, необходимо установить три дополнительных библиотеки: Easy Thumbnails, django-cleanup и Pillow. Установим их, набрав команды:
pipenv install easy-thumbnails
pipenv install django-cleanup
Добавим программные ядра двух последних библиотек: приложения easy-thumbnails и django-cleanup в список зарегистрированных в проекте.
INSTALLED_APPS = [
'django_cleanup',
'easy_thumbnails',
]
Для хранения самих выгруженных файлов отведем папку media, которую создадим в папке проекта. Для хранения миниатюр создадим в ней папку thumbnails.
В модуле settings.py укажем путь к папке media и префикс для интернет-адресов выгруженных файлов:
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
И сразу же добавим туда настройки приложения easy_thumbnails:
THUMBNAIL_ALIASES = {
'':{
'default': {
'size': (96, 96),
'crop': 'scale',
},
},
}
И наконец, добавим в список маршрутов уровня проекта маршрут для обработки выгруженных файлов:
from django.conf.urls.static import static
urlpatterns = [
...
]
if settings.DEBUG:
urlpatterns.append(path('static/<path:path>', never_cache(serve)))
urlpatterns += static (settings. MEDIA_URL, document_root=settings. MEDIA_ROOT)
Модель, хранящая объявления, будет называться Bb. Ее структура приведена в таблице
Графические файлы, сохраняемые в поле image модели, будут иметь в качестве имен текущие временные отметки. Так мы приведем имена к единому типу и заодно устраним ситуацию, когда имя выгруженного файла настолько длинное, что оно не помещается в поле модели.
Поместим в utilities.py объявление функции get_timestamp_path(), генерирующей имена сохраняемых в модели выгруженных файлов.
from datetime import datetime
from os.path import splitext
def get_timestamp_path(instance, filename):
return '%s%s' % (datetime.now().timestamp(), splitext(filename)[1])
Здесь приведен код, объявляющий сам класс модели Bb.
from .utilities import get_timestamp_path
class Bb(models.Model):
rubric = models.ForeignKey(SubRubric, on_delete=models.PROTECT, verbose_name='Рубрика')
title = models.CharField(max_length=40, verbose_name='Товар')
content = models.TextField(verbose_name='Описание')
price = models.FloatField(default=0, verbose_name='Цeна')
contacts = models.TextField(verbose_name='Контакты')
image = models.ImageField(blank=True, upload_to=get_timestamp_path, verbose_name='Изображение')
author = models.ForeignKey(AdvUser, on_delete=models.CASCADE, verbose_name='Автор объявления')
is_active = models.BooleanField(default=True, db_index=True, verbose_name='Выводить в списке?')
created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='Опубликовано')
def delete(self, *args, **kwargs):
for ai in self.additionalimage_set.all():
ai.delete()
super().delete(*args, **kwargs)
class Meta:
verbose_name_plural = 'Объявления'
verbose_name = 'Объявление'
ordering = ['-created_at']
В переопределенном методе delete() перед удалением текущей записи мы перебираем и вызовом метода delete() удаляем все связанные дополнительные иллюстрации. При вызове метода delete() возникает сигнал post_delete, обрабатываемый приложением django_cleanup, которое в ответ удалит все файлы, хранящиеся в удаленной записи.
Модель дополнительных иллюстраций мы назовем AdditionalImage. Ее структура приведена в таблице.
Готовый код модели AdditionalImage
class AdditionalImage(models.Model):
bb = models.ForeignKey(Bb, on_delete=models.CASCADE, verbose_name='Объявление')
image= models.ImageField(upload_to=get_timestamp_path, verbose_name='Изображение')
class Meta:
verbose_name_plural = 'Дополнительные иллюстрации'
verbose_name = 'Дополнительная иллюстрация'
Сделаем так, чтобы при удалении пользователя удалялись оставленные им объявления. Для этого добавим в код модели AdvUser следующий фрагмент:
class AdvUser(Abstractuser):
...
def delete(self, *args, **kwargs):
for bb in self.bb_set.all () :
bb.delete ()
super().delete(*args, **kwargs)
Чтобы с объявлениями можно было работать посредством административного сайта, объявим редактор объявлений BbAdmin и встроенный редактор дополнительных иллюстраций AdditionaiImageInline.
from .models import Bb, AdditionalImage
class AdditionalImageInline(admin.TabularInline):
model = AdditionalImage
class BbAdmin(admin.ModelAdmin) :
list_display = ('rubric', 'title', 'content', 'author', 'created_at')
fields = (('rubric', 'author'), 'title', 'content', 'price', 'contacts', 'image', 'is_active')
inlines = (AdditionalImageInline,)
admin.site.register(Bb, BbAdmin)
На страницах добавления и правки объявлений выведем раскрывающиеся списки подрубрики и пользователя в одну строку — ради компактности.
Мы создадим две страницы:
Код формы поиска SearchForm очень прост.
class SearchForm (forms.Form) :
keyword = forms.CharField(required=False, max_length=20, label='')
Чтобы проверить написанный на прошлом занятии код, мы создали ничего не делающий контроллер-функцию by_rubric(). Настала пора "наполнить" его полезным кодом.
from django.core.paginator import Paginator
from django.db.models import Q
from .models import SubRubric, Bb
from .forms import SearchForm
def by_rubric(request, pk):
rubric = get_object_or_404(SubRubric, pk=pk)
bbs = Bb.objects.filter(is_active=True, rubric=pk)
if 'keyword' in request.GET:
keyword = request.GET['keyword']
q = Q(title__icontains=keyword) | Q(content__icontains=keyword)
bbs = bbs.filter(q)
else:
keyword = ''
form = SearchForm(initial={'keyword': keyword})
paginator = Paginator(bbs, 2)
if 'page' in request.GET:
page_num = request.GET['page']
else:
page_num = 1
page = paginator.get_page(page_num)
context = {'rubric': rubric, 'page': page, 'bbs': page.object_list, 'form': form}
return render(request, 'main/by_rubric.html', context)
Извлекаем выбранную посетителем рубрику — нам понадобится вывести на странице ее название. Затем выбираем объявления, относящиеся к этой рубрике и помеченные для вывода (те, у которых поле is active хранит значение True). После этого выполняем фильтрацию уже отобранных объявлений по введенному посетителем искомому слову, взятому из GET-параметра keyword.
Вот фрагмент кода, "отвечающий" за фильтрацию объявлений по введенному посетителем слову:
if 'keyword' in request.GET:
keyword = request.GET['keyword']
q = Q(title__icontains=keyword) | Q(content__icontains=keyword)
bbs = bbs.filter(q)
else:
keyword = ''
form = SearchForm(initial={'keyword': keyword})
Дальше по презентации не бежать, тут будет описание возникшей проблемы.
Откроем модуль middlewares.py пакета приложения, найдем код обработчика контекста bboard_context_processor() и вставим в него следующий фрагмент:
def bboard_context_processor(request):
context = {}
context['rubrics'] = SubRubric.objects.all()
context['keyword'] = ''
context['all'] = ''
if 'keyword' in request.GET:
keyword = request. GET ['keyword']
if keyword:
context['keyword'] = '?keyword=' + keyword
context['all'] = context['keyword']
if 'page' in request.GET:
page = request.GET['page']
if page != '1':
if context['all']:
context['all' ] += '&page=' + page
else:
context['all'] = '?page=' + page
return context
Добавленный код создаст в контексте шаблона две переменные:
Код шаблона main\by_rubric.html, который сформирует страницу списка объявлений.
{% extends "layout/basic.html" %}
{% load thumbnail %}
{% load static %}
{% load bootstrap4 %}
{% block title %}{{ rubric }}{% endblock %}
{% block content %}
<h2 class="mb-2">{{ rubric }}</h2>
<div class="container-fluid mb-2">
<div class="row">
<div class="col"> </div>
<form class="col-md-auto form-inline">
{% bootstrap_form form show_label=False %}
{% bootstrap_button content='Искать' button_type='submit' %}
</form>
</div>
</div>
{% if bbs %}
<ul class="list-unstyled">
{% for bb in bbs %}
<li class="media my-5 p-3 border">
{% url 'main:detail' rubric_pk=rubric.pk pk=bb.pk as url %}
<a href="{{ url }}{{ all }}">
{% if bb.image %}
<img class="mr-3" src="{% thumbnail bb.image 'default' %}">
{% else %}
<img class="mr-3" src="{% static 'main/empty.jpg' %}">
{% endif %}
</a>
<div class="media-body">
<h3><a href="{{ url }}{{ all }}">{{ bb.title }}</a></h3>
<div>{{ bb.content }}</div>
<p class="text-right font-weight-bold">{{ bb.price }} руб.</p>
<p class="text-right font-italic">{{ bb.created_at }}</p>
</div>
</li>
{% endfor %}
</ul>
{% bootstrap_pagination page url=keyword %}
{% endif %}
{% endblock %}
Чтобы вывести форму поиска, прижав ее к правой части страницы, используем конструкцию следующего вида:
<div class="container-fluid mb-2">
<div class="row">
<div class="col"> </div>
<form class="col-md-auto . . . ">
...
</form>
</div>
</div>
Первым элементом-"ячейкой" у нас является пустой блок, а вторым — форма поиска. В результате на экране мы увидим лишь форму, сдвинутую к правому краю страницы.
Посмотрим на код самой формы:
<form class=". . . form-inline">
{% bootstrap_form form show_label=False %}
{% bootstrap_button content='Искать' button_type='submit' %}
</form>
Для вывода очередной части списка объявлений применяем особые перечни Bootstrap. Взглянем на код, который их формирует:
<ul class="list-unstyled">
<li class="media my-5 p-3 border">
<img class="mr-3" . . .>
<div class="media-body">
...
</div>
</li>
</ul>
Гиперссылку на страницу сведений об объявлении создаем на базе основной иллюстрации и названия товара. Чтобы не генерировать интернет-адрес для этих гиперссылок дважды, сохраним его в переменной url:
{% url 'main:detail' rubric_pk=rubric.pk pk=bb.pk as url %}
Основная иллюстрация к объявлению у нас является необязательной к указанию. Поэтому нужно предусмотреть случай, когда пользователь оставит объявление без основной иллюстрации. Для этого мы написали такой код:
<a href="{{ url }}{{ all }}">
{% if bb.image %}
<img class="mr-3" src="{% thumbnail bb.image 'default' %}">
{% else %}
<img class="mr-3" src="{% static 'main/empty.jpg' %}">
{% endif %}
</a>
Напоследок посмотрим на тег шаблонизатора, создающий пагинатор: {% bootstrap_pagination page url=keyword %}
Страницу со сведениями по конкретным обьявлениям будет выводить контроллер-функция detail(), который мы напишем позже. А сейчас запишем ведущий на него маршрут, поместив его непосредственно перед маршрутом, ведущим на контроллер by rubric():
from .views import detail
...
urlpatterns = [
...
path('<int:rubric_pk>/<int:pk>/', detail, name='detail') ,
path('<int:pk>/', by_rubric, name='by_rubric'),
...
]
Код контроллера detail(). Реализуем его в виде функции, поскольку далее будем формировать в нем еще и список комментариев к объявлению, а в контроллере-функции это сделать проще.
def detail(request, rubric_pk, pk):
bb = get_object_or_404(Bb, pk=pk)
ais = bb.additionalimage_set.all()
context = {'bb': bb, 'ais': ais}
return render(request, 'main/detail.html', context)
Код шаблона main\detail.html, выводящего страницу сведений об объявлении.
{% extends "layout/basic.html" %}
{% block title %}{{ bb.title }} - {{ bb.rubric.name }}{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<div class="row">
{% if bb.image %}
<div class="col-md-auto"><img src="{{ bb.image.url }}"
class="main-image"></div>
{% endif %}
<div class="col">
<h2>{{ bb.title }}</h2>
<p>{{ bb.content }}</p>
<p class="font-weight-bold">{{ bb.price }} руб.</p>
<p>{{ bb.contacts }}</p>
<p class="text-right font-italic">Добавлено
{{ bb.created_at }}</p>
</div>
</div>
</div>
{% if ais %}
<div class="d-flex justify-content-between flex-wrap mt-5">
{% for ai in ais %}
<div>
<img class="additional-image" src="{{ ai.image.url }}">
</div>
{% endfor %}
</div>
{% endif %}
<p><a href="{% url 'main:by_rubric' pk=bb.rubric.pk %}{{ all }}">Назад</a></p>
{% endblock %}
Код, выводящий основные сведения об объявлении (название, описание и цену товара, контакты, временную отметку добавления объявления, основную иллюстрацию, если таковая указана), располагается между вот этими тегами:
<div class="container-fluid mt-3">
...
</div>
А вот код, выводящий дополнительные иллюстрации, заслуживает более пристального
рассмотрения. Вот он:
<div class="d-flex justify-content-between flex-wrap mt-5">
{% for ai in ais %}
<div>
<img class="additional-image" src="{{ ai.image.url }}">
</div>
{% endfor %}
</div>
Осталось установить ширину основной и дополнительных иллюстраций— соответственно 300 и 180 пикселов. Откроем таблицу стилей main\style.css и добавим в нее следующий фрагмент кода:
img.main-image {
width: ЗООрх;
}
img.additional-image {
width: 180px;
}
Найдем код контроллера-функции index(), который выводит главную страницу, и добавим в него фрагмент, выбирающий из базы последние 10 объявлений:
def index(request):
bbs = Bb.objects.filter(is_active=True)[:10]
context = {'bbs': bbs}
return render(request, 'main/index.html', context)
Шаблон index.html
{% extends "layout/basic.html" %}
{% load thumbnail %}
{% load static %}
{% load bootstrap4 %}
{% block content %}
<h2>Последние 10 объявлений</h2>
{% if bbs %}
<ul class="list-unstyled">
{% for bb in bbs %}
<li class="media my-5 p-3 border">
{% url 'main:detail' rubric_pk=bb.rubric.pk pk=bb.pk as url %}
<a href="{{ url }}{{ all }}">
{% if bb.image %}
<img class="mr-3" src="{% thumbnail bb.image 'default' %}">
{% else %}
<img class="mr-3" src="{% static 'main/empty.jpg' %}">
{% endif %}
</a>
<div class="media-body">
<h3><a href="{{ url }}{{ all }}">{{ bb.title }}</a></h3>
<div>{{ bb.content }}</div>
<p class="text-right font-weight-bold">{{ bb.price }} руб.</p>
<p class="text-right font-italic">{{ bb.created_at }}</p>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
Осталось подготовить инструменты, посредством которых зарегистрированные пользователи будут просматривать перечень своих объявлений, добавлять, править и удалять объявления.
Сначала добавим в контроллер-функцию profile() следующий код:
@login_required
def profile(request):
bbs = Bb.objects .filter (author=request. user.pk)
context = {'bbs':bbs}
return render(request, 'main/profile.html', context)
Код шаблона profile
{% extends "layout/basic.html" %}
{% load thumbnail %}
{% load static %}
{% load bootstrap4 %}
{% block title %}Профиль пользователя{% endblock %}
{% block content %}
<h2>Профиль пользователя {{ user.username }}</h2>
{% if user.first_name and user.last_name %}
<p>3дравствуйте, {{ user.first_name }} {{ user.last_name }}!</p>
{% else %}
<p>3дравствуите!</p>
{% endif %}
<h3>Ваши объявления</h3>
<h2 class="mb-2">{{ rubric }}</h2>
{% if bbs %}
<ul class="list-unstyled">
{% for bb in bbs %}
<li class="media my-5 p-3 border">
{% url 'main:profile_bb_detail' pk=bb.pk as url %}
<a href="{{ url }}{{ all }}">
{% if bb.image %}
<img class="mr-3" src="{% thumbnail bb.image 'default' %}">
{% else %}
<img class="mr-3" src="{% static 'main/empty.jpg' %}">
{% endif %}
</a>
<div class="media-body">
<h3><a href="{{ url }}{{ all }}">{{ bb.title }}</a></h3>
<div>{{ bb.content }}</div>
<p class="text-right font-weight-bold">{{ bb.price }} руб.</p>
<p class="text-right font-italic">{{ bb.created_at }}</p>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
Код profile_bb_detail
@login_required
def profile_bb_detail(request, pk):
bb = get_object_or_404(Bb, pk=pk)
ais = bb.additionalimage_set.all()
context = {'bb': bb, 'ais': ais}
return render(request, 'main/profile_bb_detail.html', context)
Код шаблона profile_bb_detail
{% extends "layout/basic.html" %}
{% block title %}{{ bb.title }} - {{ bb.rubric.name }}{% endblock %}
{% block content %}
<div class="container-fluid mt-3">
<div class="row">
{% if bb.image %}
<div class="col-md-auto"><img src="{{ bb.image.url }}" class="main-image"></div>
{% endif %}
<div class="col">
<h2>{{ bb.title }}</h2>
<p>{{ bb.content }}</p>
<p class="font-weight-bold">{{ bb.price }} руб.</p>
<p>{{ bb.contacts }}</p>
<p class="text-right font-italic">Добавлено
{{ bb.created_at }}</p>
</div>
</div>
</div>
{% if ais %}
<div class="d-flex justify-content-between flex-wrap mt-5">
{% for ai in ais %}
<div>
<img class="additional-image" src="{{ ai.image.url }}">
</div>
{% endfor %}
</div>
{% endif %}
<p><a href="{% url 'main:by_rubric' pk=bb.rubric.pk %}{{ all }}">Назад</a></p>
{% endblock %}
Объявим форму BbForm, связанную с моделью вь, для ввода самого объявления и встроенный набор форм AiFormSet, связанный с моделью AdditionalImage, в которые будут заноситься дополнительные иллюстрации.
from django.forms import inlineformset_factory
from .models import Bb, AdditionalImage
class BbForm(forms.ModelForm):
class Meta:
model = Bb
fields = '__all__'
widgets = {'author': forms.HiddenInput}
AiFormSet = inlineformset_factory(Bb, AdditionalImage, fields='__all__')
Контроллер, добавляющий объявление, реализуем в виде функции (в виде класса его реализовать будет сложнее) и назовем profile_bb_add().
from django.shortcuts import redirect
from .forms import BbForm, AiFormSet
@login_required
def profile_bb_add(request):
if request.method == 'POST':
form = BbForm(request.POST, request.FILES)
if form.is_valid():
bb = form.save()
formset = AiFormSet(request.POST, request.FILES, instance=bb)
if formset.is_valid() :
formset.save()
messages.add_message(request, messages.SUCCESS, 'Объявление добавлено')
return redirect('main:profile')
else:
form = BbForm (initial={'author': request.user.pk})
formset = AiFormSet()
context = {'form': form, 'formset': formset}
return render(request, 'main/profile_bb_add.html', context)
Напишем маршрут, который укажет на страницу добавления, поместив его перед маршрутом, указывающим на страницу профиля:
from .views import profile_bb_add
...
urlpatterns = [
...
path ('accounts/profile/add/', profile_bb_add, name='profile_bb_add') ,
path('accounts/profile/<int:pk>/', profile_bb_detail,
name='profile_bb_detail'),
...
]
Займемся шаблоном main\profile_bb_add.html, который создаст страницу добавления объявления.
{% extends "layout/basic.html" %}
{% load bootstrap4 %}
{% block title %}Добавление объявления - Профиль пользователя{% endblock %}
{% block content %}
<h2>Добавление объявления</h2>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
{% bootstrap_formset formset layout='horizontal' %}
{% buttons submit='Добавить' %}{% endbuttons %}
</form>
{% endblock %}
Наконец, в шаблон страницы профиля main\profile.html добавим гиперссылку на страницу добавления объявления:
<p><a href="{% url 'main:profile_bb_add' %}">Добавить объявление</a></p>
Код контроллеров profile_bb_change() И profile_bb_delete (), которые соответственно правят и удаляют объявление.
@login_required
def profile_bb_change(request, pk):
bb = get_object_or_404(Bb, pk=pk)
if request.method == 'POST':
form = BbForm(request.POST, request.FILES, instance=bb)
if form.is_valid():
bb = form.save()
formset = AiFormSet(request.POST, request.FILES, instance=bb)
if formset.is_valid():
formset.save()
messages.add_message(request, messages.SUCCESS, 'Объявление исправлено')
return redirect('main:profile')
else:
form = BbForm (instance=bb)
formset = AiFormSet(instance=bb)
context = {'form': form, 'formset': formset}
return render(request, 'main/profile_bb_change.html', context)
@login_required
def profile_bb_delete(request, pk):
bb = get_object_or_404(Bb, pk=pk)
if request.method == 'POST':
bb.delete()
messages.add_message(request, messages.SUCCESS, 'Объявление удалено')
return redirect('main:profile')
else:
context = {'bb': bb}
return render(request, 'main/profile_bb_delete.html', context)
Объявим необходимые маршруты:
from .views import profile_bb_change, profile_bb_delete
...
urlpatterns = [
...
path ('acoounts/profile/change/<int:pk>/', profile_bb_change, name='profile_bb_change'),
path ('accounts/profile/delete/<int:pk>/', profile_bb_delete, name= 'profile_bb_delete'),
path('accounts/profile/add/', profile_bb_add, name='profile_bb_add'),
...
]
Шаблон для изменения формы
{% extends "layout/basic.html" %}
{% load bootstrap4 %}
{% block title %}Правка личных данных{% endblock %}
{% block content %}
<h2>Правка личных данных пользователя {{ user.username }}</h2>
<form method="post">
{% csrf_token %}
{% bootstrap_form form layout='horizontal' %}
{% buttons submit='Сохранить' %}{% endbuttons %}
</form>
{% endblock %}
Шаблон для удаления формы
{% extends "layout/basic.html" %}
{% load bootstrap4 %}
{% block title %}Удаление записи!{% endblock %}
{% block content %}
<h2>Удаление записи {{ object.username }}</h2>
<div class="container-fluid mt-3">
<div class="row">
{% if bb.image %}
<div class="col-md-auto"><img src="{{ bb.image.url }}" class="main-image"></div>
{% endif %}
<div class="col">
<h2>{{ bb.title }}</h2>
<p>{{ bb.content }}</p>
<p class="font-weight-bold">{{ bb.price }} руб.</p>
<p>{{ bb.contacts }}</p>
<p class="text-right font-italic">Добавлено
{{ bb.created_at }}</p>
</div>
</div>
</div>
<form method="post">
{% csrf_token %}
{% buttons submit='Удалить' %}{% endbuttons %}
</form>
{% endblock %}
В шаблон main\profile.html нужно добавить код, создающий гиперссылки для правки и удаления каждого из занесенных пользователем в базу объявлений:
<div class="media-body">
<р>Рубрика: {{ bb.rubric }}</р>
<p class="text-right mt-2">
<a href="{% url 'main:profile_bb_change' pk=bb.pk %}">Исправить</a>
<a href="{% url 'main:profile_bb_delete' pk=bb.pk %}">Удалить</a>
</p>
</div>