31

The main view is a simple paginated ListView and I want to add a search form to it.

I thought something like this would do the trick:

class MyListView(ListView, FormView):
    form_class = MySearchForm
    success_url = 'my-sucess-url'
    model = MyModel
    # ...

But apparently I got it wrong .. and I can't find how to do it in the official documentation.

Suggestions ?

h3.
  • 10,688
  • 15
  • 51
  • 54

5 Answers5

32

These answers have helped so much to steer me in the right direction. Thank guys.

For my implementation I needed a form view that returned a ListView on both get and post. I don't like having to repeat the contents of the get function but it needed a couple of changes. The form is now available from get_queryset now too with self.form.

from django.http import Http404
from django.utils.translation import ugettext as _
from django.views.generic.edit import FormMixin
from django.views.generic.list import ListView

class FormListView(FormMixin, ListView):
    def get(self, request, *args, **kwargs):
        # From ProcessFormMixin
        form_class = self.get_form_class()
        self.form = self.get_form(form_class)

        # From BaseListView
        self.object_list = self.get_queryset()
        allow_empty = self.get_allow_empty()
        if not allow_empty and len(self.object_list) == 0:
            raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.")
                          % {'class_name': self.__class__.__name__})

        context = self.get_context_data(object_list=self.object_list, form=self.form)
        return self.render_to_response(context)

    def post(self, request, *args, **kwargs):
        return self.get(request, *args, **kwargs)


class MyListView(FormListView):
    form_class = MySearchForm
    model = MyModel
    # ...
AgentK
  • 665
  • 7
  • 8
  • 1
    Perfect! I had to swap the inheritance order tho (ListView, FormMixin) in order to keep pagination working. – laffuste Feb 05 '14 at 07:16
  • This is nice but what i should use in template to output all forms. I see only one instance of form which is the first element and objects_list that is models list. – Boris Davidov Apr 19 '15 at 18:12
  • Okay, I manage to get a form and a list on a page, thanks. But how can I process the search? I guess have to override `post(self, request, *args, **kwargs)` in MyListView. But how? Copy the code from get and do the search there? Sometimes my impression is that LisViews make things more complicated in the end than writing a simple view function. Or am I missing something? – Jens-Erik Weber Jul 04 '16 at 12:30
8

I've been seaching for a proper solution too. But I could not find any so had to come up with my own.

views.py

class VocationsListView(ListView):

    context_object_name = "vocations"
    template_name = "vocations/vocations.html"
    paginate_by = 10

    def get_queryset(self):
        get = self.request.GET.copy()
        if(len(get)):
            get.pop('page')
        self.baseurl = urlencode(get)
        model = Vocation
        self.form = SearchForm(self.request.GET)
        filters = model.get_queryset(self.request.GET)
        if len(filters):
            model = model.objects.filter(filters)
        else:
            model = model.objects.all()
        return model



def get_context_data(self):
    context = super(VocationsListView, self).get_context_data()
    context['form'] = self.form
    context['baseurl']= self.baseurl
    return context

models.py

class Vocation(models.Model):
    title = models.CharField(max_length = 255)
    intro = models.TextField()
    description = models.TextField(blank = True)
    date_created = models.DateTimeField(auto_now_add = True)
    date_modified = models.DateTimeField(auto_now = True)
    created_by = models.ForeignKey(User, related_name = "vocation_created")
    modified_by = models.ForeignKey(User, related_name = "vocation_modified")

    class Meta:
        db_table = "vocation"

    @property
    def slug(self):
        return defaultfilters.slugify(self.title)

    def __unicode__(self):
        return self.title

    @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

so basically I add this piece of code to every model class, where I want implement the search functionality. This is because filters for the every model have to be prepared explicitly

@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

it prepares the qset filter that I use to retrieve the data from the model

Nebulosar
  • 1,727
  • 3
  • 20
  • 46
Azamat Tokhtaev
  • 447
  • 3
  • 8
4

In Django 2.2 you can do this (it works fine at least with a get-request):

from django.views.generic import ListView
from django.views.generic.edit import FormMixin

from .models import MyModel
from .forms import MySearchForm

class ListPageView(FormMixin, ListView):
    template_name = 'property_list.html'
    model = MyModel
    form_class = MySearchForm
    queryset = MyModel.objects.all()

Use the FormMixin before the ListView. If you want to use the SearchForm in a TemplateView you can do this:

from django.views.generic.base import TemplateView
from django.views.generic.edit import FormMixin

from .models import MyModel
from .forms import MySearchForm

class HomePageView(FormMixin, TemplateView):
    template_name = 'home.html'
    form_class = MySearchForm
didierCH
  • 378
  • 2
  • 17
1

From previous answers, here's my take on the views I used in order to display the form on the same page as the ListView :

class IndexView(FormMixin, ListView):
    ''' Homepage: displays list of links, and a form used to create them '''
    template_name = "links/index.html"
    context_object_name = "links"
    form_class = LinkForm

    def get_queryset(self):
        return Links.objects.all()

def add_link(request):
    # Sole job of this function is to process the form when POSTed. 
    if request.method == "POST":
        form = LinkForm(request.POST)

        if form.is_valid():
            Links.objects.create(address=form.cleaned_data['address'])

        return HttpResponseRedirect('/')

Then, the last thing is to bind the add_link view function to the form's action url, and you are good to go I think.

lkostka
  • 711
  • 1
  • 5
  • 10
1

Adding forms to index and list views using mixins is covered in the the official documentation.

The documentation generally recommends against this approach. It suggests to instead simply write a bit more python, and code the view manually.

ArtOfWarfare
  • 20,617
  • 19
  • 137
  • 193
Owen Brown
  • 179
  • 9
  • It's worth noting that they recommend against it because it's complicated, not because of any inherent issues that will arise from doing it. They even provide example code for doing it, although their example is subclassing `(FormMixin, DetailView)`, not a `ListView`. – ArtOfWarfare Sep 30 '20 at 00:26