Rating Views
============

Let's examine the default Rating Views, how they are intended to
opperate and how they may be customized.

The basic rating view looks for a specific vocabulary, though
subclasses can easily specify their own as we'll see soon.  The
view expects to adapt an IRatingManager object, so we'll provide
a category to operate on::

    >>> from contentratings.category import RatingsCategoryFactory
    >>> from contentratings.storage import EditorialRatingStorage
    >>> from contentratings.interfaces import IEditorialRating
    >>> from zope.interface import Interface
    >>> category = RatingsCategoryFactory(u"My Category", view_name=u'test',
    ...                                   storage=EditorialRatingStorage)
    >>> from zope.app.testing import ztapi
    >>> ztapi.provideAdapter((Interface,), IEditorialRating, category)
    >>> rating = IEditorialRating(my_container)

Let's instantiate our basic editorial view::

    >>> from contentratings.browser.basic import BasicEditorialRatingView
    >>> from zope.publisher.browser import TestRequest
    >>> request = TestRequest()
    >>> view = BasicEditorialRatingView(rating, request)


Vocabularies
------------

The view uses named vocabulary, there isn't one currently registered though::

    >>> view.vocabulary is None
    True

Let's register a simple one:

    >>> from contentratings.browser.base_vocabs import titled_vocab
    >>> vocab = titled_vocab(((0, 'Rejected'), (1, 'Accepted')))
    >>> from zope.schema.interfaces import IVocabularyTokenized
    >>> ztapi.provideUtility(IVocabularyTokenized, vocab,
    ...               name='contentratings.browser.base_vocabs.five_star_vocab')


    >>> [(t.value, t.title) for t in view.vocabulary]
    [(0, 'Rejected'), (1, 'Accepted')]

A subclass or even a particular instance can use a different named vocabulary::

    >>> vocab = titled_vocab(((0, 'No'), (1, 'Yes')))
    >>> ztapi.provideUtility(IVocabularyTokenized, vocab,
    ...                      name='my_vocabs.simple_vocab')
    >>> view.vocab_name = 'my_vocabs.simple_vocab'
    >>> [(t.value, t.title) for t in view.vocabulary]
    [(0, 'No'), (1, 'Yes')]


Common Properties
-----------------

The view provides a "content_url" property which gets the content's url,
it uses Zope 3's IAbsoluteURL interface if available, otherwise
it falls back on looking for Zope 2's 'absolute_url' method::

    >>> view.content_url is None
    True


    >>> def absolute_url():
    ...     return 'My "absolute" url'
    >>> my_container.absolute_url = absolute_url
    >>> view.content_url
    'My "absolute" url'


    >>> def url(context, request):
    ...     return 'A fake URL'
    >>> from zope.publisher.interfaces.browser import IBrowserRequest
    >>> from zope.traversing.browser.interfaces import IAbsoluteURL
    >>> from zope.app.container.sample import SampleContainer
    >>> ztapi.provideAdapter((SampleContainer, IBrowserRequest), IAbsoluteURL,
    ...                      url)
    >>> view.content_url
    'A fake URL'


Editorial Ratings
-----------------

The view provides a rate method, which simply sets the rating and optionally
redirects back to the content url.  When told not to redirect, it returns
a status message::

    >>> rating.rating is None
    True
    >>> view.rate(1, redirect=False)
    u'The rating has been changed'
    >>> float(rating.rating)
    1.0

