Flexible pagination in Django - django

I'd like to implement pagination such that I can allow the user to choose the number of records per page such as 10, 25, 50 etc. How should I go about this? Is there an app I can add onto my project to do this?
Thanks

Django has a Paginator object built into core. It's a rather straightforward API to use. Instantiate a Paginator class with two arguments: the list and the number of entries per "page". I'll paste some sample code at the bottom.
In your case you want to allow the user to choose the per-page count. You could either make the per-page count part of the URL (ie. your/page/10/) or you could make it a query string (ie. your/page/?p=10).
Something like...
# Assuming you're reading the Query String value ?p=
try:
per_page = int(request.REQUEST['p'])
except:
per_page = 25 # default value
paginator = Paginator(objects, per_page)
Here's some sample code from the Django doc page for the Paginator to better see how it works.
>>> from django.core.paginator import Paginator
>>> objects = ['john', 'paul', 'george', 'ringo']
>>> p = Paginator(objects, 2)
>>> p.count
4
>>> p.num_pages
2
>>> p.page_range
[1, 2]
>>> page1 = p.page(1)
>>> page1
<Page 1 of 2>
>>> page1.object_list
['john', 'paul']

google on "django pagination" and make sure to use "covering index" in your SQL for efficient query.

T. Stone's answer covers most of what I was going to say. I just want to add that you can use pagination in Generic Views. In particular, you may find django.views.generic.list_detail.object_list useful.
You can write a small wrapper function that gets the number of objects to display per page from the request object, then calls object_list.
def paginated_object_list(request, page):
my_queryset=MyModel.objects.all()
#Here's T. Stone's code to get the number of items per page
try:
per_page = int(request.REQUEST['p'])
except:
per_page = 25 # default value
return object_list(request, queryset=my_queryset,
paginate_by=per_page, page=page)
Then, the context for your template will contain the variables,
paginator: An instance of django.core.paginator.Paginator.
page_obj: An instance of django.core.paginator.Page.
and you can loop through page_obj to display the objects for that page.

What does need? Well.
You can add custom control for change_list.html, for pagination block for example.
This will be reload list page with get parameter per_page for example with certain value onchange event.
For your adminModel you must override changelist_view method where you must handle get parameter and set this value as list_per_page field value.
def changelist_view(self, request):
if request.GET.get('per_page') and int(
request.GET.get('per_page')) in CHANGELIST_PERPAGE_LIMITS:
self.list_per_page = int(request.GET.get('per_page'))
else:
self.list_per_page = 100
extra_context = {'changelist_perpage_limits': CHANGELIST_PERPAGE_LIMITS,
'list_per_page': self.list_per_page}
return super(mymodelAdmin, self).changelist_view(request, extra_context)
I use extra_context for access to this values into template. Maybe there is more neat approach to access i don't know :-)

Related

How can I return a new non-repeating item from a list in Django while keeping track of previous items?

