Ticket #10403: declarativeformsets.diff

File declarativeformsets.diff, 15.9 KB (added by Koen Biermans <koen.biermans@…>, 7 years ago)

patch to provide declarative syntax for modelformset and inlineformset

  • django/forms/models.py

     
    2323__all__ = (
    2424    'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model',
    2525    'save_instance', 'form_for_fields', 'ModelChoiceField',
    26     'ModelMultipleChoiceField',
     26    'ModelMultipleChoiceField', 'ModelFormSet', 'InlineFormSet'
    2727)
    2828
    2929
     
    249249                # This is an extra field that's not on the ModelForm, ignore it
    250250                continue
    251251            if not isinstance(f, ModelField):
    252                 # This is an extra field that happens to have a name that matches, 
    253                 # for example, a related object accessor for this model.  So 
     252                # This is an extra field that happens to have a name that matches,
     253                # for example, a related object accessor for this model.  So
    254254                # get_field_by_name found it, but it is not a Field so do not proceed
    255255                # to use it as if it were.
    256256                continue
     
    456456    FormSet.model = model
    457457    return FormSet
    458458
     459class ModelFormSetMetaclass(type):
     460    def __new__(cls, name, bases, attrs):
     461        try:
     462            parents = [b for b in bases if issubclass(b, ModelFormSet)]
     463        except NameError:
     464            # We are defining ModelFormSet itself.
     465            parents = None
     466        new_class = super(ModelFormSetMetaclass, cls).__new__(cls, name, bases,
     467                            attrs)
     468        if not parents:
     469            return new_class
    459470
     471        if 'can_order' not in attrs:
     472            new_class.can_order = False
     473        if 'can_delete' not in attrs:
     474            new_class.can_delete = False
     475        if 'max_num' not in attrs:
     476            new_class.max_num = 0
     477        if 'extra' not in attrs:
     478            new_class.extra = 1
     479
     480        if 'form' not in attrs:
     481            raise Exception(u'Error in definition of ModelFormSet subclass %s. You need to define a form.' % name)
     482
     483        if 'model' not in attrs:
     484            raise Exception(u'Error in definition of ModelFormSet subclass %s. You need to define a model.' % name)
     485        model = attrs.get('model')
     486
     487        new_class.model = model
     488
     489        return new_class
     490
     491class ModelFormSet(BaseModelFormSet):
     492    """
     493    ModelFormSet subclasses need these attributes: form and model
     494    """
     495    __metaclass__ = ModelFormSetMetaclass
     496
     497
    460498# InlineFormSets #############################################################
    461499
    462500class BaseInlineFormSet(BaseModelFormSet):
     
    578616    FormSet.fk = fk
    579617    return FormSet
    580618
     619class InlineFormSetMetaclass(type):
     620    def __new__(cls, name, bases, attrs):
     621        try:
     622            parents = [b for b in bases if issubclass(b, InlineFormSet)]
     623        except NameError:
     624            # We are defining InlineFormSet itself.
     625            parents = None
     626        new_class = super(InlineFormSetMetaclass, cls).__new__(cls, name, bases,
     627                            attrs)
     628        if not parents:
     629            return new_class
    581630
     631        if 'can_order' not in attrs:
     632            new_class.can_order = False
     633        if 'can_delete' not in attrs:
     634            new_class.can_delete = False
     635        if 'max_num' not in attrs:
     636            new_class.max_num = 0
     637        if 'extra' not in attrs:
     638            new_class.extra = 3
     639
     640        if 'form' not in attrs:
     641            raise Exception(u'Error in definition of InlineFormSet subclass %s. You need to define a form.' % name)
     642
     643        if 'model' not in attrs:
     644            raise Exception(u'Error in definition of InlineFormSet subclass %s. You need to define a model.' % name)
     645        model = attrs.get('model')
     646
     647
     648        if 'parent' not in attrs:
     649            raise Exception(u'Error in definition of InlineFormSet subclass %s. You need to define a parent attribute.' % name)
     650        parent = attrs.pop('parent')
     651
     652        try:
     653            fk = (i for i in model._meta.fields if i.name == parent).next()
     654        except:
     655            raise Exception(u'Error in definition of InlineFormSet subclass %s. The parent attribute of the inlineformset is not a valid field.' % name)
     656
     657        from django.db.models import ForeignKey
     658        if not isinstance(fk, ForeignKey):
     659            raise Exception(u'Error in definition of InlineFormSet subclass %s. The parent attribute is not a ForeignKey.' % name)
     660
     661        new_class.fk = fk
     662
     663        return new_class
     664
     665class InlineFormSet(BaseInlineFormSet):
     666    """
     667    ModelFormSet subclasses need these attributes: form and model and parent
     668    """
     669    __metaclass__ = InlineFormSetMetaclass
     670
     671
    582672# Fields #####################################################################
    583673
    584674class InlineForeignKeyHiddenInput(HiddenInput):
  • tests/modeltests/model_formsets/models.py

     
    2727
    2828    def __unicode__(self):
    2929        return self.title
    30    
     30
    3131class BookWithCustomPK(models.Model):
    3232    my_pk = models.DecimalField(max_digits=5, decimal_places=0, primary_key=True)
    3333    author = models.ForeignKey(Author)
     
    3535
    3636    def __unicode__(self):
    3737        return u'%s: %s' % (self.my_pk, self.title)
    38    
     38
    3939class AlternateBook(Book):
    4040    notes = models.CharField(max_length=100)
    41    
     41
    4242    def __unicode__(self):
    4343        return u'%s - %s' % (self.title, self.notes)
    44    
     44
    4545class AuthorMeeting(models.Model):
    4646    name = models.CharField(max_length=100)
    4747    authors = models.ManyToManyField(Author)
     
    6060class Place(models.Model):
    6161    name = models.CharField(max_length=50)
    6262    city = models.CharField(max_length=50)
    63    
     63
    6464    def __unicode__(self):
    6565        return self.name
    6666
     
    6868    auto_id = models.AutoField(primary_key=True)
    6969    name = models.CharField(max_length=100)
    7070    place = models.ForeignKey(Place)
    71    
     71
    7272    def __unicode__(self):
    7373        return "%s at %s" % (self.name, self.place)
    7474
     
    8181class OwnerProfile(models.Model):
    8282    owner = models.OneToOneField(Owner, primary_key=True)
    8383    age = models.PositiveIntegerField()
    84    
     84
    8585    def __unicode__(self):
    8686        return "%s is %d" % (self.owner.name, self.age)
    8787
    8888class Restaurant(Place):
    8989    serves_pizza = models.BooleanField()
    90    
     90
    9191    def __unicode__(self):
    9292        return self.name
    9393
     
    114114# using inlineformset_factory.
    115115class Repository(models.Model):
    116116    name = models.CharField(max_length=25)
    117    
     117
    118118    def __unicode__(self):
    119119        return self.name
    120120
    121121class Revision(models.Model):
    122122    repository = models.ForeignKey(Repository)
    123123    revision = models.CharField(max_length=40)
    124    
     124
    125125    class Meta:
    126126        unique_together = (("repository", "revision"),)
    127    
     127
    128128    def __unicode__(self):
    129129        return u"%s (%s)" % (self.revision, unicode(self.repository))
    130130
     
    146146class Player(models.Model):
    147147    team = models.ForeignKey(Team, null=True)
    148148    name = models.CharField(max_length=100)
    149    
     149
    150150    def __unicode__(self):
    151151        return self.name
    152152
     
    526526...     print book.title
    527527Les Fleurs du Mal
    528528
    529 Test inline formsets where the inline-edited object uses multi-table inheritance, thus 
     529Test inline formsets where the inline-edited object uses multi-table inheritance, thus
    530530has a non AutoField yet auto-created primary key.
    531531
    532532>>> AuthorBooksFormSet3 = inlineformset_factory(Author, AlternateBook, can_delete=False, extra=1)
     
    663663>>> formset.save()
    664664[<OwnerProfile: Joe Perry is 55>]
    665665
    666 # ForeignKey with unique=True should enforce max_num=1 
     666# ForeignKey with unique=True should enforce max_num=1
    667667
    668668>>> FormSet = inlineformset_factory(Place, Location, can_delete=False)
    669669>>> formset = FormSet(instance=place)
     
    861861>>> formset.get_queryset()
    862862[<Player: Bobby>]
    863863
     864
     865# Tests for declarative modelformset
     866
     867# clean up first
     868>>> Author.objects.all().delete()
     869
     870>>> class AuthorForm(forms.ModelForm):
     871...     class Meta:
     872...         model = Author
     873
     874# test definition checks
     875>>> class BadFormSet1(forms.models.ModelFormSet):
     876...     form = AuthorForm
     877Traceback (most recent call last):
     878...
     879Exception: Error in definition of ModelFormSet subclass BadFormSet1. You need to define a model.
     880
     881>>> class BadFormSet2(forms.models.ModelFormSet):
     882...     model = Author
     883Traceback (most recent call last):
     884...
     885Exception: Error in definition of ModelFormSet subclass BadFormSet2. You need to define a form.
     886
     887# test the modelformset functionality
     888>>> class DeclarativeAuthorFormSet(forms.models.ModelFormSet):
     889...     form = AuthorForm
     890...     model = Author
     891...     extra = 3
     892
     893>>> qs = Author.objects.all()
     894>>> formset = DeclarativeAuthorFormSet(queryset=qs)
     895>>> for form in formset.forms:
     896...     print form.as_p()
     897<p><label for="id_form-0-name">Name:</label> <input id="id_form-0-name" type="text" name="form-0-name" maxlength="100" /><input type="hidden" name="form-0-id" id="id_form-0-id" /></p>
     898<p><label for="id_form-1-name">Name:</label> <input id="id_form-1-name" type="text" name="form-1-name" maxlength="100" /><input type="hidden" name="form-1-id" id="id_form-1-id" /></p>
     899<p><label for="id_form-2-name">Name:</label> <input id="id_form-2-name" type="text" name="form-2-name" maxlength="100" /><input type="hidden" name="form-2-id" id="id_form-2-id" /></p>
     900
     901>>> data = {
     902...     'form-TOTAL_FORMS': '3', # the number of forms rendered
     903...     'form-INITIAL_FORMS': '0', # the number of forms with initial data
     904...     'form-0-name': 'Charles Baudelaire',
     905...     'form-1-name': 'Arthur Rimbaud',
     906...     'form-2-name': '',
     907... }
     908
     909>>> formset = DeclarativeAuthorFormSet(data=data, queryset=qs)
     910>>> formset.is_valid()
     911True
     912
     913>>> formset.save()
     914[<Author: Charles Baudelaire>, <Author: Arthur Rimbaud>]
     915
     916>>> for author in Author.objects.order_by('name'):
     917...     print author.name
     918Arthur Rimbaud
     919Charles Baudelaire
     920
     921
     922
     923# Tests for declarative inlineformset
     924
     925# clean up first
     926>>> Book.objects.all().delete()
     927>>> author = Author.objects.get(pk=1)
     928>>> class BookForm(forms.ModelForm):
     929...     class Meta:
     930...         model = Book
     931
     932# test definition checks
     933>>> class BadFormSet1(forms.models.InlineFormSet):
     934...     form = BookForm
     935...     parent = 'author'
     936Traceback (most recent call last):
     937...
     938Exception: Error in definition of InlineFormSet subclass BadFormSet1. You need to define a model.
     939
     940>>> class BadFormSet2(forms.models.InlineFormSet):
     941...     model = Book
     942...     parent = 'author'
     943Traceback (most recent call last):
     944...
     945Exception: Error in definition of InlineFormSet subclass BadFormSet2. You need to define a form.
     946
     947>>> class BadFormSet3(forms.models.InlineFormSet):
     948...     model = Book
     949...     form = BookForm
     950Traceback (most recent call last):
     951...
     952Exception: Error in definition of InlineFormSet subclass BadFormSet3. You need to define a parent attribute.
     953
     954>>> class BadFormSet4(forms.models.InlineFormSet):
     955...     model = Book
     956...     form = BookForm
     957...     parent = 'unexistingfield'
     958Traceback (most recent call last):
     959...
     960Exception: Error in definition of InlineFormSet subclass BadFormSet4. The parent attribute of the inlineformset is not a valid field.
     961
     962>>> class BadFormSet5(forms.models.InlineFormSet):
     963...     model = Book
     964...     form = BookForm
     965...     parent = 'title'
     966Traceback (most recent call last):
     967...
     968Exception: Error in definition of InlineFormSet subclass BadFormSet5. The parent attribute is not a ForeignKey.
     969
     970# test the inlineformset functionality
     971>>> class DeclarativeAuthorBooksFormSet(forms.models.InlineFormSet):
     972...     model = Book
     973...     form = BookForm
     974...     parent = 'author'
     975
     976>>> formset = DeclarativeAuthorBooksFormSet(instance=author)
     977>>> for form in formset.forms:
     978...     print form.as_p()
     979<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" maxlength="100" /><input type="hidden" name="book_set-0-author" value="1" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" id="id_book_set-0-id" /></p>
     980<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-author" value="1" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
     981<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-author" value="1" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
     982
     983>>> data = {
     984...     'book_set-TOTAL_FORMS': '3', # the number of forms rendered
     985...     'book_set-INITIAL_FORMS': '0', # the number of forms with initial data
     986...     'book_set-0-title': 'Les Fleurs du Mal',
     987...     'book_set-1-title': '',
     988...     'book_set-2-title': '',
     989... }
     990
     991>>> formset = DeclarativeAuthorBooksFormSet(data, instance=author)
     992>>> formset.is_valid()
     993True
     994
     995>>> formset.save()
     996[<Book: Les Fleurs du Mal>]
     997
     998>>> for book in author.book_set.all():
     999...     print book.title
     1000Les Fleurs du Mal
     1001
     1002>>> formset = DeclarativeAuthorBooksFormSet(instance=author)
     1003>>> for form in formset.forms:
     1004...     print form.as_p()
     1005<p><label for="id_book_set-0-title">Title:</label> <input id="id_book_set-0-title" type="text" name="book_set-0-title" value="Les Fleurs du Mal" maxlength="100" /><input type="hidden" name="book_set-0-author" value="1" id="id_book_set-0-author" /><input type="hidden" name="book_set-0-id" value="1" id="id_book_set-0-id" /></p>
     1006<p><label for="id_book_set-1-title">Title:</label> <input id="id_book_set-1-title" type="text" name="book_set-1-title" maxlength="100" /><input type="hidden" name="book_set-1-author" value="1" id="id_book_set-1-author" /><input type="hidden" name="book_set-1-id" id="id_book_set-1-id" /></p>
     1007<p><label for="id_book_set-2-title">Title:</label> <input id="id_book_set-2-title" type="text" name="book_set-2-title" maxlength="100" /><input type="hidden" name="book_set-2-author" value="1" id="id_book_set-2-author" /><input type="hidden" name="book_set-2-id" id="id_book_set-2-id" /></p>
     1008<p><label for="id_book_set-3-title">Title:</label> <input id="id_book_set-3-title" type="text" name="book_set-3-title" maxlength="100" /><input type="hidden" name="book_set-3-author" value="1" id="id_book_set-3-author" /><input type="hidden" name="book_set-3-id" id="id_book_set-3-id" /></p>
     1009
     1010>>> data = {
     1011...     'book_set-TOTAL_FORMS': '3', # the number of forms rendered
     1012...     'book_set-INITIAL_FORMS': '1', # the number of forms with initial data
     1013...     'book_set-0-id': '1',
     1014...     'book_set-0-title': 'Les Fleurs du Mal',
     1015...     'book_set-1-title': 'Le Spleen de Paris',
     1016...     'book_set-2-title': '',
     1017...     'book_set-3-title': '',
     1018... }
     1019
     1020>>> formset = DeclarativeAuthorBooksFormSet(data, instance=author)
     1021>>> formset.is_valid()
     1022True
     1023
     1024>>> formset.save()
     1025[<Book: Le Spleen de Paris>]
     1026
     1027
     1028# specific declarative field definitions just work on the declarative form for formsets
     1029# no need to define the widget in __init__
     1030>>> class MembershipForm(forms.ModelForm):
     1031...     date_joined = forms.SplitDateTimeField(initial=now, widget=forms.SplitDateTimeWidget)
     1032...     class Meta:
     1033...         model = Membership
     1034
     1035>>> class FormSet(forms.models.InlineFormSet):
     1036...     model = Membership
     1037...     form = MembershipForm
     1038...     parent = 'person'
     1039...     extra = 1
     1040>>> data = {
     1041...     'membership_set-TOTAL_FORMS': '1',
     1042...     'membership_set-INITIAL_FORMS': '0',
     1043...     'membership_set-0-date_joined_0': unicode(now.strftime('%Y-%m-%d')),
     1044...     'membership_set-0-date_joined_1': unicode(now.strftime('%H:%M:%S')),
     1045...     'initial-membership_set-0-date_joined': unicode(now.strftime('%Y-%m-%d %H:%M:%S')),
     1046...     'membership_set-0-karma': '',
     1047... }
     1048>>> formset = FormSet(data, instance=person)
     1049>>> formset.is_valid()
     1050True
     1051
    8641052"""}
Back to Top