Evgenii Legotckoi
4 июля 2018 г. 12:38

Django - Урок 034. Как сделать поиск по нескольким моделям данных

Содержание

В предыдущих статьях мы рассмотрели, как сделать поиск по сайту на сайте. А именно:

Но что, если у вас более одного типа контента. Вы можете иметь статьи, комментарии, форум и сообщения на форуме. Как тогда быть?

Если вы хотите сделать все сами, без использования сторонних библиотек, то вам нужно будет сделать поиск по всем нужным моделям и объединить результат. Я сделал точно так же на сайте.


urls.py

Вам понадобится один класс View, который будет обрабатывать поисковый запрос.

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

В файле urls.py пропишем маршрут для поиска

  1. app_name = 'home'
  2. urlpatterns = [
  3. path('search/', views.SearchView.as_view(), name='search'),
  4. ]

views.py

Допустим, у нас есть несколько типов контента:

  • Article
  • Comment
  • Topic
  • Post

Нужно в представлении выполнить поиск по всем видам контента и объединить их в один QuerySet и подготовить к пагинации и выдаче

  1. from itertools import chain
  2.  
  3. from django.shortcuts import render
  4. from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
  5. from django.views import View
  6.  
  7. from .models import Article, Comment, Topic, Post
  8.  
  9.  
  10. class ESearchView(View):
  11. template_name = 'search/index.html'
  12.  
  13. def get(self, request, *args, **kwargs):
  14. context = {}
  15.  
  16. q = request.GET.get('q')
  17. if q:
  18. query_sets = [] # Total QuerySet
  19.  
  20. # Searching for all models
  21. query_sets.append(Article.objects.search(query=q))
  22. query_sets.append(Comment.objects.search(query=q))
  23. query_sets.append(Topic.objects.search(query=q))
  24. query_sets.append(Post.objects.search(query=q))
  25.  
  26. # and combine results
  27. final_set = list(chain(*query_sets))
  28. final_set.sort(key=lambda x: x.pub_date, reverse=True) # Sorting
  29.  
  30. context['last_question'] = '?q=%s' % q
  31.  
  32. current_page = Paginator(final_set, 10)
  33.  
  34. page = request.GET.get('page')
  35. try:
  36. context['object_list'] = current_page.page(page)
  37. except PageNotAnInteger:
  38. context['object_list'] = current_page.page(1)
  39. except EmptyPage:
  40. context['object_list'] = current_page.page(current_page.num_pages)
  41.  
  42. return render(request=request, template_name=self.template_name, context=context)

Нюанс приведенного выше кода в том, что мы объединяем все модели данных, а также сортируем их по дате. Чтобы это стало возможным, мы будем использовать возможности языка программирования Python, а именно утиную типизацию. Сортировка по дате стала возможной благодаря тому, что во всех моделях данных есть поле даты публикации с одинаковым названием pub_date.

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

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

ArticleManager

Рассмотрим типовой менеджер статей. Мы наследуем его от базового менеджера моделей и определяем метод поиска с помощью логики поиска. Аналогично нужно прописать этот метод для всех моделей, в которых будет производиться поиск.

  1. from django.db import models
  2. from django.db.models import Q
  3.  
  4. class ArticleManager(models.Manager):
  5. use_for_related_fields = True
  6.  
  7. def search(self, query=None):
  8. qs = self.get_queryset()
  9. if query:
  10. or_lookup = (Q(title__icontains=query) | Q(content__icontains=query))
  11. qs = qs.filter(or_lookup)
  12.  
  13. return qs

Установка менеджера в модель

  1. class Article(models.Model):
  2. objects = ArticleManager()

search/index.html

А для шаблона поиска можно использовать немного измененный шаблон из одной из первых статей по организации поиска.

  1. {% load bootstrap34%}
  2. {% block page %}
  3. <h1>Поиск</h1>
  4. {% if object_list %}
  5. {% for object in object_list %}
  6. <div>
  7. <a href="{{ object.get_absolute_url }}">
  8. <h2>{{ object.itle }}</h2>
  9. </a>
  10. {{ object.content|safe }}
  11. <p><a class="btn btn-default btn-sm" href="{{ object.get_absolute_url }}">Читать далее</a></p>
  12. </div>
  13. {% endfor %}
  14. {% bootstrap_pagination object_list url=last_question %}
  15. {% else %}
  16. <p>Не найдено публикаций по вашему запросу<br>Попробуйте повторить запрос с другой формулировкой</p>
  17. {% endif %}
  18. {% endblock %}

