I want to implement an entity with 2 boolean attributes, and a requirement is that these two attributes never have the same boolean value (think of some kind of radio buttons).

Let's start with a simple schema example:

# in schema.py
class MyEntity(EntityType):
   use_option1 = Boolean(required=True, default=True)
   use_option2 = Boolean(required=True, default=False)

So new entities will be conform to the spec.

To do this, you need two things:

  • a constraint in the entity schema which will ring if both attributes have the same value
  • a hook which will toggle the other attribute when one attribute is changed.

RQL constraints are generally meant to be used on relations, but you can use them on attributes too. Simply use 'S' to denote the entity, and write the constraint normally. You need to have the same constraint on both attributes, because the constraint evaluation is triggered by the modification of the attribute.

# in schema.py
class MyEntity(EntityType):
   use_option1 = Boolean(required=True, default=True,
                         constraints = [
                              RQLConstraint('S use_option1 O1, S use_option2 != O1')
   use_option2 = Boolean(required=True, default=False,
                         constraints = [
                              RQLConstraint('S use_option1 O1, S use_option2 != O1')

With this update, it is no longer possible to have both options set to True or False (you will get a ValidationError). The nice thing to have is to get the other option to be updated when one of the two attributes is changed, which means that you don't have to take care of this when editing the entity in the web interface (which you cannot do anyway if you are using reledit for instance).

A nice way of writing the hook is to use Python's sets to avoid tedious logic code:

class RadioButtonUpdateHook(Hook):
   '''ensure use_option1 = not use_option2 (and conversely)'''
   __regid__ = 'mycube.radiobuttonhook'
   events = ('before_update_entity', 'before_add_entity')
   __select__ = Hook.__select__ & is_instance('MyEntity')
   # we prebuild the set of boolean attribute names
   _flag_attributes = set(('use_option1', 'use_option2'))
   def __call__(self):
       entity = self.entity
       edited = set(entity.cw_edited)
       attributes = self._flag_attributes
       if attributes.issubset(edited):
           # both were changed, let the integrity hooks do their job
       if not attributes & edited:
           # none of our attributes where changed, do nothing
       # find which attribute was modified
       modified_set = attributes & edited
       # find the name of the other attribute
       to_change = (attributes - modified_set).pop()
       modified_name = modified_set.pop()
       # set the value of that attribute
       entity.cw_edited[to_change] = not entity.cw_edited[modified_name]

That's it!

blog entry of