This is a series of tests for the behavior of the default rating
manager class.  It adapts a rating category, and rating storage, and
the content object being rated.  It is expected to provide many
of the attributes of the rating category, as well as implement
the api of the rating storage.  First we need to create a rating
manager::

    >>> from contentratings.tests.test_category import DummyStorage
    >>> from contentratings.category import RatingsCategoryFactory
    >>> category = RatingsCategoryFactory(title=u"foo", name=u"bar",
    ...                                view_name=u"my_view",
    ...                                storage=DummyStorage,
    ...                                description=u"baz", read_expr=u"python:4",
    ...                                write_expr=u"python:5", order=50)
    >>> from contentratings.category import RatingCategoryAdapter
    >>> manager = RatingCategoryAdapter(category, my_container)

Creating the manager should have assigned the category, context, and
created a storage::

    >>> manager.category is category
    True
    >>> isinstance(manager.storage, DummyStorage)
    True
    >>> manager.context is my_container
    True

The adaptation stores and instance of the rating storage as an
annotation on the content object.  We need to verify that storage,
and examine how the annotation can be controlled.  There is a default
annotation key where annotations are stored::

    >>> from zope.annotation.interfaces import IAnnotations
    >>> annotations = IAnnotations(my_container)
    >>> list(annotations.keys())
    ['contentratings.userrating.bar']

However, changing category name will change that key::

    >>> category.name = 'my_annotation'
    >>> manager = RatingCategoryAdapter(category, my_container)
    >>> list(annotations.keys())
    ['contentratings.userrating.bar', 'contentratings.userrating.my_annotation']

The storage may provide a hint regarding the annotation prefix, using a
`annotation_key` attribute::

    >>> DummyStorage.annotation_key = 'contentratings.dummy'
    >>> manager = RatingCategoryAdapter(category, my_container)
    >>> list(annotations.keys())
    ['contentratings.dummy.my_annotation', 'contentratings.userrating.bar', 'contentratings.userrating.my_annotation']

Finally the category itself may specify a `key` attribute which will
override the generated key.  This is primarily intended to support
backwards compatibility and should probably not be used directly::

    >>> category.key = 'contentratings.category_specific'
    >>> manager = RatingCategoryAdapter(category, my_container)
    >>> list(annotations.keys())
    ['contentratings.category_specific', 'contentratings.dummy.my_annotation', 'contentratings.userrating.bar', 'contentratings.userrating.my_annotation']

An instance of the storage should be contained in the annotation::

    >>> storage = annotations['contentratings.category_specific']
    >>> isinstance(storage, DummyStorage)
    True
    >>> manager.storage == storage
    True

Additionally, the rating manager constructor supports in place
automatic migration of storage.  If the storage in an annotation does
not match the category's storage factory then an adapter is looked up
to perform the migration.  Let's see how this works::

    >>> annotations['contentratings.category_specific'] = 'A String'

We have a string stored rather than a rating storage, though this
could also be another rating storage.  If we don't have a registered
handler, then we get an error because no migration was possible::

    >>> RatingCategoryAdapter(category, my_container) # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    ComponentLookupError: (('A String', <contentratings.tests.test_category.DummyStorage object at ...>), <InterfaceClass contentratings.interfaces.IRatingStorageMigrator>, u'')

If we have a registered migrator our storage gets migrated::

    >>> class DummyMigrator(object):
    ...     def __init__(self, orig, new):
    ...         self.orig = orig
    ...         self.new = new
    ...     def migrate(self):
    ...         self.new.dummy_attr = self.orig
    ...         return self.new
    >>> from zope.app.testing import ztapi
    >>> from contentratings.interfaces import IRatingStorageMigrator
    >>> from contentratings.tests.test_category import ITestRatingType
    >>> ztapi.provideAdapter((str, ITestRatingType), IRatingStorageMigrator,
    ...                      DummyMigrator)
    >>> manager = RatingCategoryAdapter(category, my_container)
    >>> storage = annotations['contentratings.category_specific']
    >>> isinstance(storage, DummyStorage)
    True
    >>> storage.dummy_attr
    'A String'


This rating manager should have introspected the IRatingType api provided
by our DummyStorage (ITestRatingType) and should be providing that api
transparently::

    >>> from contentratings.tests.test_category import ITestRatingType
    >>> ITestRatingType.implementedBy(RatingCategoryAdapter)
    False
    >>> ITestRatingType.providedBy(storage)
    True
    >>> ITestRatingType.providedBy(manager)
    True
    >>> from zope.interface.verify import verifyObject
    >>> verifyObject(ITestRatingType, manager)
    True

The manager also provides some attributes proxied from the category::

    >>> manager.title == category.title
    True
    >>> category.title = u'changed'
    >>> manager.title
    u'changed'

    >>> manager.description == category.description
    True
    >>> category.description = u'changed'
    >>> manager.description
    u'changed'

    >>> manager.order == category.order
    True
    >>> category.order = 200
    >>> manager.order
    200

    >>> manager.name == category.name
    True

    >>> manager.view_name
    u'my_view'
    >>> category.view_name = u'another_view'
    >>> manager.view_name
    u'another_view'

