Collector
=========

Setup
-----

See ``collective.singing.interfaces.ICollector``

  >>> from collective.dancing.tests import replace_with_fieldindex
  >>> replace_with_fieldindex('created', portal)
  >>> replace_with_fieldindex('effective', portal)

Give me a list of all collectors
--------------------------------

Collectors are stored separately from channels, since they can be
useful for more than one channel at the same time.  The "Collector
Vocabulary" provides us with a list of available collectors.  Luckily,
some default collectors are created at install time:

  >>> from zope import interface
  >>> from zope import component
  >>> from zope.schema.interfaces import IVocabularyFactory
  >>> factory = component.getUtility(
  ...     IVocabularyFactory, u"Collector Vocabulary")
  >>> vocab = factory(None)
  >>> for term in vocab: # doctest: +NORMALIZE_WHITESPACE
  ...     print '%s: %r' % (term.token, term.value)
  /plone/portal_newsletters/collectors/default-latest-news:
    <Collector at /plone/portal_newsletters/collectors/default-latest-news>

Getting items from a collector
------------------------------

Collectors implement the ``get_item`` method, which returns a tuple of
the form ``(items, cue)``.  See
``collective.singing.interfaces.ICollector``.


  >>> from Products.ATContentTypes.content.topic import ATTopic
  >>> collector = term.value
  >>> for child in collector.objectValues():
  ...     if isinstance(child, ATTopic):
  ...       child.setSortCriterion('effective', False)
  >>> items, cue = collector.get_items()
  >>> len(items)
  0

To have this default collector return items, we'll need to add a news
item in the site:

  >>> from DateTime import DateTime
  >>> news = self.portal.news
  >>> workflow = self.portal.portal_workflow
  >>> self.loginAsPortalOwner()
  >>> news.invokeFactory(
  ...     'News Item', id='flu', title='Drug-resistant flu rising, says WHO')
  'flu'
  >>> news['flu'].update(effectiveDate=DateTime()-10.0/24/60)  # effective 10 minutes ago
  >>> workflow.doActionFor(news['flu'], 'publish')

  >>> items, cue = collector.get_items()
  >>> items
  [<ATNewsItem at /plone/news/flu>]

By passing the retrieved cue to the ``get_items`` method, we restrict
the results to items that are newer than the last time we called the
method.  Let's create another news item to verify that:

  >>> news.invokeFactory(
  ...     'News Item', id='mini', title='The miniskirt is back again')
  'mini'
  >>> news['mini'].update(effectiveDate=DateTime()-10.0/24/60)  # effective 10 minutes ago
  >>> workflow.doActionFor(news['mini'], 'publish')

Additionaly create News Item which is going to be published 4 days from now.

  >>> news.invokeFactory(
  ...     'News Item', id='later', title='This should be visible later only')
  'later'
  >>> news['later'].update(effectiveDate=DateTime()+4)
  >>> workflow.doActionFor(news['later'], 'publish')

Let's imagine we have a cue that we retrieved yesterday.  When using
it, we'll get two currently available items only. The third item (later) must 
not be visible:

  >>> items, cue = collector.get_items(cue=cue-1)
  >>> items
  [<ATNewsItem at /plone/news/flu>, <ATNewsItem at /plone/news/mini>]

"Later" item is not visible until its publication date.

  >>> items, cue = collector.get_items(cue=cue+3)
  >>> items
  []

But the "later" item will be available after its publication date.

  >>> items, cue = collector.get_items(cue=cue+5)
  >>> items
  [<ATNewsItem at /plone/news/later>]
  
  Remove "later" item.
  
  >>> news.manage_delObjects(['later'])

The collector also implements a ``schema`` attribute.  See the
``ICollector`` interface.  Our default collector returns an empty
schema:

  >>> collector.schema.names()
  []

Criterions for everyone
-----------------------

The collector currently contains one topic.  To be able to make parts
of the newsletter optional for subscribers, we'll need to have each
part in its own collector, and then set the collectors to be optional
later.

  >>> collector.manage_delObjects(['0'])
  >>> import collective.dancing.collector
  >>> def add_collector(context, title):
  ...     name = collector.get_next_id()
  ...     collector[name] = collective.dancing.collector.Collector(name, title)
  ...     return collector[name]
  >>> events_collector = add_collector(collector, u'Events')
  >>> news_collector = add_collector(collector, u'News')

  >>> def add_criterions(topic, type):
  ...     type_crit = topic.addCriterion('Type', 'ATPortalTypeCriterion')
  ...     type_crit.setValue(type)
  ...     sort_crit = topic.addCriterion('created', 'ATSortCriterion')
  ...     state_crit = topic.addCriterion('review_state',
  ...                                     'ATSimpleStringCriterion')
  ...     state_crit.setValue('published')
  ...     topic.setSortCriterion('created', True)

  >>> add_criterions(events_collector['0'], 'Event')
  >>> add_criterions(news_collector['0'], 'News Item')

