neurix
neurix

Reputation: 4316

Django: Search form in Class Based ListView

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))

Upvotes: 33

Views: 55088

Answers (7)

Houdaeyfha Alsalih
Houdaeyfha Alsalih

Reputation: 11

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>

Upvotes: 0

Otskimanot Sqilal
Otskimanot Sqilal

Reputation: 2402

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

Upvotes: 54

Rohan
Rohan

Reputation: 53316

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()

Upvotes: 5

sptramp
sptramp

Reputation: 946

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>

Upvotes: 16

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

Upvotes: 1

jasisz
jasisz

Reputation: 1298

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()

Upvotes: 16

marianobianchi
marianobianchi

Reputation: 8488

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!

Upvotes: 1

Related Questions