Migration
=========

To test migration we first add some sample content::

    >>> self.loginAsPortalOwner()
    >>> for x in ('foo', 'bar'):
    ...     dummy = self.portal.invokeFactory('PoiTracker', x)
    ...     for y in ('foo', 'bar' , 'baz'):
    ...         dummy = self.portal[x].invokeFactory('PoiIssue', y)
    ...         for z in ('foo', 'bar' , 'baz', 'quux'):
    ...             dummy = self.portal[x][y].invokeFactory('PoiResponse', z)
    >>> catalog = self.portal.portal_catalog
    >>> len(catalog(portal_type='PoiTracker'))
    2
    >>> len(catalog(portal_type='PoiIssue'))
    6
    >>> len(catalog(portal_type='PoiResponse'))
    24


Issue Description
-----------------

The Description field of an Issue is always unicode now:

    >>> issue = self.portal.foo.foo
    >>> issue.Description()
    u''
    >>> type(issue.Description())
    <type 'unicode'>

Since we are testing this, we will also demonstrate that the
Description method returns the description field when it has been set
and the details field otherwise::

    >>> issue.update(details='details field')
    >>> issue.Description()
    u'details field'
    >>> issue.update(description='description field')
    >>> issue.Description()
    u'description field'

But anyway, Description always is of type unicode.  This was not
always the case.  And that could give problems while reindexing an
issue.  We cheat to test that by changing the Description method into
an attribute::

    >>> issue.Description = 'plain old string'
    >>> issue.Description
    'plain old string'
    >>> issue.reindexObject()

At this point it is still fine, but this changes when the Description
is non-ascii.  If on your system sys.getdefaultencoding() returns
something else than 'ascii' you may not actually run into problems.
But we cannot assume that.  At first all looks fine, a reindex gives
no complaints::

    >>> issue.Description = 'Café René'
    >>> issue.reindexObject()

But now we undo our cheating by removing the Description attribute
from the issue::

    >>> del issue.__dict__['Description']
    >>> issue.Description()
    u'description field'

Now we come to the meat of the matter.  A reindex of this object now
fails, as during the reindexing (actually in the updateMetadata
method) the non-ascii Description string that is in the catalog gets
turned into unicode to compare it with the unicode Description of the
issue and this fails::

    >>> issue.reindexObject()
    Traceback (most recent call last):
    ...
    UnicodeDecodeError: 'ascii' codec can't decode byte ...

The way to fix this is by first unindexing the object.  Then a reindex
works fine again::

    >>> issue.unindexObject()
    >>> issue.reindexObject()

The workaround is to clear and rebuild the portal_catalog.  But there
is also migration for this so it can be done automatically for all
issues.  Let's hack some wrong Descriptions in the existing issues::

    >>> issues = [x.getObject() for x in catalog(portal_type='PoiIssue')]
    >>> for issue in issues:
    ...     issue.unindexObject()
    ...     issue.Description = 'Café René'
    ...     issue.reindexObject()
    ...     del issue.__dict__['Description']

This should again fail:

    >>> for issue in issues:
    ...     issue.reindexObject()
    Traceback (most recent call last):
    ...
    UnicodeDecodeError: 'ascii' codec can't decode byte ...

The migration code is usually called with portal_setup as context, but
let's call use a different context to show that also works::

    >>> from Products.Poi.migration import fix_descriptions
    >>> fix_descriptions(self.portal.foo)

Now everything is fine again:

    >>> for issue in issues:
    ...     issue.reindexObject()


Responses: from Archetypes to Zope 3
------------------------------------

We look at the first issue created.  Like all the others it has five
oldstyle response and no new style responses::

    >>> tracker = self.portal.foo
    >>> issue = tracker.foo
    >>> len(issue.contentValues(filter={'portal_type' : 'PoiResponse'}))
    4
    >>> from Products.Poi.adapters import IResponseContainer
    >>> rc = IResponseContainer(issue)
    >>> len(rc)
    0

We call the migrator on this issue::

    >>> from Products.Poi.migration import replace_old_with_new_responses
    >>> replace_old_with_new_responses(issue)

Now the numbers of old and new style responses have flipped::

    >>> len(issue.contentValues(filter={'portal_type' : 'PoiResponse'}))
    0
    >>> len(rc)
    4

Calling the migrator a second time has no ill effect::

    >>> replace_old_with_new_responses(issue)
    >>> len(issue.contentValues(filter={'portal_type' : 'PoiResponse'}))
    0
    >>> len(rc)
    4

Calling the migrator on something that is not a PoiIssue (actually not
implementing IIssue), gives no errors::

    >>> replace_old_with_new_responses(self.portal)

Calling migrate_responses searches for all PoiTrackers and migrates
all responses::

    >>> from Products.Poi.migration import migrate_responses
    >>> migrate_responses(self.portal)


All PoiResponses are gone after migration and they have been added as
new style responses::

    >>> len(catalog(portal_type='PoiResponse'))
    0
    >>> issues = catalog(portal_type='PoiIssue')
    >>> len(issues)
    6
    >>> for issue in issues:
    ...     issue = issue.getObject()
    ...     rc = IResponseContainer(issue)
    ...     self.assertEqual(len(rc), 4)


Migrate workflow changes from ids to titles
-------------------------------------------

When a response changes the workflow state of an issue, this change is
recorded in that response.  This used to be done by storing review
state ids.  Currently this is done by storing review state titles.
Friendlier for the end user and translatable to boot.  We have a
migration step that finds responses with review state ids in them and
turns them into titles.

    >>> from Products.Poi.migration import migrate_workflow_changes
    >>> for brain in issues:
    ...     issue = brain.getObject()
    ...     rc = IResponseContainer(issue)
    ...     for response in rc:
    ...         self.assertEquals(len(response.changes), 0)
    ...         # Add change with review state ids:
    ...         response.add_change('review_state', u'Issue state', 'unconfirmed', 'open')
    ...         self.assertEquals(response.changes[0]['before'], 'unconfirmed')
    ...         self.assertEquals(response.changes[0]['after'], 'open')
    >>> migrate_workflow_changes(self.portal)
    >>> for brain in issues:
    ...     issue = brain.getObject()
    ...     rc = IResponseContainer(issue)
    ...     for response in rc:
    ...         self.assertEquals(len(response.changes), 1)
    ...         # The review state ids are now titles:
    ...         self.assertEquals(response.changes[0]['before'], 'Unconfirmed')
    ...         self.assertEquals(response.changes[0]['after'], 'Confirmed')
