Responses
=========

Adapters
--------

Responses used to be Archetypes objects.  Not anymore.  They are light
weight Zope 3 objects now.  Before we switch to that we had better
make sure that they continue to work at least as good as they used to.
So some tests are in order.  First we make a response::

    >>> from Products.Poi.adapters import Response
    >>> res = Response(u"I cannot reproduce this error.")
    >>> res.creator
    '(anonymous)'
    >>> res.date
    DateTime('...')
    >>> res.changes
    []
    >>> res.add_change("review_state", "Transition", "unconfirmed", "confirmed")
    >>> res.changes == [{'before': 'unconfirmed', 'after': 'confirmed', 'id': 'review_state', 'name': 'Transition'}]
    True

That is everything we need to know about a response really, so our
basis is fine this way.

One thing that a PoiIssue does is being a container for responses.  We
can factor that out into an adapter.  We use annotations to do that,
so we need to load some zcml so Zope knows how to annotate an
AttributeAnnotatable object::

    >>> import Products.Five
    >>> try:
    ...     from Zope2.App import zcml
    ... except ImportError:
    ...     # BBB Zope 2.12
    ...     from Products.Five import zcml
    >>> zcml.load_config('configure.zcml', Products.Five)
    >>> import zope.annotation
    >>> zcml.load_config('configure.zcml', zope.annotation)
    >>> from zope.annotation.interfaces import IAttributeAnnotatable

Now we make a mock issue (just to show that we do not need a full
blown PoiIssue) and turn it into a response container::

    >>> from Products.Poi.adapters import ResponseContainer
    >>> from zope.interface import implements
    >>> from Products.Poi.interfaces import IIssue
    >>> class MockIssue(object):
    ...     implements(IIssue, IAttributeAnnotatable)
    >>> issue = MockIssue()
    >>> rc = ResponseContainer(issue)
    >>> rc
    <Products.Poi.adapters.ResponseContainer object at ...>

This container does not have any responses yet, so let's do something
about that::

    >>> len(rc) == 0
    True
    >>> rc.add(res)
    >>> len(rc) == 1
    True
    >>> res in rc
    True

Adding something that is not an IResponse should fail::

    >>> rc.add("Bogus response.")
    Traceback (most recent call last):
    ...
    ValueError: IResponse interface not provided.


Removing responses
------------------

We add a few more responses::

    >>> for i in range(2, 12):
    ...     rc.add(Response("Response %d." % i))
    >>> len(rc)
    11

Responses can be deleted; they are not really removed, but emptied::

    >>> rc.delete(7)
    >>> len(rc) == 11
    True
    >>> rc[7] is None
    True

Non existing responses cannot be deleted::

    >>> rc.delete(20)
    Traceback (most recent call last):
    ...
    IndexError: list index out of range


Browser views
-------------

We have a browser view that takes care of showing the responses and
adding a response.  To properly test that, we really need a normal
PoiIssue within a PoiTracker.  To have this work we also need to load
a bit of zcml::

    >>> import Products.Archetypes.Schema
    >>> zcml.load_config('configure.zcml', Products.Archetypes.Schema)
    >>> from Products.Poi.content.PoiTracker import PoiTracker
    >>> from Products.Poi.content.PoiIssue import PoiIssue
    >>> tracker = PoiTracker("issues")
    >>> tracker.issue = PoiIssue("one")
    >>> issue = tracker.issue

We get some very basic Plone functionality going here.  At least we
need some tools and set a Creator for this tracker::

    >>> from Products.CMFPlone.WorkflowTool import WorkflowTool
    >>> from Products.CMFPlone.URLTool import URLTool
    >>> from Products.CMFDefault.MembershipTool import MembershipTool
    >>> from Products.CMFPlone.CatalogTool import CatalogTool
    >>> from Products.PluggableAuthService.PluggableAuthService import addPluggableAuthService
    >>> tracker.portal_workflow = WorkflowTool()
    >>> tracker.portal_url = URLTool()
    >>> tracker.portal_membership = MembershipTool()
    >>> addPluggableAuthService(tracker.portal_membership)
    >>> class MockTransformData(object):
    ...     def __init__(self, text):
    ...         self.text = text
    ...     def getData(self):
    ...         return self.text
    >>> class MockTransforms(object):
    ...     def convertTo(self, target, text, mimetype=''):
    ...         return MockTransformData(text)
    >>> tracker.portal_transforms = MockTransforms()
    >>> tracker.portal_catalog = CatalogTool()
    >>> tracker.linkDetection = lambda x: x
    >>> tracker.Schema()['creators'].set(tracker, "manager")
    >>> tracker.getManagers()
    ('manager',)

We want to test some browser views.  So we need some test requests.
And a test request needs to be annotatable as we use memoize in some
parts::

    >>> from zope.publisher.browser import TestRequest
    >>> from zope.annotation.interfaces import IAttributeAnnotatable
    >>> from zope.interface import classImplements
    >>> classImplements(TestRequest, IAttributeAnnotatable)

And browser views can add status messages to a request, so we need
some more setup code::

    >>> from zope.component import provideAdapter
    >>> from Products.statusmessages.interfaces import IStatusMessage
    >>> from Products.statusmessages.adapter import StatusMessage
    >>> from zope.publisher.interfaces.browser import IBrowserRequest
    >>> provideAdapter(StatusMessage, adapts=(IBrowserRequest,),
    ...                provides=IStatusMessage)
    >>> TestRequest.RESPONSE = TestRequest.response
    >>> request = TestRequest()

