# Работа с обьявлениями --- ## Установка сторонних библиотек ---- Чтобы 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. Ее структура приведена в таблице ![](https://i.imgur.com/eeAZqOT.png) ---- Графические файлы, сохраняемые в поле 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. Ее структура приведена в таблице. ![](https://i.imgur.com/VB9tQTd.png) ---- Готовый код модели 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 ``` ---- Добавленный код создаст в контексте шаблона две переменные: * keyword — с GET-параметром keyword, который понадобится для генерирования интернет-адресов в гиперссылках пагинатора; * all— с GET-параметрами keyword и раде, которые мы добавим к интернет-адресам гиперссылок, указывающих на страницы сведений об объявлениях. ---- Код шаблона 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">&nbsp;</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">&nbsp;</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> ```
{"metaMigratedAt":"2023-06-16T16:09:30.245Z","metaMigratedFrom":"Content","title":"Работа с обьявлениями","breaks":true,"contributors":"[{\"id\":\"0d39d5a3-691d-488c-8f1e-1a0fb0be4f13\",\"add\":25443,\"del\":266}]"}
    357 views