Django: Search form in Class Based ListView
Asked Answered
T

7

33

I am trying to realize a Class Based ListView which displays a selection of a table set. If the site is requested the first time, the dataset should be displayed. I would prefer a POST submission, but GET is also fine.

That is a problem, which was easy to handle with function based views, however with class based views I have a hard time to get my head around.

My problem is that I get a various number of error, which are caused by my limited understanding of the classed based views. I have read various documentations and I understand views for direct query requests, but as soon as I would like to add a form to the query statement, I run into different error. For the code below, I receive an ValueError: Cannot use None as a query value.

What would be the best practise work flow for a class based ListView depending on form entries (otherwise selecting the whole database)?

This is my sample code:

models.py

class Profile(models.Model):
    name = models.CharField(_('Name'), max_length=255)

    def __unicode__(self):
        return '%name' % {'name': self.name}

    @staticmethod
    def get_queryset(params):

        date_created = params.get('date_created')
        keyword = params.get('keyword')
        qset = Q(pk__gt = 0)
        if keyword:
            qset &= Q(title__icontains = keyword)
        if date_created:
            qset &= Q(date_created__gte = date_created)
        return qset

forms.py

class ProfileSearchForm(forms.Form):
    name = forms.CharField(required=False)

views.py

class ProfileList(ListView):
    model = Profile
    form_class = ProfileSearchForm
    context_object_name = 'profiles'
    template_name = 'pages/profile/list_profiles.html'
    profiles = []


    def post(self, request, *args, **kwargs):
        self.show_results = False
        self.object_list = self.get_queryset()
        form = form_class(self.request.POST or None)
        if form.is_valid():
            self.show_results = True
            self.profiles = Profile.objects.filter(name__icontains=form.cleaned_data['name'])
        else:
            self.profiles = Profile.objects.all()
        return self.render_to_response(self.get_context_data(object_list=self.object_list, form=form))

    def get_context_data(self, **kwargs):
        context = super(ProfileList, self).get_context_data(**kwargs)
        if not self.profiles:
            self.profiles = Profile.objects.all()
        context.update({
            'profiles': self.profiles
        })
        return context

Below I added the FBV which does the job. How can I translate this functionality into a CBV? It seems to be so simple in function based views, but not in class based views.

def list_profiles(request):
    form_class = ProfileSearchForm
    model = Profile
    template_name = 'pages/profile/list_profiles.html'
    paginate_by = 10

    form = form_class(request.POST or None)
    if form.is_valid():
        profile_list = model.objects.filter(name__icontains=form.cleaned_data['name'])
    else:
        profile_list = model.objects.all()

    paginator = Paginator(profile_list, 10) # Show 10 contacts per page
    page = request.GET.get('page')
    try:
        profiles = paginator.page(page)
    except PageNotAnInteger:
        profiles = paginator.page(1)
    except EmptyPage:
        profiles = paginator.page(paginator.num_pages)

    return render_to_response(template_name, 
            {'form': form, 'profiles': suppliers,}, 
            context_instance=RequestContext(request))
Tynan answered 16/11, 2012 at 12:22 Comment(4)
Question, are you trying to display a value from a queryset based on form submission?Piteous
Have a look at this generic mixin approach: #7012273Biolysis
Give up on sticking with class-based views. Do it with function-based views to gain on speed and readability. You'd really not want to maintain this code after, lets say a year!Doggo
possible duplicate of Django class based view ListView with formMoquette
P
54

I think your goal is trying to filter queryset based on form submission, if so, by using GET :

class ProfileSearchView(ListView)
    template_name = '/your/template.html'
    model = Person

    def get_queryset(self):
        name = self.kwargs.get('name', '')
        object_list = self.model.objects.all()
        if name:
            object_list = object_list.filter(name__icontains=name)
        return object_list

Then all you need to do is write a get method to render template and context.

Maybe not the best approach. By using the code above, you no need define a Django form.

Here's how it works : Class based views separates its way to render template, to process form and so on. Like, get handles GET response, post handles POST response, get_queryset and get_object is self explanatory, and so on. The easy way to know what's method available, fire up a shell and type :

from django.views.generic import ListView if you want to know about ListView

and then type dir(ListView). There you can see all the method defined and go visit the source code to understand it. The get_queryset method used to get a queryset. Why not just define it like this, it works too :

class FooView(ListView):
    template_name = 'foo.html'
    queryset = Photo.objects.all()  # or anything