Let's see if a new event is returned by the collector now:

  >>> events = self.portal.events
  >>> events.invokeFactory(
  ...     'Event', id='super-bowl',
  ...     title='Super Bowl XLII')
  'super-bowl'
  >>> workflow.doActionFor(events['super-bowl'], 'publish')

  >>> items, cue = collector.get_items()
  >>> len(items)
  3
  >>> events['super-bowl'] in items 
  True
  
We can signal that we want one or more collectors to be made available
for further restriction by the user.  The ``optional`` attribute.
Let's set it so that users can select out of the two available
collectors the ones they'd like to have in their newsletter:

  >>> [subcollector.optional for subcollector in collector.objectValues()]
  [False, False]
  >>> for subcollector in collector.objectValues():
  ...     subcollector.optional = True

The schema of our collector now includes a choice field for the
collectors:

  >>> field = collector.schema['selected_collectors']
  >>> [t.title for t in field.value_type.vocabulary]
  [u'Events', u'News']

The ``collective.singing.interfaces.ICollectorData`` mapping gives us
information about what choices a subscriber made.  See the interface
for details.

Let's first define a subscription class along with an adapter that
gives us the data that the collector looks for to include subcollectors:

  >>> class Subscription(object):
  ...     def __init__(self):
  ...         self.collector_data = {}

First of all, it should be fine to call ``get_items`` with a
subscriber that does *not* have any collector data, i.e. he hasn't
made any choices.  This will return the same items as before:

  >>> items, cue = collector.get_items(subscription=Subscription())
  >>> items # doctest: +NORMALIZE_WHITESPACE
  [<ATEvent at /plone/events/super-bowl>, <ATNewsItem at /plone/news/mini>,
   <ATNewsItem at /plone/news/flu>]

We can provide both a cue and a subscription:

  >>> items, cue = collector.get_items(cue=cue-1, subscription=Subscription())
  >>> items # doctest: +NORMALIZE_WHITESPACE
  [<ATEvent at /plone/events/super-bowl>, <ATNewsItem at /plone/news/mini>,
   <ATNewsItem at /plone/news/flu>]

  >>> len(collector.get_items(cue=cue+1, subscription=Subscription())[0])
  0

Now comes the interesting part.  We'll say that our subscriber chose
to only see News:

  >>> subscription = Subscription()
  >>> subscription.collector_data[field.__name__] = [news_collector]
  >>> collector.get_items(subscription=subscription)[0]
  [<ATNewsItem at /plone/news/mini>, <ATNewsItem at /plone/news/flu>]

If we select no value, then we should get all items:

  >>> subscription.collector_data[field.__name__] = []
  >>> items, cue = collector.get_items(subscription=Subscription())
  >>> items # doctest: +NORMALIZE_WHITESPACE
  [<ATEvent at /plone/events/super-bowl>, <ATNewsItem at /plone/news/mini>,
   <ATNewsItem at /plone/news/flu>]

Significant items
-----------------

Collectors may be marked ``significant``.  This means that items from
the collector should always be included.  If significant is set to
``False``, we're signalling that the collector's items should only be
included if there are significant sibling collectors that returned
items.

Let's add to the events collector a bit of text:

  >>> name = events_collector.get_next_id()
  >>> events_collector[name] = collective.dancing.collector.TextCollector(
  ...     'text', 'Some text')
  >>> text_collector = events_collector[name]
  >>> text_collector.value = u"You just read our events section."

By default, this bit of text is not included if there are no events:

  >>> collector.get_items(subscription=Subscription())[0] \
  ... # doctest: +NORMALIZE_WHITESPACE
  [<ATEvent at /plone/events/super-bowl>,
   u'You just read our events section.',
   <ATNewsItem at /plone/news/mini>,
   <ATNewsItem at /plone/news/flu>]

  >>> collector.get_items(cue=cue+1, subscription=Subscription())[0]
  []

We'll now mark the text collector ``significant``.  With the effect
that the text is included now even if there are no events:

  >>> text_collector.significant = True

  >>> collector.get_items(subscription=Subscription())[0] \
  ... # doctest: +NORMALIZE_WHITESPACE
  [<ATEvent at /plone/events/super-bowl>,
   u'You just read our events section.',
   <ATNewsItem at /plone/news/mini>,
   <ATNewsItem at /plone/news/flu>]

  >>> collector.get_items(cue=cue+1, subscription=Subscription())[0]
  [u'You just read our events section.']

Reference collector
-------------------

The reference collector is able to keep a list of references to
content items.

  >>> from collective.dancing.collector import ReferenceCollector
  >>> rc = ReferenceCollector(
  ...     'example', u"Example reference collector").__of__(portal)

Let's set references to the items created above.

  >>> from persistent.wref import WeakRef
  >>> rc.items = map(WeakRef, items)

We can get back the items using the ``get_items`` method of the
collector.
  
  >>> items, cue = rc.get_items()
  >>> items
  (<ATEvent at /plone/events/super-bowl>,
   <ATNewsItem at /plone/news/mini>,
   <ATNewsItem at /plone/news/flu>)

Make sure the items are properly acquisition-wrapped:

  >>> event = items[0]
  >>> event.aq_chain[-1]
  <ZPublisher.BaseRequest.RequestContainer object at ...>
