from Acquisition import ImplicitAcquisitionWrapper, aq_base, aq_inner class RelationAware: """ The RelationAware mixin class provides an object with transparent relations management. An object may specify that it has one or more predefined relations, by specifying a _relations structure in its class: _relations=( {'id': 'Person_to_Organisation', 'attr': 'Organisation'¸ 'cardinality': 'single'}, ) 'id' refers to the id of the mxmRelation that needs to exist in the default_relations_mngr (This is just a folder with the id 'relations' in the acquisition path of the object. It should contain mxmRelation instances for each relation in the _relations dictionary. Call 'manage_editRelationsMngr' to override 'default_relations_mngr'). 'attr' refers to the attribute that should be computed on the object to make references like Person.Organisation.Name possible. 'cardinality' can be either 'single' or 'multiple'. This is used to help with the computation of the attribute. If the the 'cardinality' is 'multiple' we return a list otherwise we return a single object. TODO: 'cardinality' is not currently used to strictly enforce the cardinality of relations. To add and edit the relations for an object call 'manage_addRelations' and 'manage_changeRelations'. It is the responsibility of the subclass to call these methods - they are not called automatically. Both methods accept either values set on the REQUEST or keyword arguments. The keys should match the value of 'attr' defined in '_relations' and key values should be paths to objects eg.: folder.manage_addProduct['MyProduct'].manage_addPerson('pete') person = folder.pete person.manage_changeProperties(Name='Pete', Surname='Smith') person.manage_addRelations( Organisation='/path/to/this_organisation') or in the case where pete exists person = folder.pete person.manage_changeRelations( Organisation='/path/to/that_organisation') TODO: Handle copy and paste of objects """ _relations = () meta_type = 'RelationAware' # By default we assume there is a folder with id == 'relations' in # which we store all mxmRelations default_relations_mngr = 'relations' def __of__(self, parent, _iaw=ImplicitAcquisitionWrapper): # Bound to unwrapped parent, just try again if type(parent) is not _iaw or not hasattr(parent,'REQUEST'): return self # Create our canonical form new_self = _iaw(self,parent) if type(self) is _iaw: # Been wrapped already? return new_self # No special handling if not hasattr(self, '_relations'): return new_self dm = parent.aq_acquire(self.default_relations_mngr) for d in self._relations: id, attr = d['id'], d['attr'] mxmRelation = dm[id] relations = mxmRelation.get(new_self) spec = self.relationSpec(id) if spec['cardinality'] == 'single' and relations: setattr(new_self, attr, relations[0]) else: setattr(new_self, attr, relations) return new_self def manage_editRelationMngr(self, mngr_id): """ Set the relations manager """ self.default_relations_mngr = mngr_id def getRelationMngr(self): """ Return the relations data manager TODO: should we raise an exception if not found? """ if hasattr(self, self.default_relations_mngr): return getattr(self, self.default_relations_mngr) def manage_addRelations(self, REQUEST=None, **kw): """ Add relations for RelationAware subclasses. Values can be passed as keyword arguments or set on the REQUEST. Keyword arguments or REQUEST is search for keys matching the ids of relations defined in '_relations'. Key values must be paths. """ relationMngr = self.getRelationMngr() if not relationMngr: return if REQUEST is None: props={} else: props=REQUEST props.update(kw) for d in self._relations: id, attr = d['id'], d['attr'] value = props.get(attr, '') if not value: continue # Add relations mxmRelation = relationMngr[id] if type(value) == type(''): value = [value] for path in value: relate_to = self.restrictedTraverse(path) mxmRelation.relate(self, relate_to) def manage_changeRelations(self, REQUEST=None, **kw): """ Edit relations for RelationAware subclasses. Values can be passed as keyword arguments or set on the REQUEST. Keyword arguments or REQUEST is search for keys matching the ids of relations defined in '_relations'. Key values must be paths. Orphaned relations are removed. """ relationMngr = self.getRelationMngr() if not relationMngr: return if REQUEST is None: props={} else: props=REQUEST props.update(kw) for d in self._relations: id, attr = d['id'], d['attr'] mxmRelation = relationMngr[id] # Get existing relations old_relations = self.relationValues(id) value = props.get(attr, '') if not value: mxmRelation.unrelate(self, old_relations) continue # Add relations if type(value) == type(''): value = [value] for path in value: relate_to = self.unrestrictedTraverse(path) if relate_to in old_relations: old_relations.remove(relate_to) else: mxmRelation.relate(self, relate_to) # Unrelate orphaned relations mxmRelation.unrelate(self, old_relations) def relationSpec(self, relation_id): """ Return relation with id == relation_id """ return filter(lambda i, n=relation_id: i['id'] == n, self._relations)[0] def relationIds(self): """ Return a list of relation ids """ return map(lambda i: i['id'], self._relations) def relationValues(self, relation_id): """ Return the objects for a relationship """ mxmRelation = self.getRelationMngr()[relation_id] relations = mxmRelation.get(self) spec = self.relationSpec(relation_id) if spec['cardinality'] == 'single': return relations[0] else: return relations