We can do it like above, but we can't do dynamic filtering by using that approach. By using get_queryset we can do dynamic filtering, using any data/value/information we have, it means we also can use name parameter that is sent by GET, and it's available on kwargs, or in this case, on self.kwargs["some_key"] where some_key is any parameter you specified

Piteous answered 23/11, 2012 at 11:44 Comment(2)
instead of using the try you could use get with a default value, i think it helps with readability. name = self.kargs.get('name', None) and then if name: # do some stuffCoulombe
how we can do the same if we have to query in multiple models and send the results?Covered
V
16

Well, I think that leaving validation to form is nice idea. Maybe not worth it in this particular case, because it is very simple form - but for sure with more complicated one (and maybe yours will grow also), so I would do something like:

class ProfileList(ListView):
    model = Profile
    form_class = ProfileSearchForm
    context_object_name = 'profiles'
    template_name = 'pages/profile/list_profiles.html'
    profiles = []


    def get_queryset(self):
        form = self.form_class(self.request.GET)
        if form.is_valid():
            return Profile.objects.filter(name__icontains=form.cleaned_data['name'])
        return Profile.objects.all()
Veridical answered 23/11, 2012 at 18:53 Comment(3)
i got an error that form_class is not defined, how to fix?Atone
Vic Nicethemer, you can define itDedicated
@VicNicethemer It should be form = self.form_class(self.request.GET).Expendable
E
16

This is similar to @jasisz 's approach, but simpler.

class ProfileList(ListView):
    template_name = 'your_template.html'
    model = Profile

    def get_queryset(self):
        query = self.request.GET.get('q')
        if query:
            object_list = self.model.objects.filter(name__icontains=query)
        else:
            object_list = self.model.objects.none()
        return object_list

Then all you have to do on the html template is:

<form method='GET'>
  <input type='text' name='q' value='{{ request.GET.q }}'>
  <input class="button" type='submit' value="Search Profile">
</form>
Euell answered 18/1, 2019 at 1:24 Comment(1)
can you please guide me with url here,Semipostal
M
4

This has been explained nicely on the generic views topic here Dynamic filtering.

You can do filtering through GET, I don't think you can use POST method for this as ListView is not inherited from edit mixings.

What you can do is:

urls.py

urlpatterns = patterns('', 
                (r'^search/(\w+)/$', ProfileSearchListView.as_view()),
              )

views.py

class ProfileSearchListView(ListView):
    model = Profile
    context_object_name = 'profiles'
    template_name = 'pages/profile/list_profiles.html'
    profiles = []

    def get_queryset(self):
         if len(self.args) > 0:
               return Profile.objects.filter(name__icontains=self.args[0])
         else:
               return Profile.objects.filter()
Maidservant answered 23/11, 2012 at 13:49 Comment(1)
Working link for dynamic filtering : docs.djangoproject.com/en/dev/topics/class-based-views/…Reenter
G
1

I think that the error you are getting is because your form doesn't require the name field. So, although the form is valid, the cleaned_data for your name field is empty.

These could be the problematic lines:

if form.is_valid():
    self.show_results = True
    self.profiles = Profile.objects.filter(name__icontains=form.cleaned_data['name'])

If I were you, I would try changing the line:

self.profiles = Profile.objects.filter(name__icontains=form.cleaned_data['name'])

to this:

self.profiles = Profile.objects.none()

If you stop receiving errors (and your template receives an empty object_list), the problem you have is what I said before: name field not required.

Let us know if this doesn't work!

Grievous answered 24/11, 2012 at 4:55 Comment(0)
T
1

Search on all fields in model

class SearchListView(ItemsListView):

# Display a Model List page filtered by the search query.

def get_queryset(self):
    fields = [m.name for m in super(SearchListView, self).model._meta.fields]
    result = super(SearchListView, self).get_queryset()
    query = self.request.GET.get('q')
    if query:
        result = result.filter(
            reduce(lambda x, y: x | Q(**{"{}__icontains".format(y): query}), fields, Q())
        )
    return result
Tent answered 16/10, 2017 at 10:22 Comment(0)
T
0
def get_queryset(self):
    query_name = self.request.GET.get('query', '')
    object_list = Product.objects.filter(
        Q(title__icontains=query_name) 
    )
    return object_list

<form action="" method="GET">
{% csrf_token %}
<input type="text" name="query" placeholder="Search keyword">
       <i class="ti-search"></i>
</form>
Tinny answered 21/4, 2022 at 21:53 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Raddie

© 2022 - 2024 — McMap. All rights reserved.