I'm working on an app where a user can select a category, which will return a random selection from that category. The main functionality I'm trying to implement is once an item is selected, it can no longer be randomly selected in session.
For example, we have 3 categories of photos: landscape, urban, and portraits, each with 5 photos. A user selects urban, is then redirected to a details page with a random photo from urban category. He can either refresh the page or click a button to get a new photo from that category. When that category is out of new photos, he is redirected home.
I am able to get my random item from the selected category through converting a queryset to a list, but the data isn't persisting. On every refresh the list I have resets, thus a previously selected photo can come up again, ignoring the the fact that I removed the item from the list after it was selected.
Here's the views.py with the function responsible for this:
def randomPhoto(request, pk, **kwargs):
# queryset to get all photos from selected category
gallery = list(Photos.objects.filter(id=pk)
.values_list("partof__category", flat=True))
# select random photo from list
last = len(gallery) -1
randomInt = random.randint(0, last)
randomPic = gallery[randomInt]
gallery.remove(randomPic)
if len(gallery) == 0:
return render(request, 'gallery/category_select.html')
photoDetails = {
'category' : Category.objects.get(id=pk),
'author' : Author.objects.get(tookin__category=randomPic),
'uploadedPhoto' : 'http://localhost:8000/media/' +
str(Photo.objects.get(category=randomPic).photoUpload),
'randomPic' : randomPic,
}
return render(request, 'gallery/random_photo.html', {'photoDetails': photoDetails})
The functionality I'm looking for is (where each number is an object/item in list):
User selects urban category:
urban has the following items: [1, 2, 3, 4, 5]
random [3] selected from urban
urban now has [1, 2, 4, 5]
User refreshes:
random [4] selected
urban now has [1, 2, 5]
User refreshes:
random [2] selected
urban now has [1, 5]
User refreshes:
random [5] selected
urban now has [1]
User refreshes:
random [1] selected
urban now has []
User is redirected home
I believe my problem lies in having to configure either sessions or cookies to have the data persist in an anonymous session. Eventually I will be adding a Users module so each user will have their browsing history saved but for now I want it to work just as an anonymous user.
I've tried adding SESSION_SAVE_EVERY_REQUEST = True to settings.py and placing request.session.modified = True in my views.py, though I doubt I'm implementing them properly. I've read some SO questions on sessions and cookies but wasn't able to find something to work with my issue. The Django Sessions Doc seemed interesting but overwhelming. I'm not sure where to begin trying to experiment with wiring the sessions aspect together.
I am wondering if there's an easy/Pythonic way to achieve having my web app give me a non-repeating item from a list until none are left within the session.
Your issue is that your variable is not carried over from one request to the next. The best way to do this would be to use request.session = ... to set a variable, and then check it later and perform actions. Here is an example that you can expand on to make it to your liking:
import random
from django.shortcuts import redirect
class TestView(View):
def get(self, request, *args, **kwargs):
gallery = request.session.get('gallery', None)
if (type(gallery) is list) and (len(gallery) == 0): # When list is empty, clear session & then redirect
del request.session['gallery']
request.session.modified = True
return redirect('<your_redirect_url>')
if gallery is None: # If first visit to page, create gallery list
gallery = list(models.Photos.objects.all().values_list("partof__category", flat=True))
# select random photo from list
last = len(gallery) -1
randomInt = random.randint(0, last)
randomPic = gallery[randomInt]
gallery.remove(randomPic)
request.session['gallery'] = gallery
return render(request, 'test.html', {})

Django URLs: Can't get the keyword arg into my view?

The URL pattern having issues is:
url(r'^$', business_list, name='business_list_home'),
url(r'^(?P<param>\w+)$', business_list, name='business_list_results'),
My view is:
#render_to('app/business_list.html')
def business_list(request, param=None):
queryset = Business.objects.all()
search_form = SearchForm
print request.GET
if param in request.GET:
param = request.GET.get('param')
if queryset.filter(city__iexact=param).exists():
queryset = queryset.filter(city__iexact=param)
elif queryset.filter(category__iexact=param).exists():
queryset = queryset.filter(category__iexact=param)
print queryset
else:
queryset = None
print queryset
return {'business_list': queryset, 'search_form': search_form}
Essentially, I don't understand why when I go to /Miami I don't have access to it via request.GET['param'] in the view? print request.GET prints <QueryDict: {}>
The reason I want to do this is to have a nice URL scheme for displaying results of businesses for the city or category (and that's why I check if it's a city or category in the view too) in the url. Let me know if there's a better way to accomplish this like
url(r'^$', business_list),
url(r'^(?P<city>\w+)$', business_list),
url(r'^(?P<category>\w+)$', business_list),
url(r'^(?P<category>\w+)/(?P<city>\w+)$', business_list),
Thanks for any help!
business_list is a function that takes a request and a parameter called "param" (should probably give it a better name as to make it a little less generic). In Django, the URL routes can define what values get passed to the parameters of the view functions.
In the situation above, when a user goes to /Miami, Django will try to match the string "Miami" with any of the regular expressions defined in the URL routes. In this case, the matching expression is ^(?P<param>\w+)$. When this match is made, the string Miami is captured into param.
Django will then call business_list(request, param="Miami"). Note that there were no query parameters passed in the URL (e.g., /Miami?color=blue).
The issue in the code you've written above is that you're checking to see not if param exists, but rather that param was passed in the query parameters. Try going to the URL /Miami?param=test and it'll probably work the way that you expected.
The real fix here is to not reference request.GET, because using GET parameters is exactly what you're trying to avoid. So, instead of
if param in request.GET:
param = request.GET.get('param')
if queryset.filter(city__iexact=param).exists():
queryset = queryset.filter(city__iexact=param)
elif queryset.filter(category__iexact=param).exists():
queryset = queryset.filter(category__iexact=param)
print queryset
Just do:
if param:
if queryset.filter(city__iexact=param).exists():
queryset = queryset.filter(city__iexact=param)
elif queryset.filter(category__iexact=param).exists():
queryset = queryset.filter(category__iexact=param)

