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))
Scott Woodall
  • 10,456
  • 6
  • 39
  • 37
neurix
  • 4,126
  • 6
  • 46
  • 71
  • 1
    Question, are you trying to display a value from a queryset based on form submission? – Otskimanot Sqilal Nov 23 '12 at 10:51
  • Have a look at this generic mixin approach: http://stackoverflow.com/questions/7011773/how-to-create-a-filter-form-for-a-class-based-generic-object-list-in-django – Scott Woodall Nov 24 '12 at 22:28
  • 1
    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! – seler Nov 24 '12 at 23:35
  • possible duplicate of [Django class based view ListView with form](http://stackoverflow.com/questions/6406553/django-class-based-view-listview-with-form) – laffuste Feb 05 '14 at 04:37

7 Answers7

53

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

GriceTurrble
  • 38
  • 1
  • 3
Otskimanot Sqilal
  • 2,332
  • 4
  • 21
  • 25
  • 9
    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 stuff` – agmezr Jul 21 '17 at 18:11
  • how we can do the same if we have to query in multiple models and send the results? – umair mehmood Feb 02 '22 at 13:56
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()
Dustin Wyatt
  • 4,046
  • 5
  • 31
  • 60
jasisz
  • 1,288
  • 8
  • 9
15

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>
sptramp
  • 897
  • 2
  • 10
  • 29
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()
pyjavo
  • 1,598
  • 2
  • 23
  • 41
Rohan
  • 52,392
  • 12
  • 90
  • 87
  • Working link for dynamic filtering : https://docs.djangoproject.com/en/dev/topics/class-based-views/generic-display/#dynamic-filtering – EastSw Jun 19 '18 at 07:07
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!

Aamir Rind
  • 38,793
  • 23
  • 126
  • 164
marianobianchi
  • 8,238
  • 1
  • 20
  • 24
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
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>
  • 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](/help/how-to-answer). – Community Apr 22 '22 at 00:08