Django 1.11.4
I have build a search form with method="get".
Search form has a lot of forms.
Then this input values are transmitted as get parameters in url.
The problem is how to get pagination. The database contains thousands of objects. Pagination is necessary.
This is what the documentation tells us:
https://docs.djangoproject.com/en/1.11/topics/pagination/#using-paginator-in-a-view
It suggests like this:
previous
But this will ruin all the get parameters.
What I have managed to invent is:
previous
This works. But this is goofy. If one switches pages forward and backward, it produces urls ending like this:
page=2&page=3&page=2
I have had a look at how Google managed this problem. In the middle of the url they have start=30. And change this parameter: start=20, start=40. So, they switch.
Could you help me understand how preserve get parameters and switch pages in Django? In an elegant way, of course.
The generic solution is to define a "custom template tag" (a function) which keeps the complete URL but updates the GET parameters you pass to it.
After registration, you can use this function in your templates:
previous
To define and register the custom template tag, include this code in a python file:
from django import template
register = template.Library()
#register.simple_tag(takes_context=True)
def query_transform(context, **kwargs):
query = context['request'].GET.copy()
for k, v in kwargs.items():
query[k] = v
return query.urlencode()
*Thanks to Ben for the query_transform code. This is an adapation for python 3 from his code.
Why this method is better than reconstructing the URLs manually:
If you later decide that you need additional parameters in your URLs: 1. you don't have to update all the links in your templates. 2. You don't need to pass all the params required for recostructing the URL to the templates.
Typically, to preserve GET parameters you simply re-write them manually. There shouldn't be many cases where having to do this will matter.
&page={{page}}&total={{total}}
You can abstract this away into a template include or a custom template tag.
https://docs.djangoproject.com/en/1.11/howto/custom-template-tags/
Additionally, you could create a string filter that takes URL params as a string as well as a dict of values you want to change. The filter could then parse the params string, update the value, then recombine the string back into URL params.
{{ request.get_full_path | update_param:'page=8' }}
This is what I did and find easier. Many not be better approach though!
param = ""
if search_programs:
qs = qs.filter(title__icontains=search_programs)
param = param + f"&search_programs={search_programs}"
if level:
qs = qs.filter(level__icontains=level)
param = param + f"&level={level}"
if category:
qs = qs.filter(sector__icontains=category)
param = param + f"&category={category}"
paginator = Paginator(qs, 16)
page_number = self.request.GET.get('page')
page_obj = paginator.get_page(page_number)
context = {
'page_obj': page_obj,
"param": param
}
return render(self.request, self.template_name, context)
# in html
next
Subclassing
I solved this problem by subclassing Django's Paginator class and adding the stuff I needed there.
You need to override the init method to allow yourself to pass the request and form objects from the view, so that you can retrieve the request.GET parameters and compare them to form.fields. You want to only allow GET parameter names that are predefined in your form. Otherwise you remove them.
Once you have subclassed the Pagination class, you can use it in your views.py files, passing the extra request and form parameters to it. Then you can render your template as suggested in the Django documentation, but now you have access to the paginator.first_params, paginator.last_params, page.previous_params, and page.next_params variables that you can use.
pagination.py
import urllib
from django.core.paginator import Paginator as DjangoPaginator, EmptyPage
class Paginator(DjangoPaginator):
def __init__(self, *args, **kwargs):
request = kwargs.pop('request', None)
form = kwargs.pop('form', None)
super().__init__(*args, **kwargs)
self.params = {key: value for key, value in request.GET.items() if key in form.fields.keys()}
self.first_params = urllib.parse.urlencode({**self.params, 'page': 1})
self.last_params = urllib.parse.urlencode({**self.params, 'page': self.num_pages})
def get_page(self, *args, **kwargs):
page = super().get_page(*args, **kwargs)
try:
page.previous_page_number = page.previous_page_number()
except EmptyPage:
page.previous_page_number = None
try:
page.next_page_number = page.next_page_number()
except EmptyPage:
page.next_page_number = None
page.previous_params = urllib.parse.urlencode({**self.params, 'page': page.previous_page_number})
page.next_params = urllib.parse.urlencode({**self.params, 'page': page.next_page_number})
return page
views.py
from .pagination import Paginator
paginator = Paginator(queryset, per_page, request=request, form=form)
page = paginator.get_page(page_number)
pagination.html
{% if page %}
<nav id="pagination">
{% if page.has_previous %}
« First
Previous
{% endif %}
<span class="current">
Page {{ page.number }} of {{ page.paginator.num_pages }}.
</span>
{% if page.has_next %}
Next
Last »
{% endif %}
</nav>
{% endif %}
Related
I'm trying to send an object obtained with a ModelChoiceField into a view via urls and views parameters by I don't think that's the right method. I don't know if I have to use POST or GET method.
When I use the GET method, the object is displayed in the url but the view stay the same.
With the POST method, django send errors messages about parameters.
EDIT : I don't remeber the exact messages, I can't recover them for now but as I remember they said something like Reverse for argument not found
My form
class IndividuForm(forms.Form):
individu = forms.ModelChoiceField(queryset=Individu.objects.all()
Here's the view with the form
def index(request):
individu_form = IndividuForm(request.GET or None)
if individu_form.is_valid():
individu_id = individu_form.cleaned_data['individu'].id
HttpResponseRedirect('%i/raison/' % individu_id)
return render(request, 'derog_bv/index.html', {'individu_form':individu_form})
The template where the form is displayed
<form action="{% url 'index' <!-- Errors when I put parameters here --> %} method="<!-- GET or POST -->">
{% csrf_token %}
{{ form }}
<input type="submit">Suivant</input>
</form>
The view where I want to get my object
def raison(request, """ error with this parameter : individu_id"""):
individu = get_object_or_404(Individu, pk=individu_id)
URLs
urlpatterns = [
path('', views.index, name='index'),
path('<int:individu_id>/raison/', views.raison, name='raison'),
]
Ok, so:
1/ you definitly want to use the GET method for your form (you're not submitting data for processing / server state change)
2/ I don't know why you'd want to pass parameters (nor which parameters FWIW) to the {% url %} tag in your index template - you're submitting the form to the index view, which doesn't expect any additional param. Actually you could just remove the action attribute of the HTML form tag since the default (submitting to the current url) is what you want.
3/ your views.raison prototype must match the url definition, so it has to be:
def raison(request, individu_id):
# code here
4/ in your index view:
HttpResponseRedirect('%i/raison/' % individu_id)
you want to build the url using django.core.urlresolvers.reverse instead :
from django.core.urlresolvers import reverse
def index(request):
# code here
if ...:
next = reverse("raison", kwargs={"individu_id": individu_id})
return HttpResponseRedirect(next)
or - even easier - just use the redirect shortcut:
from django.shortcuts import redirect
def index(request):
# code here
if ...:
return redirect("raison", individu_id=individu_id)
There might be other issues with your code but from the infos you posted, those hints should at least put you back on tracks.
I am using Django for develop a website. The website is intended to use to search information stored in a MySQL database.
This is the current basic flow of the web site.
1) index.html - this has a form to select an option
2) according the option, users will redirect to search.html (include a form)
3) once the user provides the criteria, the result will be displayed in reply.html
In my views.py , I have two functions.
from django.shortcuts import render
from website.models import WebsiteRepository
from .forms import SearchForm
from .forms import SelectTypeForm
def Search(request):
if request.method == 'POST':
#do something
return render(request, 'reply.html', {'env_dict':env_dict})
else:
#do something
return render(request, 'search.html', context = context)
def index(request):
if request.method =='POST':
#do something
return render(request, 'search.html', context = context)
else:
#do something
return render(request, 'index.html', context= context)
When I go to index.html page, I can select a option and it will direct me to search.html. After, I fill the form there and submit, it wont give me the reply.html page.
I have a feeling that, I could make this work by changing urls.py.
from django.urls import path
from website import views
urlpatterns = [
path('', views.index, name='index'),
#path('search/', view.Search, name ='Search')
]
I tried to google it. But its too much details and Iam kind of lost.
Do any of you guys know how to achieve this?
Thanks
search.html
{% extends "base_generic.html" %}
{% block content %}
<h3>Welcome to search information Repository</h3>
<form method="post">
{% csrf_token %}
{{form.as_p}}
<button type = 'submit'>submit</button>
</form>
{% endblock %}
index.html
{% block content %}
<h3>Welcome to information Repository</h3>
<form method="post">
{% csrf_token %}
{{form.as_p}}
<button type = 'submit'>submit</button>
</form>
just for clarify things more, ill add the forms.py too
from django import forms
from .models import WebsiteRepository
class SearchForm(forms.Form):
websiterepository = WebsiteRepository
env_indicators = websiterepository.objects.filter (key_aspect='Environmental').values_list('repo_id','indicator')
indicator = forms.ChoiceField(choices=env_indicators,label = 'Indicator' )
OPTIONS = (('2000','2000'),('2001','2001'),('2002','2002'), ('2003','2003'),('0000','0000'),)
year = forms.ChoiceField(choices=OPTIONS)
class SelectTypeForm(forms.Form):
OPTIONS = (('1', 'Envirnmental Indicators'),('2','Economic Indicators'),('3','Social Indicators'),)
types = forms.ChoiceField(choices=OPTIONS)
Your code is wrong on many points.
First thing first: for a search, you want a GET request, not a POST (POST is for updating the server's state - adding or updating your database mostly). This is the semantically correct method (since you want to GET data), and it will allow a user to bookmark the url.
Second point: you don't want to submit the search form to the index view but to the search view. No need for redirects etc, just use the {% url %} templatetag to fill the action attribute of your form (you of course need to have a 'Search' url in your urls.py):
<form method="get" action="{% url 'Search' %}">
{% csrf_token %}
{{form.as_p}}
<button type = 'submit'>submit</button>
</form>
if you want to have this form on more than one page (which is often the case for search forms), use an inclusion tag tha will take care of creating an unbound SearchForm and render the template fragment.
Then in your search view, you only want GET requests, and do not use two different templates, this will only lead to useless duplication.
def Search(request):
form = SearchForm(request.GET)
# use the form's data - if any - to get search results
# and put those results (even if empty) in you context
return render(request, 'reply.html', {'env_dict':env_dict})
And finally, your search form is totally broken:
class SearchForm(forms.Form):
# this is totally useless
websiterepository = WebsiteRepository
# this will only be evaluated once at process startup, so you will
# get stale data in production - and probably different data
# per process, in a totally unpredictable way.
# You need to either put this in the form's __init__ or wrap it
# in a callable and pass this callable
env_indicators = websiterepository.objects.filter (key_aspect='Environmental').values_list('repo_id','indicator')
indicator = forms.ChoiceField(choices=env_indicators,label = 'Indicator' )
# are you going to manually add a new year choice every year ???
OPTIONS = (('2000','2000'),('2001','2001'),('2002','2002'), ('2003','2003'),('0000','0000'),)
year = forms.ChoiceField(choices=OPTIONS)
For the "indicators" ChoiceField you want something like:
def get_indicators_choices():
return Websiterepository.objects.filter (key_aspect='Environmental').values_list('repo_id','indicator')
class SearchForm(forms.Form):
# IMPORTANT : we are NOT calling the function here, just
# passing it (python functions are objects) to the field, which
# will call it everytime the form is instanciated, so you don't
# have stale data
indicator = forms.ChoiceField(
choices=get_indicator_choices,
label='Indicator')
As a last note: be consistent with your namings (ie why name one view in all lower (index) and capitalize the other (Search) ? Whichever convention you choose (I strongly suggest respecting pep8 here), at least stick to it for the whole project.
The problem is that code is not redirecting to /search, instead rendering search.html after post from index.html.
Try doing like-
views.py-
#your code
def index(request):
#do something
if request.method == 'POST':
return redirect('Search')
else:
#render index.html
def search(request):
#do something
if request.method == 'POST':
#render reply.html
else:
#render search.html
Another way to achieve this is if you specify action in your form so that form posts on /search.
search.html
<form method="post" action="/search">
{% csrf_token %}
{{form.as_p}}
<button type = 'submit'>submit</button>
</form>
So basically i have form on my homepage that asks users to choose two cities : where they are now, and where they want to go. I display all the available options with ModelChoiceField() easily, but when i try to use user's choices to make arguments for url, i get NoReverseMatch. I did a little research and found out due to the fact that at the time when page is loaded, user hasn't chosen anything, so there are no arguments. After that, i took different approach - i tried to set /search/ as url for the form. There, i extracted user's choices and tried to redirect back to the main url with these two arguments. Error still persists
Traceback Url :
http://dpaste.com/34E3S2V
Here's my forms.py :
class RouteForm(forms.Form):
location = forms.ModelChoiceField(queryset=Location.objects.all())
destination = forms.ModelChoiceField(queryset=Destination.objects.all())
Here's my template :
<p> From where to where ? </p>
<form action="{% url 'listings:search' %}" method="POST">
{{ form.as_p }}
{% csrf_token %}
<input type="submit" value="Let's go!">
</form>
My urls.py :
urlpatterns = [
path('', views.index, name="index"),
path('<location>/<destination>', views.route, name="route"),
path('search/', views.search, name="search")
]
and views.py :
def index(request):
form = forms.RouteForm()
listings = Listing.objects.all()
context = {"listings" : listings, "form" : form }
def route(request, location, destination):
current_location = Location.objects.get(city=location)
future_destination = Destination.objects.get(city=destination)
context = {"current_location" : current_location, "future_destination" : future_destination}
return render(request, 'listings/route.html', context)
def search(request, location, destination):
chosen_location = Location.objects.get(pk=request.POST['location'])
chosen_destination = Destination.objects.get(pk=request.POST['destination'])
return HttpResponseRedirect(reverse('listings:route', args=[chosen_location, chosen_destination]))
What am i missing?
You need to show the full traceback in your question. Actually, the code you've shown wouldn't give that error; instead you would get a TypeError for the search view.
Nevertheless, you have quite a few things wrong here.
Firstly, you need to decide how you want to represent those fields in the URL. You can't just put a Location object in a URL. Do you want to use numeric IDs, or string slugs? Assuming you want to use slugs, your URL would be:
path('<slug:location>/<slug:destination>', views.route, name="route"),
Secondly, you shouldn't have location and destination as parameters to the search function. They aren't being passed in the URL, but you in the POST data.
Next, you need to actually use the Django form you've defined, and get the values from that form cleaned_data. Using the form - in particular calling its is_valid() method - ensures that the user actually chooses options from the fields. So the search function needs to look like this:
def search(request):
if request.method == 'POST':
form = RouteForm(request.POST)
if form.is_valid():
chosen_location = form.cleaned_data['location']
chosen_destination = form.cleaned_data['destination']
return HttpResponseRedirect(reverse('listings:route', args=[chosen_location.city, chosen_destination.city]))
else:
form = RouteForm()
return render(request, 'search.html', {'form': form})
I have a search form
class PostSearchForm(forms.Form):
keyword = forms.CharField(label=_('Search'))
def __init__(self,*args,**kwargs):
self.request = kwargs.pop('request', None)
super(PostSearchForm,self).__init__(*args,**kwargs)
raise Exception(self.request) # This returns none
categories = Category.objects.filter(status=True)
self.fields['categories'] = forms.ModelChoiceField(queryset=categories, widget=forms.SelectMultiple(attrs={'class': 'some-class'}))
Whenever search is made I have a build a form with default value as what they have searched, so what I tried is get the url parameters in form init and set the value but it was returning None due to some mistake. Can any one tell me where I am wrong
Updated Question
views.py
def get(self,request,category,*args,**kwargs):
search_form = PostSearchForm()
return render(request,self.template_name,{'search_form':search_form})
Template
<form method="get" action="." id="searchForm">
{{ search_form.keyword }} # When searched then the value should be searched term
{{ search_form.categories }}
</form>
URL
http://example.com/?keyword=test
In the form I need to show test as value
Nothing can get into an object unless you pass it there. If you want the request in your object, then pass it:
search_form = PostSearchForm(request=request)
However this will not help you in any way at all and I'm confused about why you thought it would. If you want to pass in initial data for your form, do that:
search_form = PostSearchForm(initial={'categories': categories, 'keyword': keyword})
I am new to the Django web framework.
I have a template that displays the list of all objects. I have all the individual objects listed as a link (object title), clicking on which I want to redirect to another page that shows the object details for that particular object.
I am able to list the objects but not able to forward the object/object id to the next template to display the details.
views.py
def list(request):
listings = listing.objects.all()
return render_to_response('/../templates/listings.html',{'listings':listings})
def detail(request, id):
#listing = listing.objects.filter(owner__vinumber__exact=vinumber)
return render_to_response('/../templates/listing_detail.html')
and templates as:
list.html
{% for listing in object_list %}
<!--<li> {{ listing.title }} </li>-->
{{ listing.title}}<br>
{% endfor %}
detail.html
{{ id }}
The variables that you pass in the dictionary of render_to_response are the variables that end up in the template. So in detail, you need to add something like {'listing': MyModel.objects.get(id=vinumber)}, and then the template should say {{ listing.id }}. But hat'll crash if the ID doesn't exist, so it's better to use get_object_or_404.
Also, your template loops over object_list but the view passes in listings -- one of those must be different than what you said if it's currently working....
Also, you should be using the {% url %} tag and/or get_absolute_url on your models: rather than directly saying href="{{ listing.id }}", say something like href="{% url listing-details listing.id %}", where listing-details is the name of the view in urls.py. Better yet is to add a get_absolute_url function to your model with the permalink decorator; then you can just say href="{{ listing.get_absolute_url }}", which makes it easier to change your URL structure to look nicer or use some attribute other than the database id in it.
You should check the #permalink decorator. It enables you to give your models generated links based on your urls pattern and corresponding view_function.
For example:
# example model
class Example(models.Model):
name = models.CharField("Name", max_length=255, unique=True)
#more model fields here
#the permalink decorator with get_absolute_url function
#models.permalink
def get_absolute_url(self):
return ('example_view', (), {'example_name': self.name})
#example view
def example_view(request, name, template_name):
example = get_object_or_404(Example, name=name)
return render_to_response(template_name, locals(),
context_instance=RequestContext(request))
#example urls config
url(r'^(?P<name>[-\w]+)/$', 'example_view', {'template_name': 'example.html'}, 'example_view')
Now you can do in your templates something like this:
<a href={{ example.get_absolute_url }}>{{ example.name }}</a>
Hope this helps.
In your detail method, just pass the listing into your template like so:
def detail(request, id):
l = listing.objects.get(pk=id)
return render_to_response('/../templates/listing_detail.html', {'listing':l})