8

I'm looking for an updated version of these Django SuperForms. Can't seem to get it to work in Django 1.2. In particular, I'd like it to work with ModelForms.

My use case is almost identical to his; I have an Address model that I'd like to use as a sub-form in various places. It's a pain to try and combine everything in the view func.

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • What is SuperForms supposed to do? – Nick Presta Jul 07 '10 at 05:10
  • Do you basically want a `ModelForm` that traverses foreign keys? – Sam Dolan Jul 07 '10 at 16:14
  • @sdolan: Something like that, yeah. Well, not *just* traverse foreign keys, because then the subforms would have to be solely based on the automatically generated form produced from the model. I'd like to be able to actually choose another form (or model form) to embed. – mpen Jul 07 '10 at 16:38
  • @Nick: The linked google groups post sums it up pretty well: http://groups.google.com/group/django-developers/browse_thread/thread/b39ec8990e756b53 The particular example I had in mind, is that users have usernames, emails, phone numbers, etc. But they also have addresses. Addresses are used throughout the project, and are there own model, so I'd like to embed this form within the user registration so that I don't have to try and validate two forms separately in the view and then combine the resulting models. – mpen Jul 07 '10 at 16:41

1 Answers1

6

I've updated superforms.py to work w/1.2, and attached it to the ticket you linked to: http://code.djangoproject.com/attachment/ticket/3706/superform.2.py

There's a project I'm working on that could benefit from this, so I figured I'd spend the time and help you out as well.

Keep in mind that I just got this to work w/1.2, and didn't really try to clean up the internals. Now that I have test cases proving the API, I can go back and clean that up later.

If you're using it with ModelForms and you'd like the save() functionality you'll have to override the method on your SuperForm class.

I currently have it locally in my "common" repository w/~90% code coverage that covers multiple SubForms, mixed SubForms & declared forms, and ModelForms. I've included the test cases below (beware it uses my TestCaseBase class, but this should give you the gist of the API). Let me know if you have questions, or any areas I missed.

from django_common.forms.superform import SuperForm, SubForm
from django_common.test import TestCaseBase
from django import forms

class WhenSuperFormsIsUsedWithOnlySubForms(TestCaseBase):
    def get_superform_with_forms(self, post_data=None):
        class AddressForm(forms.Form):
            street = forms.CharField(max_length=255)
            city = forms.CharField(max_length=255)

        class BusinessLocationForm(forms.Form):
            phone_num = forms.CharField(max_length=255)

        class TestSuperForm(SuperForm):
            address = SubForm(AddressForm)
            business_location = SubForm(BusinessLocationForm)

        return TestSuperForm(data=post_data)

    def should_not_be_valid_with_no_data(self):
        tsf = self.get_superform_with_forms()
        self.assert_false(tsf.is_valid())

    def should_have_two_sub_forms(self):
        tsf = self.get_superform_with_forms()
        self.assert_equal(len(tsf.base_subforms),  2)
        self.assert_equal(len(tsf.forms), 2)

    def should_display_as_ul(self):
        tsf = self.get_superform_with_forms()
        self.assert_equal(tsf.as_ul(), '<li><label for="id_business_location-phone_num">Phone num:</label> <input id="id_business_location-phone_num" type="text" name="business_location-phone_num" maxlength="255" /></li>\n<li><label for="id_address-street">Street:</label> <input id="id_address-street" type="text" name="address-street" maxlength="255" /></li>\n<li><label for="id_address-city">City:</label> <input id="id_address-city" type="text" name="address-city" maxlength="255" /></li>')

    def should_display_as_table(self):
        tsf = self.get_superform_with_forms()
        self.assert_equal(tsf.as_table(), '<tr><th><label for="id_business_location-phone_num">Phone num:</label></th><td><input id="id_business_location-phone_num" type="text" name="business_location-phone_num" maxlength="255" /></td></tr>\n<tr><th><label for="id_address-street">Street:</label></th><td><input id="id_address-street" type="text" name="address-street" maxlength="255" /></td></tr>\n<tr><th><label for="id_address-city">City:</label></th><td><input id="id_address-city" type="text" name="address-city" maxlength="255" /></td></tr>')

    def should_display_as_p(self):
        tsf = self.get_superform_with_forms()
        self.assert_equal(tsf.as_p(), '<p><label for="id_business_location-phone_num">Phone num:</label> <input id="id_business_location-phone_num" type="text" name="business_location-phone_num" maxlength="255" /></p>\n<p><label for="id_address-street">Street:</label> <input id="id_address-street" type="text" name="address-street" maxlength="255" /></p>\n<p><label for="id_address-city">City:</label> <input id="id_address-city" type="text" name="address-city" maxlength="255" /></p>')

    def should_display_as_table_with_unicode(self):
        tsf = self.get_superform_with_forms()
        self.assert_equal(tsf.__unicode__(), tsf.as_table())

    def should_be_valid_if_good_data(self):
        data = {
            'business_location-phone_num' : '8055551234',
            'address-street' : '1234 Street Dr.',
            'address-city' : 'Santa Barbara',
        }
        tsf = self.get_superform_with_forms(data)
        self.assert_true(tsf.is_valid())
        self.assert_equal(tsf.cleaned_data['business_location']['phone_num'],
                          '8055551234')
        self.assert_equal(tsf.cleaned_data['address']['street'], '1234 Street Dr.')
        self.assert_equal(tsf.cleaned_data['address']['city'], 'Santa Barbara')

    def should_be_invalid_if_missing_data(self):
        data = {
            'business_location-phone_num' : '8055551234',
            'address-street' : '1234 Street Dr.',
        }
        tsf = self.get_superform_with_forms(data)
        self.assert_false(tsf.is_valid())

        self.assert_false(tsf.errors['business_location'])
        self.assert_true(tsf.errors['address'])
        self.assert_equal(tsf.errors['address']['city'], ['This field is required.'])

    def should_be_invalid_if_invalid_data(self):
        data = {
            'business_location-phone_num' : '8055551234',
            'address-street' : '1234 Street Dr.',
            'address-city' : '',
        }
        tsf = self.get_superform_with_forms(data)
        self.assert_false(tsf.is_valid())