Я специально использую в данном случае параметр обезличенного объекта, который не имеет указания на конкретный тип контента, поэтому было понятно, что там может быть любой объект контента. Главное, чтобы все методы были реализованы для всех моделей, которые используются в этом шаблоне.

Вам это нравится? Поделитесь в социальных сетях!

g
  • 9 июля 2018 г. 15:00

Исправьте код в views.py, пропущен импорт chain из itertools.

Evgenii Legotckoi
  • 10 июля 2018 г. 22:49

Спасибо! Исправил.

S
  • 13 мая 2020 г. 16:36

Добрый день, небольшая проблемка с пагинацией, отображается информация только на первой странице при переходе на другую - пустая страница, views, models.Manager, шаблон, все сделал как в данном уроке, подставил только свои значения. Подскажите, в чем может быть проблема?
Спасибо.

Evgenii Legotckoi
  • 13 мая 2020 г. 16:41

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

S
  • 13 мая 2020 г. 17:28
  1. Models, остальные похожие:
  2.  
  3. class PostManager(models.Manager):
  4. use_for_related_fields = True
  5. def search(self, query=None):
  6. qs = self.get_queryset()
  7. if query:
  8. or_lookup = (Q(title__icontains=query) | Q(text__icontains=query))
  9. qs = qs.filter(or_lookup)
  10. return qs
  11.  
  12. class Post(models.Model):
  13. title = models.CharField(max_length=40, verbose_name='Название')
  14. text = RichTextUploadingField(blank=True, null=True, verbose_name='Текст')
  15. image = models.ImageField(blank=True, upload_to=get_timestamp_path, verbose_name='Изображение')
  16. is_active = models.BooleanField(default=True, db_index=True, verbose_name='Выводить в списке?')
  17. created_at = models.DateTimeField(auto_now_add=True, verbose_name='Создано')
  18. published_date = models.DateTimeField(blank=True, null=True, verbose_name='Опубликовано')
  19. slug = models.SlugField(max_length=255, blank=True)
  20. objects = PostManager()
  21.  
  22. def delete(self, *args, **kwargs):
  23. for ap in self.postImage_set.all():
  24. ap.delete()
  25. super().delete(*args, **kwargs)
  26.  
  27. def publish(self):
  28. self.published_date = timezone.now()
  29. self.save()
  30.  
  31. def __str__(self):
  32. return self.title
  33.  
  34. def get_absolute_url(self):
  35. return '/post_list/%s/' % self.pk
  36.  
  37. class Meta:
  38. verbose_name_plural = 'Публикации'
  39. verbose_name = 'Публикация'
  40.  
  41.  
  42. Views:
  43. class SearchView(View):
  44. template_name = 'main/search.html'
  45.  
  46. def get(self, request, *args, **kwargs):
  47. context = {}
  48.  
  49. q = request.GET.get('q')
  50. if q:
  51. query_sets = [] # Общий QuerySet
  52.  
  53. # Ищем по всем моделям
  54. query_sets.append(Bb.objects.search(query=q))
  55. query_sets.append(Article.objects.search(query=q))
  56. query_sets.append(Post.objects.search(query=q))
  57.  
  58.  
  59. # и объединяем выдачу
  60. final_set = list(chain(*query_sets))
  61. final_set.sort(key=lambda x: x.created_at, reverse=True) # Выполняем сортировку
  62.  
  63. context['last_question'] = '?q=%s' % query_sets
  64.  
  65. paginator = Paginator( final_set, 2)
  66.  
  67. page = request.GET.get('page')
  68.  
  69. try:
  70. context['object_list'] = paginator.page(page)
  71. except PageNotAnInteger:
  72. context['object_list'] = paginator.page(1)
  73. except EmptyPage:
  74. context['object_list'] = paginator.page(paginator.num_pages)
  75. return render(request=request, template_name=self.template_name, context=context )
  76.  
  77.  
  78.  
  79.  
  80. Шаблон:
  81. {% extends "layout/basic.html" %}
  82. {% load thumbnail %}
  83. {% load static %}
  84. {% load bootstrap4 %}
  85.  
  86. {% block content %}
  87.  
  88. <h1>Поиск</h1>
  89. {% if object_list %}
  90. {% for object in object_list %}
  91.  
  92. <div>
  93. <a href="{{ object.get_absolute_url }}">
  94. <h2>{{ object.title }}</h2>
  95. </a>
  96. {{ object.text|safe }}
  97. <p><a class="btn btn-default btn-sm" href="{{ object.get_absolute_url }}">Читать далее</a></p>
  98. </div>
  99.  
  100. {% endfor %}
  101.  
  102. {% bootstrap_pagination object_list url=last_question %}
  103.  
  104. {% else %}
  105. <p>Не найдено публикаций по вашему запросу<br>Попробуйте повторить запрос с другой формулировкой</p>
  106. {% endif %}
  107. {% endblock %}
  108.  
  109.  
  110. urls:
  111. path('search/', views.SearchView.as_view(), name='search'),
  112.  
  113.  
  114. кнопка:
  115.  
  116. <form class="navbar-form navbar-left m-2 p-4" action="{% url 'main:search' %}">
  117. <div class="input-group">
  118. <input id="search" name="q" type="text" class="form-control" placeholder="Найти">
  119. <div class="input-group-btn">
  120. <button class="btn btn-success" type="submit">
  121. <i class="fas fa-search"></i>
  122. </button>
  123. </div>
  124. </div>
  125. </form>