Prevent django admin from running SELECT COUNT(*) on the list form

Every time I use Admin to list the entries of a model, the Admin count the rows in the table. Worse yet, it seems to be doing so even when you are filtering your query.
For instance if I want to show only the models whose id is 123, 456, 789 I can do:
/admin/myapp/mymodel/?id__in=123,456,789
But the queries ran (among others) are:
SELECT COUNT(*) FROM `myapp_mymodel` WHERE `myapp_mymodel`.`id` IN (123, 456, 789) # okay
SELECT COUNT(*) FROM `myapp_mymodel` # why???
Which is killing mysql+innodb. It seems that the problem is partially acknowledged in this ticket, but my issue seems more specific since it counts all the rows even if it is not supposed to.
Is there a way to disable that global rows count?
Note: I am using django 1.2.7.
Django 1.8 lets you disable this by setting show_full_result_count = False.
https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.show_full_result_count
Okay, I think I found a solution. As Peter suggested, the best approach is to work on the count property and it can be done by overriding it with custom query set (as seen in this post) that specialises the count with an approximate equivalent:
from django.db import connections, models
from django.db.models.query import QuerySet
class ApproxCountQuerySet(QuerySet):
"""Counting all rows is very expensive on large Innodb tables. This
is a replacement for QuerySet that returns an approximation if count()
is called with no additional constraints. In all other cases it should
behave exactly as QuerySet.
Only works with MySQL. Behaves normally for all other engines.
"""
def count(self):
# Code from django/db/models/query.py
if self._result_cache is not None and not self._iter:
return len(self._result_cache)
is_mysql = 'mysql' in connections[self.db].client.executable_name.lower()
query = self.query
if (is_mysql and not query.where and
query.high_mark is None and
query.low_mark == 0 and
not query.select and
not query.group_by and
not query.having and
not query.distinct):
# If query has no constraints, we would be simply doing
# "SELECT COUNT(*) FROM foo". Monkey patch so the we
# get an approximation instead.
cursor = connections[self.db].cursor()
cursor.execute("SHOW TABLE STATUS LIKE %s",
(self.model._meta.db_table,))
return cursor.fetchall()[0][4]
else:
return self.query.get_count(using=self.db)
Then in the admin:
class MyAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyAdmin, self).queryset(request)
return qs._clone(klass=ApproxCountQuerySet)
The approximate function could mess things up on page number 100000, but it is good enough for my case.
I found Nova's answer very helpful, but i use postgres. I modified it slightly to work for postgres with some slight alterations to handle table namespaces, and slightly different "detect postgres" logic.
Here's the pg version.
class ApproxCountPgQuerySet(models.query.QuerySet):
"""approximate unconstrained count(*) with reltuples from pg_class"""
def count(self):
if self._result_cache is not None and not self._iter:
return len(self._result_cache)
if hasattr(connections[self.db].client.connection, 'pg_version'):
query = self.query
if (not query.where and query.high_mark is None and query.low_mark == 0 and
not query.select and not query.group_by and not query.having and not query.distinct):
# If query has no constraints, we would be simply doing
# "SELECT COUNT(*) FROM foo". Monkey patch so the we get an approximation instead.
parts = [p.strip('"') for p in self.model._meta.db_table.split('.')]
cursor = connections[self.db].cursor()
if len(parts) == 1:
cursor.execute("select reltuples::bigint FROM pg_class WHERE relname = %s", parts)
else:
cursor.execute("select reltuples::bigint FROM pg_class c JOIN pg_namespace n on (c.relnamespace = n.oid) WHERE n.nspname = %s AND c.relname = %s", parts)
return cursor.fetchall()[0][0]
return self.query.get_count(using=self.db)
The Nova's solution (ApproxCountQuerySet) works great, however in newer versions of Django queryset method got replaced with get_queryset, so it now should be:
class MyAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(MyAdmin, self).get_queryset(request)
return qs._clone(klass=ApproxCountQuerySet)
If this is a serious problem you may have to take Drastic Actions™.
Looking at the code for a 1.3.1 install, I see that the admin code is using the paginator returned by get_paginator(). The default paginator class appears to be in django/core/paginator.py. That class has a private value called _count which is set in Paginator._get_count() (line 120 in my copy). This in turn is used to set a property of the Paginator class called count. I think that _get_count() is your target. Now the stage is set.
You have a couple of options:
Directly modify the source. I do not recommend this, but since you seem to be stuck at 1.2.7 you may find that it is the most expedient. Remember to document this change! Future maintainers (including possibly yourself) will thank you for the heads up.
Monkeypatch the class. This is better than direct modification because a) if you don't like the change you just comment out the monkeypatch, and b) it is more likely to work with future versions of Django. I have a monkeypatch going back over 4 years because they still have not fixed a bug in the template variable _resolve_lookup() code that doesn't recognize callables at the top level of evaluation, only at lower levels. Although the patch (which wraps the method of a class) was written against 0.97-pre, it still works at 1.3.1.
I did not spend the time to figure out exactly what changes you would have to make for your problem, but it might be along the lines of adding a _approx_count member to appropriate classes class META and then testing to see if that attr exists. If it does and is None then you do the sql.count() and set it. You might also need to reset it if you are on (or near) the last page of the list. Contact me if you need a little more help on this; my email is in my profile.
It is possible to change the default paginator used by the admin class. Here's one that caches the result for a short period of time: https://gist.github.com/e4c5/6852723
I managed to create a custom paginator that shows the current page numbe, a next button and a show full count link. It allows for the use of the original paginator if needed.
The trick used is to take per_page + 1 elements from db in order to see if we have more elements and then provide a fake count.
Let's say that we want the the third page and the page has 25 elements => We want object_list[50:75]. When calling Paginator.count the queryset will be evaluated for object_list[50:76](note that we take 75+1 elements) and then return either the count as 76 if we got 25+1 elements from db or 50 + the number of elements received if we didn't received 26 elements.
TL;DR:
I've created a mixin for the ModelAdmin:
from django.core.paginator import Paginator
from django.utils.functional import cached_property
class FastCountPaginator(Paginator):
"""A faster paginator implementation than the Paginator. Paginator is slow
mainly because QuerySet.count() is expensive on large queries.
The idea is to use the requested page to generate a 'fake' count. In
order to see if the page is the final one it queries n+1 elements
from db then reports the count as page_number * per_page + received_elements.
"""
use_fast_pagination = True
def __init__(self, page_number, *args, **kwargs):
self.page_number = page_number
super(FastCountPaginator, self).__init__(*args, **kwargs)
#cached_property
def count(self):
# Populate the object list when count is called. As this is a cached property,
# it will be called only once per instance
return self.populate_object_list()
def page(self, page_number):
"""Return a Page object for the given 1-based page number."""
page_number = self.validate_number(page_number)
return self._get_page(self.object_list, page_number, self)
def populate_object_list(self):
# converts queryset object_list to a list and return the number of elements until there
# the trick is to get per_page elements + 1 in order to see if the next page exists.
bottom = self.page_number * self.per_page
# get one more object than needed to see if we should show next page
top = bottom + self.per_page + 1
object_list = list(self.object_list[bottom:top])
# not the last page
if len(object_list) == self.per_page + 1:
object_list = object_list[:-1]
else:
top = bottom + len(object_list)
self.object_list = object_list
return top
class ModelAdminFastPaginationMixin:
show_full_result_count = False # prevents root_queryset.count() call
def changelist_view(self, request, extra_context=None):
# strip count_all query parameter from the request before it is processed
# this allows all links to be generated like this parameter was not present and without raising errors
request.GET = request.GET.copy()
request.GET.paginator_count_all = request.GET.pop('count_all', False)
return super().changelist_view(request, extra_context)
def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
# use the normal paginator if we want to count all the ads
if hasattr(request.GET, 'paginator_count_all') and request.GET.paginator_count_all:
return Paginator(queryset, per_page, orphans, allow_empty_first_page)
page = self._validate_page_number(request.GET.get('p', '0'))
return FastCountPaginator(page, queryset, per_page, orphans, allow_empty_first_page)
def _validate_page_number(self, number):
# taken from Paginator.validate_number and adjusted
try:
if isinstance(number, float) and not number.is_integer():
raise ValueError
number = int(number)
except (TypeError, ValueError):
return 0
if number < 1:
number = 0
return number
The pagination.html template:
{% if cl and cl.paginator and cl.paginator.use_fast_pagination %}
{# Fast paginator with only next button and show the total number of results#}
{% load admin_list %}
{% load i18n %}
{% load admin_templatetags %}
<p class="paginator">
{% if pagination_required %}
{% for i in page_range %}
{% if forloop.last %}
{% fast_paginator_number cl i 'Next' %}
{% else %}
{% fast_paginator_number cl i %}
{% endif %}
{% endfor %}
{% endif %}
{% show_count_all_link cl "showall" %}
</p>
{% else %}
{# use the default pagination template if we are not using the FastPaginator #}
{% include "admin/pagination.html" %}
{% endif %}
and templatetags used:
from django import template
from django.contrib.admin.views.main import PAGE_VAR
from django.utils.html import format_html
from django.utils.safestring import mark_safe
register = template.Library()
DOT = '.'
#register.simple_tag
def fast_paginator_number(cl, i, text_display=None):
"""Generate an individual page index link in a paginated list.
Allows to change the link text by setting text_display
"""
if i == DOT:
return '… '
elif i == cl.page_num:
return format_html('<span class="this-page">{}</span> ', i + 1)
else:
return format_html(
'<a href="{}"{}>{}</a> ',
cl.get_query_string({PAGE_VAR: i}),
mark_safe(' class="end"' if i == cl.paginator.num_pages - 1 else ''),
text_display if text_display else i + 1,
)
#register.simple_tag
def show_count_all_link(cl, css_class='', text_display='Show the total number of results'):
"""Generate a button that toggles between FastPaginator and the normal
Paginator."""
return format_html(
'<a href="{}"{}>{}</a> ',
cl.get_query_string({PAGE_VAR: cl.page_num, 'count_all': True}),
mark_safe(f' class="{css_class}"' if css_class else ''),
text_display,
)
You can use it this way:
class MyVeryLargeModelAdmin(ModelAdminFastPaginationMixin, admin.ModelAdmin):
# ...
Or an even simpler version that does not show the Next button and Show the total number of results :
from django.core.paginator import Paginator
from django.utils.functional import cached_property
class FastCountPaginator(Paginator):
"""A faster paginator implementation than the Paginator. Paginator is slow
mainly because QuerySet.count() is expensive on large queries.
The idea is to use the requested page to generate a 'fake' count. In
order to see if the page is the final one it queries n+1 elements
from db then reports the count as page_number * per_page + received_elements.
"""
use_fast_pagination = True
def __init__(self, page_number, *args, **kwargs):
self.page_number = page_number
super(FastCountPaginator, self).__init__(*args, **kwargs)
#cached_property
def count(self):
# Populate the object list when count is called. As this is a cached property,
# it will be called only once per instance
return self.populate_object_list()
def page(self, page_number):
"""Return a Page object for the given 1-based page number."""
page_number = self.validate_number(page_number)
return self._get_page(self.object_list, page_number, self)
def populate_object_list(self):
# converts queryset object_list to a list and return the number of elements until there
# the trick is to get per_page elements + 1 in order to see if the next page exists.
bottom = self.page_number * self.per_page
# get one more object than needed to see if we should show next page
top = bottom + self.per_page + 1
object_list = list(self.object_list[bottom:top])
# not the last page
if len(object_list) == self.per_page + 1:
object_list = object_list[:-1]
else:
top = bottom + len(object_list)
self.object_list = object_list
return top
class ModelAdminFastPaginationMixin:
show_full_result_count = False # prevents root_queryset.count() call
def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
page = self._validate_page_number(request.GET.get('p', '0'))
return FastCountPaginator(page, queryset, per_page, orphans, allow_empty_first_page)
def _validate_page_number(self, number):
# taken from Paginator.validate_number and adjusted
try:
if isinstance(number, float) and not number.is_integer():
raise ValueError
number = int(number)
except (TypeError, ValueError):
return 0
if number < 1:
number = 0
return number

Need clarification on using Django 1.4 Form Wizards, specifically pre-filling and saving

We are building a wizard using Django 1.4's new form wizard functionality.
The docs on this are very terse and we can't find any advanced examples. We are using a named step wizard (needed to support a listview/datagrid we use) and a session backend.
The wizard is meant to edit roles and linked rights and is built to provide both add and edit functionality. We do this by asking the user in the first step if he/she wants to add or edit.
The next step depends on that choice;
If the user wants to edit, there is a search screen, followed by a listview/datagrid that displays results. The user can then select one of the results and goes to a details-screen, followed by a FilteredSelectMultiple page, allowing him/her to link rights to this role.
If the user wants to add a new role, the search and results screens are skipped and the user goes directly to the details screen, followed by the link-screen.
It all works pretty well, using a condition_dict in urls.py, but we are wondering a couple of things about the general functionality:
When a specific pre-existing role is selected, how can we fill the details and the link-screen with the corresponding data?
Do we instantiate a roles-object and pass it somehow to the two forms, if so, where do we instantiate it and do we need to do that for every form separately (which seems a bit over the top)?
When saving, is it common practice to create another instance of a role object, add the form data to it and save, or can we re-use the object used in the forms somehow?
We have tried overloading get_form_instance to return instances of roles, and we have looked at instance_dict in the docs, but it feels like the wrong approach and there are no examples to be found online, and we're not even sure these are used to pre-fill data or even if we're on the right track.
Logically, I would say in the step that selects an existing role, I need to fill the wizard-variables using an instance of the chosen object, and these get displayed in the forms. At the end of the wizard we reverse the process and get all data from the wizard-variables and add them to a newly instantiated roles-object and save it. Ideally this instance will determine itself if it needs to perform an INSERT or an UPDATE, depending on whether or not the promary key is filled.
If anyone can provide an example, or a nudge in the right direction, it would be very much appreciated.
The code of the wizardview class in views.py is below:
class RolesWizard(NamedUrlSessionWizardView):
def get_template_names(self):
# get template for each step...
if self.steps.current == 'choice':
return 'clubassistant/wizard_neworeditrole.html'
if self.steps.current == 'search':
return 'clubassistant/wizard_searchrole.html'
if self.steps.current == 'results':
return 'clubassistant/wizard_pickrole.html'
if self.steps.current == 'details':
return 'clubassistant/wizard_detailsrole.html'
elif self.steps.current == 'rights':
return 'clubassistant/wizard_roles.html'
def get_context_data(self, form, **kwargs):
# get context data to be passed to the respective templates
context = super(RolesWizard, self).get_context_data(form=form, **kwargs)
# add the listview in the results screen
if self.steps.current == 'results':
# get search text from previous step
cleaned_data = self.get_cleaned_data_for_step('search')
table = RolesTable(Roles.objects.filter(
role_name__contains=cleaned_data['searchrole'])
)
RequestConfig(self.request, paginate={
"per_page": 4,
}).configure(table)
# add the listview with results
context.update({'table': table})
# add a role instance based on the chosen primary key
if self.steps.current == 'rights':
cleaned_data = self.get_cleaned_data_for_step('results')
role_id = cleaned_data['role_uuid']
role = get_object_or_404(Roles, pk=role_id)
context.update({'role': role})
return context
def done(self, form_list, **kwargs):
# this code is executed when the wizard needs to be completed
# combine all forms into a single dictionary
wizard = self.get_all_cleaned_data()
if wizard.get("neworeditrole")=="add":
role = Roles()
else:
role = get_object_or_404(Roles, pk=wizard.get("role_uuid"))
# many-to-many rights/roles
role.role_rights_new_style.clear()
for each_right in wizard.get('role_rights_new_style'):
RightsRoles.objects.create(role=role, right=each_right,)
# other properties
for field, value in self.get_cleaned_data_for_step('details'):
setattr(role, field, value)
role.save()
# return to first page of wizard...
return HttpResponseRedirect('/login/maintenance/roles/wizard/choice/')
For future googlers:
I had some success with using get_form() because it is called before a form is rendered. Start with a couple of ModelForms:
class Wizard1(models.ModelForm):
class Meta:
model = MyModel
fields = ('field0', 'model0')
class Wizard2(models.ModelForm):
class Meta:
model = MyModel
excludes = ('field0', 'model0')
Then, in your SessionWizardView:
class MyWizard(SessionWizardView):
def get_form(self, step=None, data=None, files=None):
form = super(ExtensionCreationWizard, self).get_form(step, data, files)
if step is not None and data is not None:
# get_form is called for validation by get_cleaned_data_for_step()
return form
if step == "0":
# you can set initial values or tweak fields here
elif step == "1":
data = self.get_cleaned_data_for_step('0')
if data is not None:
form.fields['field1'].initial = data.get('field0')
form.fields['field2'].widget.attrs['readonly'] = True
form.fields['field3'].widget.attrs['disabled'] = True
form.fields['model1'].queryset = Model1.objects.filter(name="foo")
return form
The action is all in step 1. You request validated data from step 0 (which triggers another call to get_form() for step 0, so be careful) and then you can access any values that were set in step 0.
I threw in a couple of examples of settings you can change on the fields. You can update a queryset to limit the values in a ChoiceField, or re-display a value again but make it read-only. One caveat I noticed... readonly does not work on ChoiceField. You can make it disabled, but then the value is not propagated when you submit the form.
Let's see if I can help. I did a form wizard that adds steps depending on the answers. At each step I save all forms in a session variable, like so:
def process_step(self, request, form, step):
request.session['form_list'] = self.form_list
request.session['initial'] = self.initial
Then, each time that view is rendered, I instantiate a new form wizard with all the previous data:
def dynamic_wizard(request):
if not request.session.get('form_list'):
form = Wizard([Form1])
else:
form = Wizard(request.session.get('form_list'), initial = request.session['initial'])
return form(context=RequestContext(request), request=request)

Help understanding a Django view

I am trying to follow the code listed on https://github.com/alex/django-ajax-validation/blob/master/ajax_validation/views.py
I have been able to understand a small chunk of it. I have added comments stating my understanding of what is happening.
I would really appreciate some assistance on questions I listed in comments next to the lines I couldn't quite follow.
def validate(request, *args, **kwargs):
# I thing it is some sort of initializations but I cannot really understand what's happening
form_class = kwargs.pop('form_class')
defaults = {
'data': request.POST
}
extra_args_func = kwargs.pop('callback', lambda request, *args, **kwargs: {})
kwargs = extra_args_func(request, *args, **kwargs)
defaults.update(kwargs)
form = form_class(**defaults)
if form.is_valid(): #straightforward, if there is no error then the form is valid
data = {
'valid': True,
}
else:
# if we're dealing with a FormSet then walk over .forms to populate errors and formfields
if isinstance(form, BaseFormSet): #I cannot really understand what is BaseFromSet
errors = {}
formfields = {}
for f in form.forms: # I am guessing that this is for when there are multiple form submitted for validation
for field in f.fields.keys(): # I think he is looping over all fields and checking for error. what does add_prefix () return? and what is formfields[]?
formfields[f.add_prefix(field)] = f[field]
for field, error in f.errors.iteritems():
errors[f.add_prefix(field)] = error
if form.non_form_errors():
errors['__all__'] = form.non_form_errors() # what is the '__all__'?
else:
errors = form.errors
formfields = dict([(fieldname, form[fieldname]) for fieldname in form.fields.keys()])
# if fields have been specified then restrict the error list
if request.POST.getlist('fields'): # I am having a hard time understanding what this if statement does.
fields = request.POST.getlist('fields') + ['__all__']
errors = dict([(key, val) for key, val in errors.iteritems() if key in fields])
final_errors = {} # here the author of this code totally lost me.
for key, val in errors.iteritems():
if '__all__' in key:
final_errors[key] = val
elif not isinstance(formfields[key].field, forms.FileField):
html_id = formfields[key].field.widget.attrs.get('id') or formfields[key].auto_id
html_id = formfields[key].field.widget.id_for_label(html_id)
final_errors[html_id] = val
data = {
'valid': False or not final_errors,
'errors': final_errors,
}
json_serializer = LazyEncoder() # Why does the result have to be returned in json?
return HttpResponse(json_serializer.encode(data), mimetype='application/json')
validate = require_POST(validate) # a decorator that requires a post to submit
LazyEncoder
class LazyEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, Promise):
return force_unicode(obj)
return obj
form_class = kwargs.pop('form_class')
This is simply pulling the keyword argument, form_class, that was passed in via the URL conf.
(r'^SOME/URL/$', 'ajax_validation.views.validate',
{'form_class': ContactForm}, # this keyword argument.
'contact_form_validate')
BaseFormSet is simply the formset class doing the work behind the scenes. When you don't know, search the source! grep -ri "baseformset" . It's an invaluable tool.
Take a look at at django.forms.formsets to see how formset_factory produces new "formset" classes based on the BaseFormSet, hence the factory part!
I am guessing that this is for when there are multiple form submitted for validation
Yes, that's exactly what a formset is for (dealing with multiple forms)
I think he is looping over all fields and checking for error. what does add_prefix () return? and what is formfields[]?
Yes, that would be looping through the field names.
add_prefix() is for prefixing form field names with a specific form. Because a formset repeats form elements multiple times, each field needs a unique prefix, such as 0-field1, 1-field1, etc.
formfields is just an empty dictionary defined a few lines above.
what is the 'all'?
__all__ is defined at the top of django.forms.forms
NON_FIELD_ERRORS = '__all__'
It's just what non field specific errors (such as constraints across 2 fields) are stored under in the errors dictionary as opposed to errors[fieldname].
I am having a hard time understanding what this if statement does.
The author has left a note:
# if fields have been specified then restrict the error list
if request.POST.getlist('fields'):
It's checking if you specified any specific fields to validate in your URLConf, this is not django but ajax_validation.
You can see that he's overwriting his errors dictionary based on only the fields specified, thus passing on the validation only for those fields.
errors = dict([(key, val) for key, val in errors.iteritems() if key in fields])
here the author of this code totally lost me.
The author has mapped a custom errors and fields dictionary to specific field names with prefixes, (as opposed to the usual FormSet with each form having its own errors dictionary, unaware of the formset itself) which he presumably uses in the AJAX response to validate all fields.
Normally, you can iterate over a formset and go through the errors on a form by form basis, but not so if you need to validate all of them through ajax.
The line pulling html_id should be straight forward most of the time, but it's there because form widgets CAN add interesting things to the end of the ID's based on whether or not the widget is a radio select for example.
From source comments :
# RadioSelect is represented by multiple <input type="radio"> fields,
# each of which has a distinct ID. The IDs are made distinct by a "_X"
# suffix, where X is the zero-based index of the radio field. Thus,
# the label for a RadioSelect should reference the first one ('_0').
Why does the result have to be returned in json?
Because it's an ajax request and javascript easily eats json.
2- could you go through these lines of code...
extra_args_func = kwargs.pop('callback', lambda request, *args, **kwargs: {})
Either return a keyword argument named 'callback' (which if passed in, is supposed to be a function that accepts request and return a dictionary), and if it wasn't, return a lambda function that only returns an empty dictionary.
I'm not sure what the specific use is for the extra context. You could use it to run arbitrary snippets of code without modifying or subclassing ajax_validation...
It might help you to run this code, and put a debugger breakpoint in somewhere so you can step through and examine the variables and methods. You can do this by simply putting this line where you want to break:
import pdb; pdb.set_trace()
and you will be dumped into the debugger in the console.