class WhenSuperformsIsUsedWithSubFormsAndDeclaredFields(TestCaseBase):
    """Some basic sanity checks that working with fields combined with SubForms works."""
    def get_superform_with_forms(self, post_data=None):
        class AddressForm(forms.Form):
            street = forms.CharField(max_length=255)

        class TestSuperForm(SuperForm):
            name = forms.CharField(max_length=255)
            address = SubForm(AddressForm)

        return TestSuperForm(data=post_data)

    def should_not_be_valid_with_no_data(self):
        tsf = self.get_superform_with_forms()
        self.assert_false(tsf.is_valid())

    def should_have_two_forms_and_a_single_subform(self):
        tsf = self.get_superform_with_forms()
        self.assert_equal(len(tsf.base_subforms),  1)
        self.assert_equal(len(tsf.forms), 2)

    def should_print_as_table(self):
        tsf = self.get_superform_with_forms()
        self.assert_equal(tsf.as_table(), '<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" maxlength="255" /></td></tr>\n<tr><th><label for="id_address-street">Street:</label></th><td><input id="id_address-street" type="text" name="address-street" maxlength="255" /></td></tr>')

    def should_validate_when_fields_exist(self):
        data = {
            'name': 'Sam',
            'address-street': 'Some Street',
        }
        tsf = self.get_superform_with_forms(data)
        self.assert_true(tsf.is_valid())

        self.assert_equal(tsf.cleaned_data['name'], 'Sam')
        self.assert_equal(tsf.cleaned_data['address']['street'], 'Some Street')

    def should_not_validate_with_invalid_data(self):
        data = {
            'name': '',
            'address-street': 'Some Street',
        }
        tsf = self.get_superform_with_forms(data)
        self.assert_false(tsf.is_valid())

        self.assert_equal(tsf.errors['name'], ['This field is required.'])



