pythonpyramiddeformcolander

Working with Many to Many Relationships in Deform/Colander HTML Select Field


I'm working in the Pyramid framework and using the Deform package to render HTML forms given a colander scheme. I'm struggling to get my head wrapped around how to handle a schema with a many to many relationship. For example, my sqlalchemy models look like this:

class Product(Base):
    """ The SQLAlchemy declarative model class for a Product object. """
    __tablename__ = 'products'

    id = Column(Integer, primary_key=True)
    name = Column(String(80), nullable=False)
    description = Column(String(2000), nullable=False)
    categories = relationship('Category', secondary=product_categories,
                               backref=backref('categories', lazy='dynamic'))


class Category(Base):                                                                                
    """ The SQLAlchemy declarative model class for a Category object. """                            
    __tablename__ = 'categories'

    id = Column(Integer, primary_key=True)                                                                                            
    name = Column(String(80), nullable=False)                                                                                                                                 
    products = relationship('Product', secondary=product_categories,                                 
                               backref=backref('products', lazy='dynamic'))


product_categories = Table('product_categories', Base.metadata,
    Column('products_id', Integer, ForeignKey('products.id')),
    Column('categories_id', Integer, ForeignKey('categories.id'))
)

As you can see, it's a pretty simple model representing a online store where a product can belong to one or more categories. In my rendered form, I would like to have a select multiple field in which I can choose a few different categories in which to place the product into. Here's a simple colander schema:

def get_category_choices():

    all_categories = DBSession.query(Category).all()

    choices = []
    for category in all_categories:
        choices.append((category.id, category.name))

    return choices


class ProductForm(colander.Schema):
    """ The class which constructs a PropertyForm form for add/edit pages. """

    name = colander.SchemaNode(colander.String(), title = "Name",
                               validator=colander.Length(max=80),
                              )

    description = colander.SchemaNode(colander.String(), title="Description",
                                  validator=colander.Length(max=2000),
                                  widget=deform.widget.TextAreaWidget(rows=10, cols=60),
                                 )

    categories = colander.SchemaNode(
                colander.Set(),
                widget=deform.widget.SelectWidget(values=get_category_choices(), multiple=True),
                validator=colander.Length(min=1),
                )

And, sure enough, I do get a proper rendering of all fields, however, the categories field doesn't seem to be 'tied' to anything. If I edit a product that I know to belong to two categories, I would expect the select field to have those two categories already highlighted. Making a change (selecting a third item) should result in a DB change where product_categories has three rows for the given product_id, each with a different category_id. It may be TMI, but I'm also using a method similar to this to read/write the appstruct.

Now, I've seen mention (and again) of using Mapping to handle many to many relationship fields such as this, but there's not a solid example of how to use it.

Thanks in advance to anybody who can lend a hand. It'd be appreciated.


Solution

  • I was out in left field on this one, not even asking the right question for the right area. What I was really after was to have some defaults selected in a multi-select colander SchemaNode. I brought my question over to pylons-discuss Google Group, and they were able to help me out. It came down to using 'set()' when I construct the appstruct in my Product class like below:

    def appstruct(self):
        """ Returns the appstruct model for use with deform. """
    
        appstruct = {}
        for k in sorted(self.__dict__):
            if k[:4] == "_sa_":
                continue
    
            appstruct[k] = self.__dict__[k]
    
        # Special case for the categories
        appstruct['categories'] = set([str(c.id) for c in self.categories])
    
        return appstruct
    

    Then, I passed that (along with the other items in the appstruct) along to the form and it rendered the HTML properly, with all the categories selected. To apply the appstruct after submitting, the code ended up looking like:

    def apply_appstruct(self, appstruct):
        """ Set the product with appstruct from the submitted form. """
    
        for kw, arg in appstruct.items():
    
            if kw == "categories":
                categories = []
                for id in arg:
                    categories.append(DBSession.query(Category).filter(Category.id == id).first())
                arg = categories
    
            setattr(self, kw, arg)
    

    The colander schema ended up looking like:

    def get_category_choices():
        all_categories = DBSession.query(Category).all()
        return [(str(c.id), c.name) for c in all_categories]
    
    categories = get_category_choices()
    
    class ProductForm(colander.Schema):
        """ The class which constructs a ProductForm form for add/edit pages. """
    
        name = colander.SchemaNode(colander.String(), title = "Name",
                                   validator=colander.Length(max=80),
                                  )
    
        description = colander.SchemaNode(colander.String(), title="Description",
                                      validator=colander.Length(max=2000),
                                      widget=deform.widget.TextAreaWidget(rows=10, cols=60),
                                     )
    
        categories = colander.SchemaNode(
                    colander.Set(),
                    widget=deform.widget.SelectWidget(
                        values=categories,
                        multiple=True,
                    ),
                    validator=colander.Length(min=1),
                    )
    

    Thanks for all who looked. My apologies I was asking the wrong questions and not keeping it simple. :-)