September 19, 2014

Copying Dexterity fields from one instance to another instance

In our current Plone project we have the usecase where we need to copy over all schema-defined values of a Dexterity-based content-type to a new "empty" instance of the same type. In Archetypes the functionality would be a three-liner however the Dexterity world with schemas, behaviors etc. is much more complicated and by a magnitude less approachable. The naive approach would be to read the values directly as attributes from the original object and assign them back to the target instance like

value = source.some_field
target.some_field = value

However the Plone world is bad and evil to developers and behaviors require some special attention since because every behavior may implement a different storage strategy under the hood that would be bypassed by direct attribute access. Instead you need to adapt the source and target instance for each field to the specific schema adapter and read/write the values from the adapter.

Here is the complete code how to copy over all schema-based values from a 'source' object to the 'target' object of the same class.

The insanity is directly visible and it is obvious that this programming approach is hard to sell as a pro-argument to non-Plone developers.

def clone_plone_metadata(source, target):
    """ Copy all metadata key-value pairs of the 'source'
        Dexterity content-object to 'target'.

    from plone.dexterity.interfaces import IDexterityFTI
    from plone.behavior.interfaces import IBehaviorAssignable

    if not isinstance(source, target.__class__):
        raise TypeError('source and target must be the same class ({} vs {})'.format(source.__class__, target.__class__))

    schema = getUtility(IDexterityFTI, name=source.portal_type).lookupSchema()
    fields = [(schema, schema[name]) for name in schema]

    assignable = IBehaviorAssignable(source)
    for behavior in assignable.enumerateBehaviors():
        behavior_schema = behavior.interface
        for name in behavior_schema:
            fields.append((behavior_schema, behavior_schema[name]))

    for schema_adapter, field in fields:
        source_adapted = schema_adapter(source)
        target_adapted = schema_adapter(target)
        value = getattr(source_adapted, field.getName())
        if isinstance(value, str):
            value = unicode(value, 'utf-8')
        setattr(target_adapted, field.getName(), value)