class WhenSuperformsIsUsedWithModelForms(TestCaseBase):
    def get_superform_with_forms(self, post_data=None):
        from django.db import models
        class Address(models.Model):
            city = models.CharField(max_length=255)

        class AddressForm(forms.ModelForm):
            class Meta:
                model = Address

        class TestSuperForm(SuperForm):
            address = SubForm(AddressForm)

        return TestSuperForm(data=post_data)

    def should_not_be_valid_with_no_data(self):
        tsf = self.get_superform_with_forms()
        self.assert_false(tsf.is_valid())

    def should_have_two_forms_and_a_single_subform(self):
        tsf = self.get_superform_with_forms()
        self.assert_equal(len(tsf.base_subforms),  1)
        self.assert_equal(len(tsf.forms), 1)

    def should_print_as_table(self):
        tsf = self.get_superform_with_forms()
        self.assert_equal(tsf.as_table(), '<tr><th><label for="id_address-city">City:</label></th><td><input id="id_address-city" type="text" name="address-city" maxlength="255" /></td></tr>')

    def should_validate_when_fields_exist(self):
        data = {
            'address-city': 'Some City',
        }
        tsf = self.get_superform_with_forms(data)
        self.assert_true(tsf.is_valid())

        self.assert_equal(tsf.cleaned_data['address']['city'], 'Some City')

    def should_not_validate_with_invalid_data(self):
        data = {
            'address-city': '',
        }
        tsf = self.get_superform_with_forms(data)
        self.assert_false(tsf.is_valid())

        self.assert_equal(tsf.errors['address']['city'], ['This field is required.'])

Enjoy!

Sam Dolan
  • 31,966
  • 10
  • 88
  • 84
  • Wow, that's awesome! Thank you so much :) Unfortunately this came a little late for my current project (found another sol'n), but maybe for my next one! – mpen Jul 08 '10 at 19:34
  • Yeah, no problem. Thank you for posing the question and giving a good starting point. This is something that would have been useful to me for a long time. As for your other solution, what'd you end up doing? Oh, and did I earn the bounty? :) – Sam Dolan Jul 08 '10 at 19:50
  • You sure did. I'm assuming your code actually works ;) What did I end up doing? Only the dumbest thing a programmer can do! Re-wrote the *entire* forms app from scratch! – mpen Jul 08 '10 at 21:02
  • To be honest, I haven't used the class in practice yet (have to move onto *paying* work now:)), though the tests are pretty thorough in testing the integration/interfaces. And, wow that as pretty silly :) The unit tests were the only time consuming part of fixing the original superforms.py file. If you did rewrite it, and it's better, you may consider uploading your new classes to bitbucket (or similar). – Sam Dolan Jul 08 '10 at 21:12
  • Might do that! Still have a bit of work to go on them, but they're turning out really nicely. Results in a lot less code required than with Django's forms. Performs both client and server side validation too! – mpen Jul 09 '10 at 02:27
  • AAACKK..... you're missing `__iter__` and `__getitem__` on `SubForm`. Very easy to implement (just redirect em to `self._form`) but very important! – mpen Jul 12 '10 at 05:01
  • Adding a `save()` method on `SubForm()` would be logical too.... right now I'm just using SuperForms to nest a bunch of regular forms.. the only real advantage I see this giving me is that it validates all of them at once...well, and it simplifies my templates a tiny bit. Do you know how these FormLists are supposed to be used? Are they like... FormSets? – mpen Jul 12 '10 at 05:42
  • @Mark: As for `__iter__` & `__getitem__`. I said that I hadn't used in practice yet, and these are simple things to clean up (as you mentioned). Could you update the patch on djangoproject with your updates? The save method would be logical, I just didn't want to deal with the complexity until I had the use case. As for the `FormLists`, I don't know. I just wanted to get the basic form api working through thorough unit tests, and clean up the internals when I needed too. – Sam Dolan Jul 18 '10 at 04:43
  • http://code.djangoproject.com/attachment/ticket/3706/superforms.2.py I added `save()` too...again, it just passes it off to the `form`. It won't exist for non-modelforms, but you really shouldn't be trying to `save()` something that isn't a model-form anyway... so I say, let it throw an error! – mpen Jul 18 '10 at 05:05
  • @sdolan, is your "common" repository with all the tests publicly available? – akaihola Oct 04 '11 at 06:13
  • @akaihola: How soon do you need it? My "common" repo got mixed up with the proprietary stuff, so I had to make it private for the time being. I can split out the SuperForm stuff out into it's own repo, but it may have to wait for the weekend. Also, was it you that updated the django ticket? If so, I completely agree. – Sam Dolan Oct 04 '11 at 06:27
  • @sdolan, Yes, I did comment on the ticket about making a reusable app out of SuperForm. I don't have an urgent need for it right now though. Looks very useful of course, thanks for sharing your code! – akaihola Nov 27 '11 at 18:49