droidballoon
droidballoon

Reputation: 730

Grouping many to many choices by type in form

I have a Sandbox which contains plenty of items composed of one or more attributes. Each attribute belongs to a particular attribute type (such as color, shape, etc) I am at loss on how to render the form with Attributes grouped together with their AttributeTypes.

Models

class Sandbox(models.Model):
    item = models.ForeignKey('Item')

class Item(models.Model):
    name = models.CharField()
    sandbox = models.ForeignKey(Sandbox)
    attributes = models.ManyToManyField('Attribute')

class Attribute(models.Model):
    name = models.CharField()
    type = models.ForeignKey('AttributeType')

class AttributeType(models.Model):
    name = models.CharField()

class ItemAttribute(models.Model):
    # intermediary model to hold references
    item = models.ForeignKey(Item)
    type = models.ForeignKey(AttributeType)
    attribute = models.ForeignKey(Attribute)

ModelForm

class Sandbox(ModelForm):
    class Meta:
        model = Sandbox

There can only be one choice per attribute type. For instance, something can only have one color or one shape.

    AttributeType   Attribute       Users choice
    color
                    red
                    blue            [x]
                    green
                    shape

    shape
                    triangular
                    squared         [x]
                    spherical

This is were I get stuck. How do I group these attributes together in the form and how can I use radio buttons to select only one attribute per type? Perhaps my original idea of having a simple model representation falls short here? I have tried documentation, StackOverflow and Google but no luck.

Any tips and ideas are welcome.

My solution

I built up a solution which satisfies my needs. @bmihelac pointed me in a good direction to this article on how to create a factory method for creating a custom form. [see below]

def make_sandbox_form(item):

    def get_attributes(item):
        item_attributes = ItemAttribute.objects.filter(item=item)
        # we want the first attribute type
        _attr_type = item_attributes[0].attribute.all()[0].attribute_type

        choices = []        # holds the final choices
        attr_fields = {}    # to hold the final list of fields
        for item_attrs in item_attributes.all():
            attributes = item_attrs.attribute.all().order_by('attribute_type')
            for attr in attributes:
                print attr.attribute_type, ' - ' , _attr_type
                if attr.attribute_type == _attr_type:
                    choices.append( ( attr.pk, attr.value ) )
                else:
                    d = {u'%s' % _attr_type : fields.ChoiceField(choices=choices, widget=RadioSelect)}
                    attr_fields = dict(attr_fields.items() + d.items() )
                    # set the _attr_type to new type and start over with next attribute type
                    _attr_type = attr.attribute_type
                    choices = []

        return attr_fields

    form_fields = {
        'item' : fields.IntegerField(widget=HiddenInput),
    }
    form_fields = dict(form_fields.items() + get_attributes(item).items())

    return type('SandboxForm', (forms.BaseForm, ), { 'base_fields' : form_fields})

Invoke the form my calling this factory method as: form = make_sandbox_form()

http://www.b-list.org/weblog/2008/nov/09/dynamic-forms/

(Wish I could upvote all but being a StackOverflow rookie I lack the reputation to do so.)

Upvotes: 1

Views: 310

Answers (2)

bmihelac
bmihelac

Reputation: 6323

I would create dynamic form, creating one choice field for every AttributeType.

Then, you can easily replace widget with radio buttons.

This article could be helpful:

http://www.b-list.org/weblog/2008/nov/09/dynamic-forms/

Upvotes: 1

James R
James R

Reputation: 4656

Can one sandbox have many items, or one item have many sandboxes? Can an item belong to multiple sandboxes at once?

I think you want one sandbox to contain many items:

class Sandbox(models.Model):
    name = models.CharField()

class Item(models.Model):
    name = models.CharField()
    sandbox= models.ForeignKey(Sandbox)
    attributes = models.ManyToManyField('Attribute')

The same analysis holds true here:

Does one attribute have many attribute types, or one attribute type have many attributes?

Here you have the relationship right, I just switched the order of the models

class AttributeType(models.Model):
    name = models.CharField()

class Attribute(models.Model):
    name = models.CharField()
    type = models.ForeignKey(AttributeType)

So each item has an attribute and they always are endowed with these attributes, color and shape.

While you could have a table that had data that looked like:

pk type
1 green
2 circular
etc

I personally wouldn't do that, because I think data that is logically the same should be grouped together. Shapes have different attributes than colors. For example and to illustrate my point, what if you wanted the RGB of a color? Then you would have extra columns for shapes when they aren't needed and this is confusing. Same is true in the converse, colors don't have dimension.

Instead, I might do something like:

class Color(models.Mode):
     #info about colors

class Shape(models.Mode):
     #info about shapes

class Item(models.Model):
    name = models.CharField()
    sandbox= models.ForeignKey(Sandbox)
    color= models.ForeignKey(Color)
    shape= models.ForeignKey(Shape)

This also guarantees you only have one choice per, because ForeignKey in django.Forms defaults you to a ChioceField (iirc).

As for overriding that and making it a radiobutton, just review the docs here:

https://docs.djangoproject.com/en/dev/ref/forms/widgets/

Upvotes: 1

Related Questions