views:

1108

answers:

2

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%3ArVtlfQ3QAjwJ%3Ahttps%3A//www.pointy-stick.com/blog/2009/01/23/advanced-formset-usage-django/+django+formset

[Solved] See solution below.

A: 

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 %}
A: 

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 %}