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 return if not attributes & edited: # none of our attributes where changed, do nothing return # 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]
- cubicweb #3749736 [hooks] enabled_category should NOT be a predicate
- TheCubicWebBook #569106 hooks section
- cubicweb #1417110 postcreate.py must fire hooks by default
- cubicweb #2932033 [security] consider operation checking only in securityafterupdateentity hook
- cubicweb #343469 schema changes trigger alteration of the database could be reported on external sources