Django: Search form in Class Based ListView - django

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

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

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

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>

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

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!

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

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>

Related

Django - show to user an object corresponding to the current url

apologies for the misleading title; let's say I've got this kind of link:
<div class="col-md-3">
<a class="btn btn-success" href="{% url 'rental-create' car.pk %}">Order this car!</a>
</div>
which leads to a view I use to process ordering my cars
class RentalCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
form_class = RentalCreateForm
template_name = 'rentals/rental_create.html'
success_message = 'Created'
context_object_name = 'order'
def get_success_url(self):
return reverse('rental-detail', kwargs={'pk': self.object.pk})
def form_valid(self, form):
form.instance.profile = self.request.user.profile
car = Car.objects.get(pk=self.kwargs['pk'])
form.instance.car = car
return super().form_valid(form)
now I would like to simply use my object (car) pk in the template or directly in the form I've defined above.
As you can see, I've managed to access this variable in my form_valid method:
car = Car.objects.get(pk=self.kwargs['pk'])
however, this mechanism only ensures that after saving an instance to the database, the car would be the same as in the requested url.
My point is to use the car object (which corresponds to url like /rental/new/1/) in the template, or as the non-editable form field.
in brief - I would like to show user the current car he is ordering.
here is my forms.py
class RentalCreateForm(ModelForm):
class Meta:
model = Rental
fields = ('start_date', 'end_date', 'additional_info',)
exclude = ('profile', 'paid')
widgets = {
'start_date': DateInput(),
'end_date': DateInput()
}
Does Django provide a simple way to achieve this? Any help would be highly appreciated.
You can add extra context in the get_context_data method.
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['car'] = get_object_or_404(Car, pk=self.kwargs['pk']
return context
Note that I've used get_object_or_404 so that you get a 404 page when the car does not exist. If you used get(), then a CarDoesNotExist exception would cause a 500 server error.
Now you can access {{ car }} in the template.

How to return a query set in get_queryset() in Django

I'm currently trying to convert my FBV codes to CBV. get_context_data is working well by returning contexts that I put in. However, get_queryset() returns NOTHING for some reason. To double-check, I tried to print search_stores right before returning it and it printed the queryset that is supposed to be printed. However, when I printed it on Django template, by typing {{ search_stores }}, it shows nothing. Am I using get_queryset in a wrong way?
class SearchListView(ListView):
model = Store
template_name = 'boutique/search.html'
# paginate_by = 5
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_text'] = self.request.GET.get('search_text')
context['sorter'] = self.request.GET.get('sorter')
if not context['sorter']:
context['sorter'] = 'popularity'
return context
def get_queryset(self):
search_text = self.request.GET.get('search_text')
sorter = self.request.GET.get('sorter')
if search_text:
search_stores = Store.objects.filter(Q(businessName__icontains=search_text) | Q(mKey__icontains=search_text))
if sorter == 'businessName':
search_stores = search_stores.order_by(sorter)
else:
search_stores = search_stores.order_by(sorter).reverse()
else:
search_stores = ''
for store in search_stores:
store.mKey = store.mKey.split(' ')
print(search_stores)
return search_stores
Your queryset is accessible via the context_object_name.
By default it's object_list if you don't provide context_object_name
You can access the queryset in templates with object_list
If you want to change the name, change the context_object_name:
class SearchListView(ListView):
model = Store
template_name = 'boutique/search.html'
context_object_name = 'search_stores'
search_stores will be the variable accessible to loop through in templates

Class Based FormView

I have researched this issue for a couple of days and can't seem to find what I'm looking for exactly. I have searched ModelChoiceField as well as ChoiceField on StackOverflow as well as Google and there are many variations of my question but nothing exactly. In a nutshell, I am trying to use a Class Based FormView and then capture the user selection and pass it to a Class Based ListView. Here is my code.
Forms.Py
class BookByStatus(forms.Form):
dropdown = forms.ChoiceField(choices=[],required=False)
def __init__(self, user, *args, **kwargs):
super(BookByStatus, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
self.fields['dropdown'].choices =
Book.objects.values_list("author","author").distinct("Publisher")
The code above works fine, and shows me the output I'm looking for on my view. No issues there....Then I have my FormView...
class BookByStatusView(LoginRequiredMixin,FormView):
model = Book
form_class = BookByStatus
template_name = 'xyz123/publisher.html'
success_url = reverse_lazy('Book:book_by_list',kwargs=
{'dropdown':'dropdown'})
def get_form_kwargs(self):
kwargs = super(BookByStatusView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
self.request.POST['dropdown']
BookByStatusView = form.cleaned_data['dropdown']
return super(BookByStatusView, self).form_valid(form)
The code above works fine, but takes me to the ListView below which I can't seem to pass the dropdown value to....I've tried several different iterations of get_form_kwargs as well as changed my form to ModelChoiceField, but still can't seem to understand how to get a queryset based on the input from the user...
And finally the ListView...
class BookByStatusListView(LoginRequiredMixin,ListView):
model = Book
form_class = BookByStatus
context_object_name = 'book_list'
template_name = 'xyz123/book_by_status_list.html'
paginate_by = 15
def get_queryset(self, *kwargs):
form = self.form_class(self.request.GET)
dropdown = self.kwargs.get('dropdown', None)
if form.is_valid():
return Book.objects.filter(dropdown__icontains=form.
cleaned_data['dropdown'])
return Book.objects.all()
I'm trying to take the dropdown input from the FormView and then pass it to a list view using two separate views. I need to pass the value from the FormView to the ListView. I'm clear on how to get the data in the FormView in the ChoiceField, and how to display a ListView, but I can't seem to figure out how to pass the dropdown data from the FormView to the ListView. I can get the ListView to work, but only with the full queryset, not with a filtered one.
Here's the book model....
class Book(models.Model):
Author CHOICES = (
("New","New"),
("Old","Old"),
)
Author = models.CharField(choices=Author_CHOICES,max_length=10)
Here's the URL...
url(r'^book_by_list/(?
P<dropdown>\w+)/$',views.BookByStatusListView.as_view(),
name='book_by_list'),
Thanks in advance for any suggestions!
Updated Approach...Using request.session. My prior approach would not let me pass the value from the one view to the other, no matter how many get_context_data or get_form_kwargs combinations I tried. Based on the input I received, I began exploring the request.session approach and I've gotten much further. One last piece remains, getting the request.session value in my LISTVIEW so I can filter my querysets accordingly.
class BookByStatusView(LoginRequiredMixin,FormView):
model = Book
form_class = BookByStatus
template_name = 'xyz123/publisher.html'
success_url = reverse_lazy('Book:book_by_list')
def form_valid(self, form):
self.request.session['dropdown'] = form.cleaned_data['dropdown']
return super(BookByStatusView, self).form_valid(form)
def get_form_kwargs(self):
kwargs = super(BookByStatusView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
And in my html template, I leverage the request.session value as...
{{ request.session.dropdown }}
And I reverted the URL back to..
url(r'^book_by_list/$,views.BookByStatusListView.as_view(),
name='book_by_list'),
The last remaining piece is to figure out how to leverage the LISTVIEW with this approach.
My current Listview:
class BookByStatusListView(LoginRequiredMixin,ListView):
model = Book
form_class = BookByStatus
context_object_name = 'book_list'
template_name = 'xyz123/book_by_status_list.html'
paginate_by = 15
def get_queryset(self):
queryset = Book.objects.none()
dropdown = self.request.session.get('dropdown')
if dropdown == 'New':
queryset = Book.objects.all()
elif dropdown == 'Old':
queryset = Book.objects.none()
return queryset
I can't seem to figure out how to pass the dropdown value correctly to the ListView so the queryset is displayed properly. Based on my testing, I don't appear to be capturing dropdown properly in the get_queryset function. Any ideas?
I figured it out. I updated the get_queryset with the proper syntax. Thanks for all of the help to nudge me in the right direction. Last questions..is this the best way to pass a value from one view to another? Is there a better way to do this? Are there any concerns with this approach?
This doesn't work, because redirect creates a new request/response and data from previous are lost. If I understand what you want correctly, one of the options would be to save the drop-down value to session in BookByStatusView and then retrieve it in BookByStatusListView.
You save to session with:
request.session['dropdown_value'] = form.cleaned_data['dropdown']
and retrieve with:
dropdown_value = request.GET.get('dropdown_value')
Here is How to use session part of Django documentation.
EDIT: You can also pass the value as an url parameter like this:
author = 'michael cricthon'
title = 'kongo'
year = [1999, 2000, 2001]
type = ['electronic', 'print', 'hardcover', 'softcover']
params = '?author={}&title={}&&year={}&type={}'.format(
urllib.parse.quote_plus(author),
urllib.parse.quote_plus(title),
','.join(year),
','.join(type))
return HttpResponseRedirect(reverse('search') + params)
The link would look like this:
../search/?author=michael+crichton&title=kongo&year=1999,2000,2001&type=electronic,print,hardcover,softcover
You get parameters with
author = request.GET.get('author')
title = request.GET.get('title')
... etc.

Django ListView - Form to filter and sort

My Goal
A site that list all my Updates (model) in a table
Dont display all models at once (pagination - maybe 10 per page)
Filter and sort the list
My thoughts
I can use ListView to get a set of all my Updates
Use paginate_by = 10
Use a form to set order_by or filter in my QuerySet
My Problem
I am not sure how to add an form to modify my QuerySet with filter and sortings. My Idea was to modify the Query in get_queryset with additional filter and order_by.
My View
class MyView(ListView):
model = Update
template_name = "updates/update.html"
paginate_by = 10
def get_queryset(self):
return Update.objects.filter(
~Q(state=Update.STATE_REJECTED),
~Q(state=Update.STATE_CANCELED),
~Q(state=Update.STATE_FINISHED),
).order_by(
'planned_release_date'
)
My Idea
Something like this. I know it's not working like this ... just to illustrate
class MyView(ListView):
model = Update
template_name = "updates/update.html"
paginate_by = 10
def post(self, request, *args, **kwargs):
new_context = Update.objects.filter(
request.POST.get("filter"),
).order_by(
request.POST.get("sorting"),
)
def get_queryset(self):
return Update.objects.filter(
~Q(state=Update.STATE_REJECTED),
~Q(state=Update.STATE_CANCELED),
~Q(state=Update.STATE_FINISHED),
).order_by(
'planned_release_date'
)
You don't need post. Pass the filter value and order_by in the url for example:
.../update/list/?filter=filter-val&orderby=order-val
and get the filter and orderby in the get_queryset like:
class MyView(ListView):
model = Update
template_name = "updates/update.html"
paginate_by = 10
def get_queryset(self):
filter_val = self.request.GET.get('filter', 'give-default-value')
order = self.request.GET.get('orderby', 'give-default-value')
new_context = Update.objects.filter(
state=filter_val,
).order_by(order)
return new_context
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
context['filter'] = self.request.GET.get('filter', 'give-default-value')
context['orderby'] = self.request.GET.get('orderby', 'give-default-value')
return context
Make sure you give proper default value to filter and orderby
Example form (you can modify this to your need):
<form method="get" action="{% url 'update-list' %}">
<p>Filter: <input type="text" value={{filter}} name="filter"/></p>
<p>order_by: <input type="text" value={{orderby}} name="orderby"/></p>
<p><input type="submit" name="submit" value="submit"/></p>
</form>
I am wondering why nobody mentioned here this cool library: django-filter https://github.com/carltongibson/django-filter
you can define your logic for filtering very clean and get fast working forms etc.
demo here: https://stackoverflow.com/a/46492378/953553
I posted this elsewhere but I think this adds to the selected answer.
I think you would be better off doing this via get_context_data. Manually create your HTML form and use GET to retrieve this data. An example from something I wrote is below. When you submit the form, you can use the get data to pass back via the context data. This example isn't tailored to your request, but it should help other users.
def get_context_data(self, **kwargs):
context = super(Search, self).get_context_data(**kwargs)
filter_set = Gauges.objects.all()
if self.request.GET.get('gauge_id'):
gauge_id = self.request.GET.get('gauge_id')
filter_set = filter_set.filter(gauge_id=gauge_id)
if self.request.GET.get('type'):
type = self.request.GET.get('type')
filter_set = filter_set.filter(type=type)
if self.request.GET.get('location'):
location = self.request.GET.get('location')
filter_set = filter_set.filter(location=location)
if self.request.GET.get('calibrator'):
calibrator = self.request.GET.get('calibrator')
filter_set = filter_set.filter(calibrator=calibrator)
if self.request.GET.get('next_cal_date'):
next_cal_date = self.request.GET.get('next_cal_date')
filter_set = filter_set.filter(next_cal_date__lte=next_cal_date)
context['gauges'] = filter_set
context['title'] = "Gauges "
context['types'] = Gauge_Types.objects.all()
context['locations'] = Locations.objects.all()
context['calibrators'] = Calibrator.objects.all()
# And so on for more models
return context
This is how we do it, that way you get validation/type conversion as well:
class UnitList(PermissionRequiredMixin, ListView):
""" Class based view to show a list of all buildings for a specific user """
model = Unit
ordering = ['building', 'unit']
paginate_by = 100
# Access
permission_required = ['core.manager_perm']
raise_exception = True # If true, give access denied message rather than redirecting to login
def get_queryset(self):
try:
units = self.model.objects.filter(building__company=self.request.user.profile.company)
except Profile.DoesNotExist:
units = self.model.objects.none()
form = UnitSearchForm(self.request.GET)
if form.is_valid():
filters = {}
address = form.cleaned_data['address']
neighborhood = form.cleaned_data['neighborhood']
beds = form.cleaned_data['beds']
amenity = form.cleaned_data['amenity']
if address:
filters['building__street_index__istartswith'] = compute_street_address_index(address)
if neighborhood:
filters['building__neighborhood__icontains'] = neighborhood
if beds:
filters['beds'] = beds
if amenity:
filters['unit_amenities__name__iexact'] = amenity
units = units.filter(**filters)
return units.select_related('building').order_by(*self.ordering)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = UnitSearchForm(self.request.GET)
return context

'str' object has no attribute 'visible_fields'; django class based views

I've been struggling with class views all day after starting on them yesterday. My issue is constantly getting 'str' object has no attribute 'visible_fields', so the 'form' item below is not really a form:
template-
<form action="" method="post">
{% csrf_token %}
{{form|bootstrap}}
<input type="submit" name="submit" value="Add new article"/>
</form>
view-
class ArticleCreateView(CreateView):
model = Article
template_name = 'index/add_article.html'
form_class = ArticleForm
def post(self, request, *args, **kwargs):
article_form = self.get_form()
if article_form.is_valid():
article = article_form.save(commit=False)
title = article_form.cleaned_data['title']
url = article_form.cleaned_data['url']
title = process_title(url)
article.title = title
article.save()
return redirect("index:article_list")
else:
form = ArticleForm()
print type(form)
print dir(self)
return render(request, 'index/add_article.html')
The worst part is printing type(form) shows it is <class 'index.forms.ArticleForm'>. I'm trying to just have it redirect to the list view if the form saved, and replay the form with the error (You already have an article with that URL) if the form is bad. I heard class views are easier to work with and huge projects I've read through use them, but they really seem worse than the old views. I assume that's because I'm not using them well
Every example I've seen has a template getting a "form" somehow, like
class RangeCreateView(CreateView):
model = Range
template_name = 'dashboard/ranges/range_form.html'
form_class = RangeForm
def get_success_url(self):
if 'action' in self.request.POST:
return reverse('dashboard:range-products',
kwargs={'pk': self.object.id})
else:
msg = render_to_string(
'dashboard/ranges/messages/range_saved.html',
{'range': self.object})
messages.success(self.request, msg, extra_tags='safe noicon')
return reverse('dashboard:range-list')
def get_context_data(self, **kwargs):
ctx = super(RangeCreateView, self).get_context_data(**kwargs)
ctx['title'] = _("Create range")
return ctx
then like magic in range_form.html:
{% include "dashboard/partials/form_fields.html" with form=form %}
My issue here is I need to process the title of the form, with
def process_title(url):
def _search_for_title(url):
try:
r = requests.get(url)
content = r.text
t = html.document_fromstring(content)
return t.find(".//title").text
except IOError:
return None
title = _search_for_title(url)
return title or 'None'
This kind of ruins the purpose of a class based view. It seems I should be processing the title by overriding 'clean' in the form itself?
Otherwise, how can I make this view pass a form object, render it in the template, and just re-render the template if the form didn't pass?
And how can I access the form in the template?
Thank you
You do it by following the example view, rather than overriding post instead as you are doing.
In your case, simply displaying and processing a form is the default behaviour of a CreateView. So there is no need to override any methods at all. Your view should just be:
class ArticleCreateView(CreateView):
model = Article
template_name = 'index/add_article.html'
form_class = ArticleForm