The manager provides an unimplemented hook for getting the userid::

    >>> manager.userid is None
    True

It also provides attributes which evaluate the expressions set on the category::

    >>> manager.can_read
    4
    >>> manager.can_write
    5
    >>> category.read_expr = 'python:True'
    >>> manager.can_read
    True

An empty expression will always return True::

    >>> category.read_expr = ''
    >>> manager.can_read
    True
    >>> category.write_expr = None
    >>> manager.can_write
    True

It proxies the IRatingType api from the storage::

    >>> manager.dummy_attr
    'A String'
    >>> manager.dummy_attr = 6
    >>> storage.dummy_attr
    6
    >>> manager.dummy_attr == storage.dummy_attr
    True
    >>> manager.dummy_method()
    'Dummy Method'
    >>> manager.dummy_method == storage.dummy_method
    True

It does not however proxy attributes or methods which are not part of
the IRatingType API::

    >>> manager.inaccessible_attribute
    Traceback (most recent call last):
    ...
    AttributeError: inaccessible_attribute
    >>> manager.inaccessible_attribute = False
    >>> manager.inaccessible_attribute
    False
    >>> storage.inaccessible_attribute
    True

The can_read attribute is checked before reading values from the storage::

    >>> category.read_expr = 'python:False'
    >>> manager.dummy_attr
    Traceback (most recent call last):
    ...
    AssertionError
    >>> category.read_expr = 'python:True'

The can_write attribute is checked before setting values on the storage::

    >>> category.write_expr = 'python:False'
    >>> manager.dummy_attr
    6
    >>> manager.dummy_attr = 7
    Traceback (most recent call last):
    ...
    AssertionError

There are some special methods and attribute names which trigger additional
behavior, for convenience.  The `rate` method will check the can_write
attribute before calling `rate` on the storage::

    >>> manager.rate(value=1, required=True)
    Traceback (most recent call last):
    ...
    AssertionError
    >>> category.write_expr = 'python:True'
    >>> manager.rate(value=1, required=True)
    'Rating: 1'

The same is true for `__setitem__`::

    >>> manager['my_user'] = 5
    item set
    >>> category.write_expr = 'python:False'
    >>> manager['another_user'] = 6
    Traceback (most recent call last):
    ...
    AssertionError

These methods, as well as the setting of the `rating` attribute, fire events
indicating that an object was rated.  Let's add a subscriber to these events
so that we can verify that they are being fired::

    >>> def subscriber(ob, ev):
    ...     print "Rating Event: %s, %s"%(ev.rating, ev.category)
    >>> from contentratings.interfaces import IObjectRatedEvent
    >>> from zope.interface import Interface
    >>> from zope.app.testing import ztapi
    >>> ztapi.subscribe((Interface, IObjectRatedEvent), None, subscriber)
    >>> category.write_expr = 'python:True'
    >>> manager.rating = 7
    Rating Event: 7, my_annotation
    >>> manager['my_user'] = 'abc'
    item set
    Rating Event: Dummy item, my_annotation
    >>> manager.rate('def', True)
    Rating Event: Rating: def, my_annotation
    'Rating: def'

Note that for the `__setitem__` call, the value sent to the event is
the result of `__getitem__` on the storage, and for `rate` it is the
return value of the `rate` function.  If the name passed to
`__setitem__` is None (e.g. for an anonymous rating), then the
`most_recent` attribute of the storage will be checked if it exists.
The same is true if the `rate` method returns None::

    >>> manager[None] = 5
    item set
    Rating Event: Dummy item, my_annotation
    >>> storage.most_recent = 'Recent'
    >>> manager[None] = 5
    item set
    Rating Event: Recent, my_annotation
    >>> def rate(value): return None
    >>> storage.rate = rate
    >>> manager.rate(5)
    Rating Event: Recent, my_annotation

There is a bit of error handling that we have yet to test.  If one of
the condition expressions raises or returns an error, that error is
raised during the attribute access::

    >>> category.read_expr = 'python:KeyError()'
    >>> manager.can_read
    Traceback (most recent call last):
    ...
    KeyError
    >>> category.write_expr = 'python:bad_name'
    >>> manager.can_write
    Traceback (most recent call last):
    ...
    NameError: name 'bad_name' is not defined

If the returned error is an AttributeError, or the expression itself
raises an AttributeError, a RuntimeError is raised instead, so that
the error does not erroneously appear as an AttributeError for the
can_read/write property itself:

    >>> category.write_expr = 'python:AttributeError()'
    >>> manager.can_write
    Traceback (most recent call last):
    ...
    RuntimeError
    >>> category.read_expr = 'python:context.bad_attribute'
    >>> manager.can_read
    Traceback (most recent call last):
    ...
    RuntimeError: 'SampleContainer' object has no attribute 'bad_attribute'
