Scheduler
=========

The ``scheduler`` module defines a couple of ``IScheduler``
implementations:

  >>> from collective.singing import scheduler
  >>> from collective.singing import interfaces
  >>> from zope.interface import verify
  >>> verify.verifyClass(interfaces.IScheduler, scheduler.DailyScheduler)
  True
  >>> verify.verifyClass(interfaces.IScheduler, scheduler.WeeklyScheduler)
  True

Schedulers have an overwritten ``__eq__`` special method for comparison:

  >>> scheduler.DailyScheduler() == scheduler.DailyScheduler()
  True
  >>> scheduler.WeeklyScheduler() == scheduler.DailyScheduler()
  False

Schedulers implement a ``tick`` method that'll call
``assemble_messages`` when it needs to:

  >>> from zope.publisher.browser import TestRequest
  >>> request = TestRequest()

  >>> daily = scheduler.DailyScheduler()
  >>> daily.tick(None, request)

Nothing happened because our scheduler is inactive by default:

  >>> daily.active = True
  >>> daily.tick(None, request) # doctest: +ELLIPSIS
  Traceback (innermost last):
  ...
  AttributeError: ...

Daily won't need to trigger that function when its ``triggered_last``
attribute is set to now:

  >>> import datetime
  >>> daily.triggered_last = datetime.datetime.now()
  >>> daily.tick(None, request)

We can override the check by calling ``trigger``:

  >>> daily.trigger(None, request) # doctest: +ELLIPSIS
  Traceback (innermost last):
  ...
  AttributeError: ...

If the ``triggered_last`` time is set to one day ago, the alarm will
go off:

  >>> daily.triggered_last = (
  ...     datetime.datetime.now() - datetime.timedelta(days=1))
  >>> daily.tick(None, request) # doctest: +ELLIPSIS
  Traceback (innermost last):
  ...
  AttributeError: ...

``assemble_messages`` takes an ``IChannel`` as its first argument;
it'll look up the channel's collector (if any) and the composer and
render messages.

At this point, we'll provide our own ``IChannel``, ``IComposer`` and
``ISubscription`` implementations to test that ``assemble_messages``
does the right thing:

  >>> class Channel(object):
  ...     def __init__(self, composers, subscriptions, collector=None):
  ...         self.composers = composers
  ...         self.subscriptions = subscriptions
  ...         self.collector = collector

  >>> class Subscription(object):
  ...     def __init__(self, name, metadata):
  ...         self.name = name
  ...         self.metadata = metadata
  ...     def __repr__(self):
  ...         return '<Subscription %r>' % self.name

  >>> class Composer(object):
  ...     def render(self, subscription, items=()):
  ...         print "Rendering message with %r for %r" % (tuple(items),
  ...                                                     subscription)
  ...         return '<Message>'

  >>> subscription = Subscription('daniel', dict(format='my-format'))
  >>> channel = Channel(
  ...     composers={'my-format': Composer()},
  ...     subscriptions={'my-subscription': subscription})

Note that the ``assemble_messages`` function returns the number of
messages that were created:

  >>> scheduler.assemble_messages(channel, request)
  Rendering message with () for <Subscription 'daniel'>
  1

If our subscription were in pending state, nothing would happen:

  >>> subscription.metadata['pending'] = True
  >>> scheduler.assemble_messages(channel, request)
  0
  >>> subscription.metadata['pending'] = False

If our subscription were for a format that's unknown, an error is
raised:

  >>> subscription.metadata['format'] = 'bar'
  >>> scheduler.assemble_messages(channel, request) # doctest: +ELLIPSIS
  Traceback (innermost last):
  ...
  KeyError: 'bar'
  >>> subscription.metadata['format'] = 'my-format'

Note that our channel lacks a collector; that's perfectly fine.  If
there is a collector however, it'll be asked for items to render the
message with:

  >>> class Collector(object):
  ...     items = ('some', 'items')
  ...     def get_items(self, cue=None, subscription=None):
  ...         print "Collecting items for %r with cue %r" % (subscription, cue)
  ...         if self.items:
  ...             items = self.items + ('for', subscription)
  ...         else:
  ...             items = ()
  ...         return items, 'somecue'

  >>> channel.collector = Collector()

Before we can render messages now, we need to tell scheduler how to
convert the items retrieved by the collector into something that the
composer can work with.

A ``UnicodeFormatter`` defined in the ``scheduler`` module returns
``unicode(item)`` for any given item.  We'll use that one specifically
for our ``my-format`` composer to render both strings and Subscription
objects to unicode:

  >>> from zope import component
  >>> from zope.publisher.interfaces.browser import IBrowserRequest
  
  >>> component.provideAdapter(scheduler.UnicodeFormatter,
  ...                          adapts=(str, IBrowserRequest))
  >>> component.provideAdapter(scheduler.UnicodeFormatter,
  ...                          adapts=(Subscription, IBrowserRequest))

  >>> scheduler.assemble_messages(channel, request)
  Collecting items for <Subscription 'daniel'> with cue None
  Rendering message with (u'some', u'items', u'for', u"<Subscription 'daniel'>") for <Subscription 'daniel'>
  1

Note that the second we call this, the cue we returned in the previous
call to ``get_items`` will be passed to the collector:

  >>> scheduler.assemble_messages(channel, request)
  Collecting items for <Subscription 'daniel'> with cue 'somecue'
  Rendering message with (u'some', u'items', u'for', u"<Subscription 'daniel'>") for <Subscription 'daniel'>
  1

If the collector decides to return no items, no messages will be
rendered:

  >>> channel.collector.items = ()
  >>> scheduler.assemble_messages(channel, request)
  Collecting items for <Subscription 'daniel'> with cue 'somecue'
  0

We can register an ``ITransform`` utility to rewrite text that's sent
out:

  >>> from zope import interface
  >>> class SimpleTransform(object):
  ...     interface.implements(interfaces.ITransform)
  ... 
  ...     def __init__(self, sub, stitue):
  ...         self.sub = sub
  ...         self.stitute = stitue
  ... 
  ...     def __call__(self, text, subscription):
  ...         return text.replace(self.sub, self.stitute)

  >>> channel.collector.items = ('We', 'love', 'life')
  >>> component.provideUtility(SimpleTransform(u'life', u'Grappa'))
  >>> scheduler.assemble_messages(channel, request)
  Collecting items for <Subscription 'daniel'> with cue 'somecue'
  Rendering message with (u'We', u'love', u'Grappa', u'for', u"<Subscription 'daniel'>") for <Subscription 'daniel'>
  1

We can supply our own items to assemble_messages:

  >>> items = ('Vincenzo', 'likes', 'life', 'and', 'singing')
  >>> scheduler.assemble_messages(channel, request, items)
  Collecting items for <Subscription 'daniel'> with cue 'somecue'
  Rendering message with (u'Vincenzo', u'likes', u'Grappa', u'and', u'singing', u'We', u'love', u'Grappa', u'for', u"<Subscription 'daniel'>") for <Subscription 'daniel'>
  1

... and choose not to include the collector items:
  
  >>> scheduler.assemble_messages(channel, request, items, use_collector=False)
  Rendering message with (u'Vincenzo', u'likes', u'Grappa', u'and', u'singing') for <Subscription 'daniel'>
  1