A redirect just changes response headers::

    >>> request.response.getHeaders()
    [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)')]
    >>> view.rate(0)
    u'The rating has been changed'
    >>> float(rating.rating)
    0.0
    >>> request.response.getHeaders()
    [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'), ('Location', 'A fake URL')]

The view ensures that input values are valid in the vocabulary::

    >>> view.rate(3) # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    AssertionError

And the manager ensures that the category conditions are checked::

    >>> category.write_expr = 'python:False'
    >>> view.rate(1) # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    AssertionError
    >>> category.write_expr = None
    >>> view.rate(1)
    u'The rating has been changed'


The User Rating View
====================

The User Rating view is slightly more complex than the editorial one
explored in `views.txt`.  We again need a category, but we want to use
a custom rating manager, which has useful userids::

    >>> category = RatingsCategoryFactory(u"User Rating", view_name=u'test')
    >>> from contentratings.category import RatingCategoryAdapter
    >>> class CustomManager(RatingCategoryAdapter):
    ...     userid = None
    >>> rating = CustomManager(category, my_container)

Now that we've got our special manager, let's create our view::

    >>> from contentratings.browser.basic import BasicUserRatingView
    >>> request = TestRequest()
    >>> view = BasicUserRatingView(rating, request)


As Anonymous
------------

The view provides a can_rate attribute which determines if the current
user can rate the object.  It checks the categories write expression::

    >>> view.can_rate
    True
    >>> category.write_expr = 'python:False'
    >>> view.can_rate
    False
    >>> category.write_expr = None

It also has some checks to ensure anonymous users don't rate twice,
but since we didn't rate anything yet, it doesn't come into play.  So
let's rate something::

    >>> view.rate(0)
    u'You have changed your rating'
    >>> rating.averageRating
    0.0
    >>> view.rate(1)
    u'You have changed your rating'
    >>> rating.averageRating
    0.5

Oh no!  The anonymous user has rated the content twice.  This view
relies on an anonymous session generation utility, we can provide a
simplified one::

    >>> class BogusSession(object):
    ...     def get_anon_key(self, request):
    ...         return 'A Session'
    >>> from contentratings.browser.interfaces import IAnonymousSession
    >>> ztapi.provideUtility(IAnonymousSession, BogusSession())

Now we try it again (note that we can rate one more time to establish
our session)::

    >>> view.rate(0)
    u'You have changed your rating'
    >>> rating.averageRating
    0.33333333333333331
    >>> view.rate(1)
    u'You have already rated this item, and cannot change your rating unless you log in.'
    >>> view.can_rate
    False

Much better.  The time period after which a user with a given session
can rate again is determined by a variable on the view class, which
we can override.  If we set a very short timeout, the user can rate again
once it has been exceeded::

    >>> from time import sleep
    >>> from datetime import timedelta
    >>> view.ANON_TIMEOUT = timedelta(0,0,50000) # 50 msec = 50000 usec
    >>> sleep(0.1)
    >>> view.can_rate
    True
    >>> view.rate(1)
    u'You have changed your rating'
    >>> view.rate(0) # race condition, but this should never take 50 ms
    u'You have already rated this item, and cannot change your rating unless you log in.'
    >>> view.can_rate
    False
    >>> sleep(0.1)
    >>> view.can_rate
    True

Reset the timeout::

    >>> del view.ANON_TIMEOUT


Authenticated
-------------

Authenticated ratings don't need this extra checking, and so are a bit
simpler.  First we need to "login", which our special rating manager
makes easy::

    >>> rating.userid = 'me'


Now we look at our rating, then set it.  Notice that the view provides
a property for looking at our personal rating, and a method for
removing our rating::

    >>> rating.userRating('me') is None
    True
    >>> view.rate(1)
    u'You have changed your rating'
    >>> float(rating.userRating('me'))
    1.0
    >>> float(view.current_rating)
    1.0


    >>> view.rate(0)
    u'You have changed your rating'
    >>> float(rating.userRating('me'))
    0.0
    >>> float(view.current_rating)
    0.0


    >>> view.remove_rating()
    u'You have removed your rating'
    >>> view.current_rating is None
    True
    >>> rating.userRating('me') is None
    True

The view uses the rating manager to enforce security; however, it bypasses
the security to allow the user to always view his own rating::

    >>> view.rate(1)
    u'You have changed your rating'
    >>> category.write_expr = "python:False"
    >>> view.rate(0) # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    AssertionError


    >>> category.read_expr = "python:False"
    >>> rating.userRating('me') # doctest: +ELLIPSIS
    Traceback (most recent call last):
    ...
    AssertionError
    >>> float(view.current_rating)
    1.0
