Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django : global search in different model - Post result does not appears

Tags:

python

django

I hope you're well. I've created a global search with more than one model in different app.

Post models belongs to Nutriscore folder

UserProfile models belongs to User folder

If I use a word which cover Post and UserProfile search, for instance Main courses. I got some search result (3 results) for UserProfile but nothing appears for Post (whereas count gives me 6 results). So I don't know what's wrong in code?

user/views.py

 # global search 
class GlobalSearchView(ListView):
    template_name = 'search_global.html'
    count = 0
    countnutri = 0
    
    def get_context_data(self, *args, **kwargs):
        context = super().get_context_data(*args, **kwargs)
        context['count'] = self.count or 0
        context['countnutri'] = self.countnutri or 0
        context['query'] = self.request.GET.get('q')
        return context
    
    def get_queryset(self): # new
        query = self.request.GET.get('q', None)
        if query is not None:
            nutriscore = Post.objects.filter(
                Q(title__icontains=query) | Q(slug__icontains=query) | Q(typederepas__name__icontains=query) | Q(prixrepas__name__icontains=query) | Q(vitesserepas__name__icontains=query) | Q(force__name__icontains=query) | Q(bienfaitrepas__name__icontains=query)
            ).distinct()
            user = UserProfile.objects.filter(
                Q(pays__icontains=query) | Q(town__icontains=query) | Q(user__username__icontains=query) | Q(mealtrend__name__icontains=query) | Q(pricetrend__name__icontains=query) | Q(speedtrend__name__icontains=query)| Q(strengthtrend__name__icontains=query) | Q(wellnesstrend__name__icontains=query)
            ).distinct()
            results = chain(nutriscore,user)
            
            qs = sorted(user, 
                        key=lambda instance: instance.pk, 
                        reverse=True)
            self.count = len(qs)
            
            qn = sorted(nutriscore, 
                        key=lambda instance: instance.pk, 
                        reverse=True)
            self.countnutri = len(qn)
            
            return qs
            return qn
            return results


         

user/urls.py

path('search/', GlobalSearchView.as_view(),name="global_search"),

user/templates/global_search.html I got a menu with display / collapse function in css.

<div class="row" id="collapseNutriscore">
{% for object in object_list %}
{% with object|class_name as klass %}
{% if klass == 'Post' %}


{{ object.title }}



{% endif %}
{% endwith %}
{% endfor %}
    </div>
    
    <br>
    <br>
    
    <div class="row collapse" id="collapseCooker">
{% for object in object_list %}
{% with object|class_name as klass %}
{% if klass == 'UserProfile' %}


{{ object.user.username }}



{% endif %}
{% endwith %}
{% endfor %}

    </div>
like image 878
Louis Avatar asked Dec 11 '22 00:12

Louis


1 Answers

Disclaimer: This answer may not fit in the OP's views or models, but, it will work as-is with the examples.

For the sake of clarity, I am assuming we have two models, Musician and Album which are located in the sample app.

# sample/models.py

from django.db import models


class Musician(models.Model):
    name = models.CharField(max_length=50)

    def __str__(self):
        return f'{self.name}'


class Album(models.Model):
    artist = models.ForeignKey(Musician, on_delete=models.CASCADE)
    name = models.CharField(max_length=100)
    description = models.TextField()

    def __str__(self):
        return f'{self.name} : {self.artist}'

Then, we have to create a mixin class, for a better OOP experience as well as for the extensibility across multiple views.

#sample/mixins.py

from django.apps import apps
from django.db.models import Q
from functools import reduce
from operator import or_


class SearchMixin:
    search_keyword_arg = 'q'
    search_settings = {}
    lookup_expr = 'icontains'

    def get_search_term(self):
        return self.request.GET.get(self.search_keyword_arg)

    def build_search_query(self, model_ref, term):
        return reduce(or_, [Q(**{f'{field}__{self.lookup_expr}': term}) for field in self.search_settings[model_ref]])

    def get_search_results(self):
        has_search_result = False
        search_term = self.get_search_term()
        if not search_term:
            return {'has_search_result': has_search_result}

        results = {}

        for model_ref, fields in self.search_settings.items():
            app_name, model_str = model_ref.split('.')
            ModelKlass = apps.get_model(app_label=app_name, model_name=model_str)
            qs = ModelKlass.objects.filter(self.build_search_query(model_ref, search_term))
            results[model_ref.replace('.', '_').lower()] = qs
            if has_search_result is False and qs.exists():
                has_search_result = True

        results['has_search_result'] = has_search_result
        return results

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['search_result'] = self.get_search_results()
        return context

This SearchMixin class have the search functionality that we want. We can add this class to any Django view to get the result.

For that, we are inheriting the SearchMixin clas to a ListView as,

# sample/views.py

from django.views.generic import TemplateView
from sample.mixins import SearchMixin


class GlobalSearchView(SearchMixin, TemplateView):
    template_name = 'sample/global_search.html'
    search_settings = {
        'sample.Musician': ['name'],
        'sample.Album': ['name', 'description'],
    }

Notes:

  • I have used a TemplateView which is more suitable in this particular case.
  • There is a new class attribute named search_settings which is used to determine the search fields.

How the search_settings attribute should be?

  • It must be a dict (or dict like object)

  • The key of the dict object should be in the app_name.ModelClassName format

  • value of the dict must be an iterable of model fields

How does it look like in the template?
The SearchMixin class adding the search results to a context variable named search_result and which has another variable has_search_result (search_result.has_search_result) which can be used to check whether we have got "any match".

Also, each QuerySets are accessible by separate variables. The format of the variable is, app_name<underscore><model_name_in_lower_case>.

For example, for the sample.Musician model, the search results can be obtained (if any) in template as, {{ search_result.sample_musician }}

# sample/templates/sample/global_search.html

{% if search_result.has_search_result %}
    <strong>Musician result</strong><br>
    {% for musician in search_result.sample_musician %}<br>
        {{ musician.name }}
    {% endfor %}
    <br><br>
    <strong>Album result</strong><br>
    {% for album in search_result.sample_album %}
        {{ album.name }} -- {{ album.description }}<br>
    {% endfor %}
{% else %}
    No match
{% endif %}

Now, connect the view in urls.py and search the by using the query parameter as,

/foo-bar/global-search/?q=xx
like image 91
JPG Avatar answered May 20 '23 18:05

JPG