Evgenii Legotckoi
  • 14 мая 2020 г. 1:50

Опечатка в статье. Напишите так

  1. context['last_question'] = '?q=%s' % q
Evgenii Legotckoi
  • 14 мая 2020 г. 2:13
  • (ред.)

И на старуху бывает проруха. Спасибо за вопрос.
Если бы не просмотрел ваш код, то скорее всего не заметил бы опечатку

S
  • 14 мая 2020 г. 2:18

Спасибо, Евгений, все заработало. Хороший сайт.

Evgenii Legotckoi
  • 14 мая 2020 г. 2:26

Спасибо за отзыв. Не стесняйтесь задавать вопросы. Только у меня убедительная просьба, все вопросы или на форуме сайта, или в комментариях к статьям. Чтобы потом у других пользователей было больше шансов найти ответ. Также на форуме присутствуют и другие опытные специалисты, они вполне могут тоже что-то подсказать, так что шансы получить ответ будут выше.

S
  • 14 мая 2020 г. 22:58

Здравствуйте, небольшой вопрос,
при добавлении формы пагинатора левая крайняя кнопка назад "<<" постоянно смещена вверх, относительно ряда цифр, не могу ее никак выровнять, в чем может быть проблема?
{% bootstrap_pagination object_list url=last_question %}
Хотел установить пагинатор прописав код, но немного запутался с адресами между страниц.
Подскажите, как правильно прописать адреса.
Спасибо.

  1. <ul class="pagination">
  2. {% if object_list.has_previous %}
  3. <li class="page-item"><a class="page-link" href="?q={{ object_list.previous_page_number }}">&lt;</a></li>
  4. <li class="page-item"><a class="page-link" href="?q={{ object_list.previous_page_number }}">{{ object_list.previous_page_number }}</a></li>
  5. {% endif %}
  6.  
  7. <li class="page-item active"><a class="page-link" href="?q={{ object_list.number }}">{{ object_list.number }}</a>
  8. </li>
  9.  
  10. {% if object_list.has_next %}
  11. <li class="page-item"><a class="page-link" href="?q={{ object_list.next_page_number }}">{{ object_list.next_page_number }}</a></li>
  12. <li class="page-item"><a class="page-link" href="?q={{ object_list.next_page_number }}">&gt;</a></li>
  13. {% endif %}
  14. </ul>
Evgenii Legotckoi
  • 15 мая 2020 г. 13:08

У вас есть ещё какие-то кастомные стили, которые могут влиять на "<<", поскольку у меня такой проблемы не наблюдалось.

Ну по идее, это как-то тако должно выглядеть

  1. href="?q={{q}}&page={{ object_list.previous_page_number }}"
S
  • 15 мая 2020 г. 15:03

Добрый день, Евгений,
сработал вот такой вариант:
href="?q={{ question }}&page={{ object_list.previous_page_number }}"
и во views добавил:
context['question'] = q
По поводу формы пагинатора, пробовал отключать все дополнительные стили, все также вылезает ошибка. Подумаю еще над этим, главное что есть рабочий вариант.
Спасибо за подсказку.

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь