I'm am utilizing a formset to enable users subscribe to multiple feeds. I require a) Users chose a subscription by selecting a boolean field, and are also required to tag the subscription and b) a user must subscribe to an specified number of subscriptions.
Currently the below code is capable of a) ensuring the users tags a subscription, however some of my forms is_valid() are False and thus preventing my validation of the full formset. [edit] Also, the relevant formset error message fails to display.
Below is the code:
from django import forms
from django.forms.formsets import BaseFormSet
from tagging.forms import TagField
from rss.feeder.models import Feed
class FeedForm(forms.Form):
subscribe = forms.BooleanField(required=False, initial=开发者_运维技巧False)
tags = TagField(required=False, initial='')
def __init__(self, *args, **kwargs):
feed = kwargs.pop("feed")
super(FeedForm, self).__init__(*args, **kwargs)
self.title = feed.title
self.description = feed.description
def clean(self):
"""apply our custom validation rules"""
data = self.cleaned_data
feed = data.get("subscribe")
tags = data.get("tags")
tag_len = len(tags.split())
self._errors = {}
if feed == True and tag_len < 1:
raise forms.ValidationError("No tags specified for feed")
return data
class FeedFormSet(BaseFormSet):
def __init__(self, *args, **kwargs):
self.feeds = list(kwargs.pop("feeds"))
self.req_subs = 3 # TODO: convert to kwargs arguement
self.extra = len(self.feeds)
super(FeedFormSet, self).__init__(*args, **kwargs)
# WARNING! Using undocumented. see for details...
def _construct_form(self, i, **kwargs):
kwargs["feed"] = self.feeds[i]
return super(FeedFormSet, self)._construct_form(i, **kwargs)
def clean(self):
"""Checks that only a required number of Feed subscriptions are present"""
if any(self.errors):
# Do nothing, don't bother doing anything unless all the FeedForms are valid
return
total_subs = 0
for i in range(0, self.extra):
form = self.forms[i]
feed = form.cleaned_data
subs = feed.get("subscribe")
if subs == True:
total_subs += 1
if total_subs != self.req_subs:
raise forms.ValidationError("More subscriptions...") # TODO more informative
return form.cleaned_data
As requested, the view code:
from django.forms import formsets
from django.http import Http404
from django.http import HttpResponseRedirect
from django.shortcuts import render_to_response
from rss.feeder.forms import FeedForm
from rss.feeder.forms import FeedFormSet
from rss.feeder.models import Feed
FeedSet = formsets.formset_factory(FeedForm, FeedFormSet)
def feeds(request):
if request.method == "POST":
formset = create_feed_formset(request.POST)
if formset.is_valid():
# submit the results
return HttpResponseRedirect('/feeder/thanks/')
else:
formset = create_feed_formset()
return render_to_response('feeder/register_step_two.html', {'formset': formset})
def create_feed_formset(data=None):
"""Create and populate a feed formset"""
feeds = Feed.objects.order_by('id')
if not feeds:
# No feeds found, we should have created them
raise Http404('Invalid Step')
return FeedSet(data, feeds=feeds) # return the instance of the formset
Any help would be appreciated.
Ps. For full disclosure, this code is based on http://google.com/search?q=cache:rVtlfQ3QAjwJ:https://www.pointy-stick.com/blog/2009/01/23/advanced-formset-usage-django/+django+formset
[Solved] See solution below.
Solved. Below is a quick run through of the solution.
Reporting the error required manipulating and formating a special error message. In the source code for formsets I found the errors that apply to a whole form are known as non_form_errors and produced a custom error based on this. [note: I couldn't find any authoritive documentation on this, so someone might know a better way]. The code is below:
def append_non_form_error(self, message):
errors = super(FeedFormSet, self).non_form_errors()
errors.append(message)
raise forms.ValidationError(errors)
The formsets clean method also needed a few tweaks. Basically it checks the if the forms is bound (empty ones aren't, hence is_valid is false in the question) and if so accesses checks there subscribe value.
def clean(self):
"""Checks that only a required number of Feed subscriptions are present"""
count = 0
for form in self.forms:
if form.is_bound:
if form['subscribe'].data:
count += 1
if count > 0 and count != self.required:
self.append_non_form_error("not enough subs")
Some might wonder why I choose to access the value using the form['field_name'].data format. This allows us to retrieve the raw value and always get a count on subscriptions, allowing me to return all relevant messages for the entire formset, i.e. specific problems with individual forms and higher level problems (like number of subscriptions), meaning that the user won't have to resubmit the form over and over to work through the list of errors.
Finally, I was missing one crucial aspect of my template, the {{ formset.non_form_errors }} tag. Below is the updated template:
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<form action="." method="post">
{{ formset.management_form }}
{{ formset.non_form_errors }}
<ol>
{% for form in formset.forms %}
<li><p>{{ form.title }}</p>
<p>{{ form.description }}</p>
{{ form.as_p }}
</li>
{% endfor %}
</ol>
<input type="submit">
</form>
{% endblock %}
I made attempt to circumvent my problem...it is not a good solution, it's very much so a hack. It allows people to proceed if they subscribe to the required number of feeds (in the case below more than 1), however if less than the required number of feeds, it fails to show the error message raised.
def clean(self):
count = 0
for i in range(0, self.extra):
form = self.forms[i]
try:
if form.cleaned_data:
count += 1
except AttributeError:
pass
if count > 1:
raise forms.ValidationError('not enough subscriptions')
return form.cleaned_data
I do use {{ formset.management_form }} in my template so as far as I know the error should display. Below my template in case I'm misguided.
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<form action="." method="post">
{{ formset.management_form }}
<ol>
{% for form in formset.forms %}
{{ form.as_p }}
</li>
{% endfor %}
</ol>
<input type="submit">
</form>
{% endblock %}
精彩评论