We need to have an adapter for our issues::

    >>> from Products.Poi.adapters import IResponseContainer
    >>> provideAdapter(ResponseContainer, adapts=(IIssue,),
    ...                provides=IResponseContainer)
    >>> from zope.interface import classImplements
    >>> classImplements(PoiIssue, IAttributeAnnotatable)

And for Plone 3.1 we need an IWorkflowChain adapter::

    >>> try:
    ...     from Products.CMFPlone.interfaces import IWorkflowChain
    ... except ImportError:
    ...     pass
    ... else:
    ...     from Products.CMFCore.interfaces import IWorkflowTool
    ...     from Products.CMFPlone.workflow import ToolWorkflowChain
    ...     provideAdapter(ToolWorkflowChain,
    ...                    adapts=(IIssue, IWorkflowTool),
    ...                    provides=IWorkflowChain)

We fire up a browser (view) and do some basic checking::

    >>> from Products.Poi.browser.response import Base
    >>> view = Base(issue, request)
    >>> view.available_severities
    []
    >>> view.available_transitions
    []
    >>> view.available_releases
    <DisplayList [] at ...>
    >>> view.available_managers
    <DisplayList [] at ...>
    >>> bool(view.upload_allowed)
    False


We give ourselves all permissions.  This should mean there are more
available options for us now::

    >>> checkPermission = lambda perm, context: True
    >>> tracker.portal_membership.checkPermission = checkPermission
    >>> request = TestRequest()
    >>> view = Base(issue, request)
    >>> view.available_severities
    ('Critical', 'Important', 'Medium', 'Low')
    >>> view.available_transitions
    ['']
    >>> view.available_releases
    <DisplayList [('(UNASSIGNED)', u'not_assigned')] at ...>
    >>> view.available_managers
    <DisplayList [('(UNASSIGNED)', u'not_assigned'), ('manager', 'manager')] at ...>
    >>> view.upload_allowed
    True

The view has some methods especially for displaying::

    >>> view.transitions_for_display == [{'checked': 'checked', 'value': '', 'label': u'No change'}]
    True
    >>> view.releases_for_display == [{'checked': 'checked', 'value': '(UNASSIGNED)', 'label': u'not_assigned'}]
    True
    >>> view.managers_for_display == [{'checked': 'checked', 'value': '(UNASSIGNED)', 'label': u'not_assigned'}, {'checked': '', 'value': 'manager', 'label': 'manager'}]
    True

And of course the responses are visible.  Well, no response has been
added yet.

    >>> view.responses()
    []

We use a different browser view to add a response::

    >>> from Products.Poi.browser.response import Create
    >>> Create(issue, request)()
    >>> view.responses()
    []

Okay, that failed as we added no response text and did not make any
issue changes.  So we create a slightly more interesting response::

    >>> form = dict(response="This issue misses a shrubbery.",
    ...             severity='Low')
    >>> Create(issue, TestRequest(form=form))()
    >>> len(view.responses())
    1
    >>> res = view.responses()[-1]['response']
    >>> res.text
    'This issue misses a shrubbery.'
    >>> res.changes
    [{'before': 'Medium', 'after': 'Low', 'id': 'severity', 'name': u'Severity'}]
    >>> res.creator
    '(anonymous)'
    >>> res.date
    DateTime('...')
    >>> form = dict(response="We are no longer the Knights who say Ni.",
    ...             severity=u'Critical', responsibleManager='manager')
    >>> Create(issue, TestRequest(form=form))()
    >>> len(view.responses())
    2
    >>> res = view.responses()[-1]['response']
    >>> res.text
    'We are no longer the Knights who say Ni.'
    >>> res.changes
    [{'before': 'Medium', 'after': u'Critical', 'id': 'severity', 'name': u'Severity'}, {'before': '(UNASSIGNED)', 'after': 'manager', 'id': 'responsibleManager', 'name': u'Responsible manager'}]
    

Type of response
----------------

Responses have a type.  This can be used for showing them in a
different colour based on their type.  The default is 'additional'::

    >>> response = Response("text")
    >>> response.creator
    '(anonymous)'
    >>> res.type
    'additional'

Since we like to test this with fast unit tests, we need to set a
creator for the issue manually::

    >>> issue.Schema()['creators'].set(issue, "reporter")
    >>> issue.Creator()
    'reporter'

Within the Create view there is a method to determine the response
type.  Since the response was made anonymously, we stick to the
default::

    >>> form = dict(response="Please help.")
    >>> createview = Create(issue, TestRequest(form=form))
    >>> createview.determine_response_type(response)
    'additional'

The original reporter of the issue can make a clarification::

    >>> response.creator = 'reporter'
    >>> createview.determine_response_type(response)
    'clarification'

A manager can reply::

    >>> response.creator = 'manager'
    >>> createview.determine_response_type(response)
    'reply'

Other persons can give additional information::

    >>> response.creator = 'Maurits'
    >>> createview.determine_response_type(response)
    'additional'


Migration
---------

For tests of the migration from old style to new style responses, see
migration.txt.
