Browser tests
=============

These are browser-driven tests for the functionality provided by the
``collective.dancing`` package.

Setup
-----

  >>> from Testing.ZopeTestCase import user_password
  >>> from Products.Five.testbrowser import Browser
  >>> def new_browser():
  ...     browser = Browser()
  ...     browser.handleErrors = False
  ...     return browser
  >>> browser = new_browser()

  >>> from collective.dancing.tests import setup_error_log
  >>> print_error = setup_error_log(portal)

We want messages to be printed out instead of sending them:

  >>> import re
  >>> from zope import interface, component
  >>> import zope.sendmail.interfaces
  >>> from email.Header import decode_header
  >>> from collective.dancing.tests import decodeMessageAsString

  >>> class MyMailDelivery(object):
  ...     interface.implements(zope.sendmail.interfaces.IMailDelivery)
  ...     sent = []
  ...
  ...     def send(self, from_, to, message):
  ...         print '*MyMailDelivery sending*:'
  ...         print 'From:', decode_header(from_)[0][0]
  ...         print 'To:', to
  ...         print 'Message follows:'
  ...         decoded = decodeMessageAsString(message)
  ...         print decoded
  ...         self.sent.append(decoded)
  ... 
  ...     @classmethod
  ...     def last_messages(klass, purge=True):
  ...         klass.order()
  ...         value = '\n'.join(klass.sent)
  ...         if purge:
  ...             klass.sent = []
  ...         return value
  ... 
  ...     @classmethod
  ...     def order(klass):
  ...         klass.sent = sorted(
  ...             klass.sent,
  ...             key=lambda msg: re.findall('To: .*$', msg, re.MULTILINE))

  >>> component.provideUtility(MyMailDelivery())

Control panel
-------------

The ``collective.dancing`` package registers a control panel; we can
reach it as an administrator through the 'Site Setup' link:

  >>> browser.addHeader('Authorization',
  ...                   'Basic %s:%s' % ('portal_owner', user_password))
  >>> browser.open(portal.absolute_url())
  >>> browser.getLink('Site Setup').click()
  >>> browser.getLink('Singing & Dancing').click()
  >>> browser.url
  'http://nohost/plone/portal_newsletters'
  >>> "Singing &amp; Dancing configuration" in browser.contents
  True

Channels administration
-----------------------

The admistration screen can be reached through Plone control panel:

  >>> browser.getLink('Channel administration').click()

Through this administration screen we can add and delete channels:

  >>> browser.getControl('Title').value = 'My channel'
  >>> browser.getControl('Add').click()
  >>> 'Item added successfully' in browser.contents
  True
  >>> 'My channel' in browser.contents
  True 
  >>> browser.getControl('Title').value = 'My other channel'
  >>> browser.getControl('Add').click()
  >>> 'Item added successfully' in browser.contents
  True

Trying to rename a channel to the empty string will trigger a form
error:

  >>> browser.getControl(name='crud-edit.my-channel.widgets.title').value = ''
  >>> browser.getControl('Apply changes').click()
  >>> "There were some errors" in browser.contents
  True
  >>> "Required input is missing" in browser.contents
  True

Let's delete the first of the two channels:

  >>> btn = browser.getControl(name='crud-edit.my-channel.widgets.select:list')
  >>> btn.value = ['selected']
  >>> browser.getControl(name='crud-edit.buttons.delete').click()
  >>> "Successfully deleted items" in browser.contents
  True
  >>> 'My channel' in browser.contents, 'My other channel' in browser.contents
  (False, True)

The default is not to have a collector or a scheduler, let's select some for our
channel:

  >>> collector = browser.getControl(
  ...     name='crud-edit.my-other-channel.widgets.collector:list')
  >>> collector.displayValue
  ['no value']
  >>> collector.displayValue = ['Latest news']

  >>> scheduler = browser.getControl(
  ...     name='crud-edit.my-other-channel.widgets.scheduler:list')
  >>> scheduler.displayValue
  ['no value']
  >>> scheduler.displayValue = ['Weekly scheduler']

  >>> browser.getControl('Apply changes').click()
  >>> 'Successfully updated' in browser.contents
  True
  
  >>> collector = browser.getControl(
  ...     name='crud-edit.my-other-channel.widgets.collector:list')
  >>> collector.displayValue
  ['Latest news']
  >>> scheduler = browser.getControl(
  ...     name='crud-edit.my-other-channel.widgets.scheduler:list')
  >>> scheduler.displayValue
  ['Weekly scheduler']


Configuring collectors
----------------------

We can click on "Latest news" to configure what items go into the
channel:

  >>> browser.getLink('Latest news').click()

The collector administration screen shows an overview of all available
collectors:

  >>> browser.getLink('Up to Collector administration').click()
  >>> "Collector administration" in browser.contents
  True
  >>> browser.getLink('Latest news').click()

Our collector currently contains one Collection.  But instead of this, we
want it to contain two collectors, both of which contain one Collection.
This way we can mark both of the contained collectors optional and
have subscribers select from either.

Let's remove the Collection before we add two new collector blocks:

  >>> browser.getControl('Remove block').click()
  >>> browser.getControl('Title', index=1).value = 'News'
  >>> browser.getControl('Add').click()
  >>> browser.getControl('Title', index=4).value = 'Events'
  >>> browser.getControl('Add', index=-1).click()

We should now have two collectors containing one Collection each:

  >>> "Collection for News" in browser.contents
  True
  >>> "Collection for Events" in browser.contents
  True

We'll configure the first Collection, the one belonging to the News
Collector, to find us all News items:

  >>> def add_type_criterion(url, content_types):
  ...     browser.getLink(url=url).click()
  ...     browser.getControl('Field name', index=0).displayValue = ['Item Type']
  ...     browser.getControl('Criteria type').displayValue = ['Select content types']
  ...     browser.getControl('Add criteria').click()
  ...     types = browser.getControl(
  ...         name='crit__Type_ATPortalTypeCriterion_value:list')
  ...     types.value = content_types
  ...     browser.getControl(name='form.button.Save').click()
  ...     browser.getLink('Latest news').click()
  >>> add_type_criterion('0/0/criterion_edit_form', ['News Item'])
  >>> add_type_criterion('1/0/criterion_edit_form', ['Event'])

Back at the collector form, we'll make both 'News' and 'Events' blocks
optional.  This will allow users to select out of the two when they
subscribe.  The default is to subscribe to both:

  >>> browser.getControl(name='EditCollectorForm-plone-portal_newsletters-collectors-default-latest-news-0.widgets.optional:list').value = ['true']
  >>> browser.getControl(name='EditCollectorForm-plone-portal_newsletters-collectors-default-latest-news-1.widgets.optional:list').value = ['true']
  >>> browser.getControl('Apply').click()

We can also move blocks around:

  >>> def index(s):
  ...     return browser.contents.index(s)
  >>> index('Collector block: News') < index('Collector block: Events')
  True
  >>> browser.getControl('Move block down').click()
  >>> index('Collector block: News') < index('Collector block: Events')
  False

We can also add a rich text field:

  >>> browser.getControl('Type', index=-1).displayValue = ['Rich text']
  >>> browser.getControl('Title', index=-1).value = 'Goodbye'
  >>> browser.getControl('Add', index=-1).click()
  >>> browser.getControl(
  ...     name='EditTextForm-plone-portal_newsletters-collectors-default-latest-news-2.widgets.value').value = "Our newsletter rocks!"
  >>> browser.getControl('Apply').click()

Or a reference collector field:

  >>> browser.getControl('Type', index=-1).displayValue = ['Content selection']
  >>> browser.getControl('Title', index=-1).value = 'A selection of site content'
  >>> browser.getControl('Add', index=-1).click()
  >>> browser.getControl('Apply').click()
  
Okay, so now that our collector catches both events and news items
throughout the site.  For now, let's create one item for each type:

  >>> from DateTime import DateTime
  >>> news = portal.news
  >>> workflow = portal.portal_workflow
  >>> self.loginAsPortalOwner()
  >>> news.invokeFactory(
  ...     'News Item', id='flu', title='Drug-resistant flu rising, says WHO')
  'flu'
  >>> workflow.doActionFor(news['flu'], 'publish')

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

Channel subscriptions
---------------------

Going back to the channel administration screen, we can click on the
channel's name to reach the channel subscriptions screen:

  >>> browser.open(portal.absolute_url() + '/portal_newsletters/channels')
  >>> browser.getLink(url='http://nohost/plone/portal_newsletters/channels/my-other-channel').click()

We can add new subscriptions here:

  >>> browser.getControl('E-mail address').value = u"daniel@localhost"
  >>> browser.getControl('Add').click()
  >>> 'Item added successfully' in browser.contents
  True
  >>> 'daniel@localhost' in browser.contents
  True

We'll add another subscription.  This one filters on the content type:

  >>> browser.getControl('E-mail address').value = u"mailman@localhost"
  >>> browser.getControl('News', index=1).click()
  >>> browser.getControl('Add').click()
  >>> 'Item added successfully' in browser.contents
  True

Stats
-----

Also the statistics screens can be reached through the control panel:

  >>> browser.open('http://nohost/plone/portal_newsletters')
  >>> browser.getLink('Statistics').click()

We can see that our channel is listed here:

  >>> print browser.contents # doctest: +ELLIPSIS
  <!DOCTYPE...My other channel...0...0...0...0...0...

We can create a message now and see how the statistics reflect this.
First, let's get a hold of our subscription object:

  >>> channel = portal.portal_newsletters.channels.objectValues()[1]
  >>> subscription = channel.subscriptions.values()[0]
  >>> subscription # doctest: +ELLIPSIS
  <SimpleSubscription to ...>

We can now queue a new message:

  >>> from collective.singing import message
  >>> message.Message(payload=u"Hello, World!", subscription=subscription) \
  ... # doctest: +ELLIPSIS
  <collective.singing.message.Message object ...>

Et voila:

  >>> browser.reload()
  >>> print browser.contents #doctest: +ELLIPSIS
  <!DOCTYPE...My other channel...0...1...0...0...0...

The statistics screen allows us to also send queued messages.  Right
now, noone knows how to send text messages like the one we just
created.  We'll register an adapter to do that for us:

  >>> import collective.singing.interfaces

  >>> class MyTextDispatch(object):
  ...     interface.implements(collective.singing.interfaces.IDispatch)
  ...     component.adapts(unicode)
  ... 
  ...     failure = False
  ... 
  ...     def __init__(self, message):
  ...         self.message = message
  ... 
  ...     def __call__(self):
  ...         if self.failure:
  ...             return u'error', self.failure
  ...         print "Sending %r" % self.message
  ...         return u'sent', None

  >>> component.provideAdapter(MyTextDispatch)

Now we can select our channel and click the "Send messages now" button:

  >>> btn = browser.getControl(
  ...     name='crud-edit.my-other-channel.widgets.select:list')
  >>> btn.value = ['selected']
  >>> browser.getControl('Send messages now').click()
  Sending u'Hello, World!'
  >>> "1 message(s) sent" in browser.contents
  True

If sending the message fails, we'll get notified:

  >>> message.Message(payload=u"Hello, Aliens!", subscription=subscription) \
  ... # doctest: +ELLIPSIS
  <collective.singing.message.Message object ...>

  >>> MyTextDispatch.failure = u'Sorry, failed'
  >>> btn = browser.getControl(
  ...     name='crud-edit.my-other-channel.widgets.select:list')
  >>> btn.value = ['selected']
  >>> browser.getControl('Send messages now').click()
  >>> "1 failure(s)" in browser.contents
  True
  >>> "0 message(s) sent" in browser.contents
  True

Subscribe
---------

Every channel has a view that allows people to subscribe:

  >>> browser = new_browser()
  >>> browser.open(channel.absolute_url() + '/subscribe.html')
  >>> "My other channel" in browser.contents
  True

We'll subscribe to events only:

  >>> browser.getControl('E-mail address').value = u"root@localhost"
  >>> browser.getControl('Events').click()
  >>> browser.getControl('Finish').click() \
  ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
  *MyMailDelivery sending*:
  From: Site Administrator <>
  To: root@localhost
  Message follows:
  ...
  To confirm your subscription with My other channel, please click here:...
  
  >>> "Thanks for your subscription" in browser.contents
  True

  >>> import quopri
  >>> msg = quopri.decodestring(MyMailDelivery.sent[-1])
  >>> confirm_url = "http://nohost/plone/portal_newsletters/channels/my-other-channel/confirm-subscription.html"
  >>> confirm_url + "?secret=" in msg
  True
  >>> secret = msg[msg.rindex('secret='):msg.index('</a')]
  >>> secret.startswith('secret=')
  True

So now we've received an e-mail to confirm our subscription.  Using
the subscriptions query interface, we can see that our subscription is
*pending*:

  >>> subscriptions = list(
  ...     channel.subscriptions.query(secret=secret.split('=')[1]))
  >>> len(subscriptions)
  1
  >>> subscription = subscriptions[0]
  >>> subscription.metadata['pending']
  True
  >>> dict((subscription.composer_data))
  {'email': u'root@localhost'}

After confirming our subscription, the subscription is no longer
pending:

  >>> browser.open(confirm_url + '?' + secret)
  >>> "You confirmed your subscription successfully" in browser.contents
  True
  >>> subscription.metadata['pending']
  False

Trying to confirm a subscription that doesn't exist will give us a
meaningful message:

  >>> browser.open(confirm_url + '?secret=imfake')
  >>> "Your subscription isn't known to us" in browser.contents
  True

Trying to subscribe to the same channel and format with the same
e-mail address fails:

  >>> browser.open(channel.absolute_url() + '/subscribe.html')
  >>> browser.getControl('E-mail address').value = u"ROOT@localhost"
  >>> browser.getControl('Finish').click()
  >>> "Your email address is already subscribed." in browser.contents
  True

Users can also request an e-mail with their secret:

  >>> browser.getControl('Send my subscription details').click() \
  ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
  *MyMailDelivery sending*:
  From: Site Administrator <>
  To: root@localhost
  Message follows:
  ...
  Click here to manage your subscriptions...

Unsubscribe
-----------

For this, we'll quickly add another subscription:

  >>> browser.open(channel.absolute_url() + '/subscribe.html')
  >>> browser.getControl('E-mail address').value = u"daemon@localhost"
  >>> browser.getControl('Finish').click() \
  ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
  *MyMailDelivery sending*:
  From: Site Administrator <>
  To: daemon@localhost...

  >>> msg = quopri.decodestring(MyMailDelivery.sent[-1])
  >>> secret = msg[msg.rindex('secret='):msg.index('</a')]
  >>> browser.open(confirm_url + '?' + secret)
  >>> "You confirmed your subscription successfully" in browser.contents
  True

Now let's unsubscribe:

  >>> browser.open(channel.absolute_url() + '/unsubscribe.html?' + secret)
  >>> "You unsubscribed successfully" in browser.contents
  True

We're now no longer subscribed:

  >>> browser.open(channel.absolute_url() + '/unsubscribe.html?' + secret)
  >>> "You aren't subscribed to this channel" in browser.contents
  True

  >>> MyMailDelivery.sent = []

Using a scheduler to send e-mails
---------------------------------

The scheduler for our channel is the weekly scheduler:

  >>> channel.scheduler
  <WeeklyScheduler at /plone/portal_newsletters/channels/my-other-channel/scheduler>

Calling the scheduler's ``tick`` method will send messages to all our
subscribers.  Right now the scheduler is inactive though, i.e. it
won't trigger, ever:

  >>> from zope.publisher.browser import TestRequest
  >>> request = TestRequest()
  
  >>> channel.scheduler.tick(channel, request)

Let's make it active.  Now we can see that the messages are being
sent:

  >>> channel.scheduler.active = True
  >>> channel.scheduler.tick(channel, request)
  3
  >>> channel.queue.dispatch()
  *MyMailDelivery sending*:
  ...
  (3, 0)

  >>> print MyMailDelivery.last_messages(purge=False) \
  ... # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
  Content-Type: multipart/mixed;
  ...
  Subject: Plone site: My other channel...
  From: Site Administrator <>
  To: daniel@localhost
  ...Super Bowl XLII...Drug-resistant flu rising, says WHO...
  To: mailman@localhost
  ...Drug-resistant flu rising, says WHO...
  To: root@localhost
  ...Super Bowl XLII...
  Our newsletter rocks!
  ...

Let's take a closer look at the mail that's sent to mailman:

  >>> msg = quopri.decodestring(MyMailDelivery.sent[-2])
  >>> 'mailman@localhost' in msg
  True
  >>> "Super Bowl" in msg
  False
  >>> msg # doctest: +ELLIPSIS
  '...<a href="http://nohost/plone/portal_newsletters/channels/my-other-channel/unsubscribe.html?secret=...">...Click here to unsubscribe...</a>...'
  >>> neglect = MyMailDelivery.last_messages(purge=True)

Ticking the scheduler a second time won't do anything:

  >>> channel.scheduler.tick(channel, request)

This has two reasons really: Firstly, the scheduler keeps track of
when it queued something the last time:

  >>> import datetime
  >>> now = datetime.datetime.now()
  >>> channel.scheduler.triggered_last < now
  True
  >>> now - datetime.timedelta(hours=1) < channel.scheduler.triggered_last
  True

Secondly, all our subscriptions have a "cue" set, which marks the time
when they last received an item:

  >>> [s.metadata['cue'] for s in channel.subscriptions.values()] \
  ... # doctest: +ELLIPSIS
  [DateTime(...), DateTime(...), DateTime(...)]

We have to reset *both* the cues and the ``triggered_last`` time for
messages to be queued again:

  >>> for subs in subscriptions:
  ...     del subs.metadata['cue']
  >>> channel.scheduler.tick(channel, request)

  >>> channel.scheduler.triggered_last = datetime.datetime(1, 1, 1, 0, 0)
  >>> channel.scheduler.tick(channel, request)
  3
  >>> channel.queue.dispatch() # doctest: +ELLIPSIS
  *MyMailDelivery sending*:...(3, 0)
  >>> print MyMailDelivery.last_messages() # doctest: +ELLIPSIS
  Content-Type:...To: daniel@localhost...To: mailman@localhost...To: root@localhost...

We can choose another scheduler for our channel by visiting the
channel administration screen.  But for now, let's resubmit the form
with the same scheduler to make sure that its ``triggered_last`` date
is preserved:

  >>> triggered_last = channel.scheduler.triggered_last
  >>> browser.addHeader('Authorization',
  ...                   'Basic %s:%s' % ('portal_owner', user_password))
  >>> browser.open(channel.aq_parent.absolute_url())
  >>> select = browser.getControl(
  ...     name='crud-edit.my-other-channel.widgets.scheduler:list')
  >>> select.displayValue
  ['Weekly scheduler']
  >>> browser.getControl('Apply').click()
  >>> "No changes made" in browser.contents
  True
  >>> triggered_last == channel.scheduler.triggered_last
  True

Let's select the daily scheduler now:

  >>> select = browser.getControl(
  ...     name='crud-edit.my-other-channel.widgets.scheduler:list')
  >>> select.displayValue = ['Daily scheduler']
  >>> browser.getControl('Apply').click()
  >>> "Successfully updated" in browser.contents
  True

Again, that scheduler is inactive by default.  Also, the subscriptions
still have their cues set:

  >>> channel.scheduler.tick(channel, request)
  >>> for subs in subscriptions:
  ...     del subs.metadata['cue']
  >>> channel.scheduler.tick(channel, request)

We can visit the scheduler's management screen to activate it:

  >>> browser.getLink('Daily scheduler').click()
  >>> "Daily scheduler for My other channel" in browser.contents
  True
  >>> browser.getControl(name='form.widgets.active:list').value = ['true']
  >>> browser.getControl('Apply').click()
  >>> "Data successfully updated" in browser.contents
  True

The scheduler will send messages now:

  >>> channel.scheduler.tick(channel, request)
  3
  >>> channel.queue.dispatch() # doctest: +ELLIPSIS
  *MyMailDelivery sending*:...(3, 0)
  >>> print MyMailDelivery.last_messages() # doctest: +ELLIPSIS
  Content-Type:...To: daniel@localhost...To: mailman@localhost...To: root@localhost...

Now it's silent again:

  >>> for subs in subscriptions:
  ...     del subs.metadata['cue']
  >>> channel.scheduler.tick(channel, request)

We can manually trigger the scheduler and thereby override the time
check by using the "Trigger now" button:

  >>> browser.getControl('Trigger now').click()
  >>> "3 messages queued" in browser.contents
  True
  >>> channel.queue.dispatch() # doctest: +ELLIPSIS
  *MyMailDelivery sending*:...(3, 0)

  >>> print MyMailDelivery.last_messages() # doctest: +ELLIPSIS
  Content-Type:...To: daniel@localhost...To: mailman@localhost...To: root@localhost...


Sending newsletters from content objects
----------------------------------------

We can also send newsletters directly from any content object.  If the
channel has a collector, this will also send the collector's items.
To show this, we'll first create a new news item:

  >>> news.invokeFactory(
  ...     'News Item', id='vdfm',
  ...     title="Vickar's daughter foully murdered")
  'vdfm'
  >>> workflow.doActionFor(news['vdfm'], 'publish')

Let's verify that a CMF action was added.

  >>> browser.open(portal.absolute_url())

This just checks that there's a link to the form, but it's good enough
for now.
  
  >>> '/send-newsletter.html' in browser.contents
  True
  
We'll now send the front page as a newsletter:
  >>> previous_trigger_time = channel.scheduler.triggered_last
  >>> browser.open(portal.absolute_url() + '/front-page/send-newsletter.html')
  >>> browser.getControl('Newsletter').click()

When we click send, the newsletter(s) will be dispatched immediately.

  >>> browser.getControl('My other channel').click()
  >>> browser.getControl('Send', index=0).click()
  >>> "3 messages queued" in browser.contents
  True
  >>> channel.queue.dispatch() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
  *MyMailDelivery sending*:
  ...
  Welcome to Plone...Are you doing something interesting with Plone?...
  Vickar's daughter...

The scheduler last triggered time is also updated.

  >>> previous_trigger_time < channel.scheduler.triggered_last
  True
  
If we want we can avoid adding the collector items for selected channels,
We create a new item again...:

  >>> news.invokeFactory(
  ...     'News Item', id='allisquiet',
  ...     title="All is quiet on the western front")
  'allisquiet'
  >>> workflow.doActionFor(news['allisquiet'], 'publish')

  >>> previous_trigger_time = channel.scheduler.triggered_last
  >>> browser.open(portal.absolute_url() + '/front-page/send-newsletter.html')
  >>> browser.getControl('Newsletter').click()
  >>> browser.getControl('My other channel').click()

... and this time we choose not to add collector items...

  >>> browser.getControl(name='form.widgets.include_collector_items:list').value = ['false']
  >>> browser.getControl('Send', index=0).click()
  >>> "3 messages queued" in browser.contents
  True
  >>> channel.queue.dispatch() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
  *MyMailDelivery sending*:
  ...

... so our new News Item is not sent out.

  >>> MyMailDelivery.order()
  >>> msg = quopri.decodestring(MyMailDelivery.sent[-3])
  >>> 'mailman@localhost' in msg
  True
  >>> "All is quiet on the western front" in msg
  False
  >>> MyMailDelivery.sent = []

Since we did not include collector items the scheduler triggered time
is not updated:

  >>> previous_trigger_time == channel.scheduler.triggered_last
  True

We can also choose to send a preview of this item.  To do this, we'll
fill in our e-mail address and click the Preview button.  Note that
the cue time remains the same:

  >>> root = tuple(channel.subscriptions.query(key=u'root@localhost'))[0]
  >>> old_cue = root.metadata['cue']
  >>> browser.getControl('Address').value = u'root@localhost'
  >>> browser.getControl('Send preview').click() # doctest: +ELLIPSIS
  >>> "1 messages queued" in browser.contents
  True
  >>> channel.queue.dispatch() # doctest: +ELLIPSIS +NORMALIZE_WHITESPACE
  *MyMailDelivery sending*:
  ...
  (1, 0)
  >>> root = tuple(channel.subscriptions.query(key=u'root@localhost'))[0]
  >>> root.metadata['cue'] == old_cue
  True
  >>> previous_trigger_time == channel.scheduler.triggered_last
  True

If we use an address that's unknown, we'll get a nice error message:

  >>> browser.getControl('Address').value = u'nobody@localhost'
  >>> browser.getControl('Send preview').click() # doctest: +ELLIPSIS
  >>> "nobody@localhost is not subscribed" in browser.contents
  True

Finally we may request an in-browser preview.

  >>> browser.getControl('Newsletter').click()
  >>> browser.getControl('Show preview').click() # doctest: +ELLIPSIS
  >>> "Previewing &quot;Welcome to Plone&quot;" in browser.contents
  True

The "My subscriptions" page
---------------------------

This page allows users to manage their subscriptions.  Subscriptions
can be edited, removed and added.

Let's first step back and let's create another channel, along with its
own selector and composers.  We'll do this using the API to make this
test more compact:

  >>> from zope import schema
  >>> from zope.schema.vocabulary import SimpleVocabulary
  >>> from collective.dancing.channel import Channel
  >>> from collective.dancing.collector import Collector
  >>> from collective.dancing.composer import HTMLComposer

  >>> class MyHTMLComposer(HTMLComposer):
  ...     title = u'Hypertext E-Mail with selectable font-size'
  ...     class schema(HTMLComposer.schema):
  ...         font_size = schema.Choice(
  ...             title=u"Font size",
  ...             vocabulary=SimpleVocabulary.fromValues([8, 12, 16]))
  >>> __builtins__['MyHTMLComposer'] = MyHTMLComposer # make it persistable

  >>> selecta = portal.portal_newsletters.collectors['brand-new-selecta'] = \
  ...     Collector('brand-new-selecta', 'Brand new selecta')
  >>> new_channel = portal.portal_newsletters.channels['brand-new-channel'] = \
  ...     Channel('brand-new-channel', 'Brand new channel',
  ...             {'html': HTMLComposer(), 'html-fontsize': MyHTMLComposer()},
  ...             selecta)

We can now look at root's subscriptions.  Note how we add the secret
as a query parameter.  We expect the subscription to "My other
channel" to be shown:

  >>> secret = channel.composers['html'].secret(dict(email='root@localhost'))
  >>> browser.open(portal.absolute_url() +
  ...              '/portal_newsletters/my-subscriptions.html?secret=' + secret)
  >>> 'You are currently subscribed to these newsletters:' in browser.contents
  True
  >>> 'You can subscribe to these newsletters:' in browser.contents
  True
  >>> html1 = browser.contents[browser.contents.index('You are currently'):
  ...                          browser.contents.index('You can subscribe')]
  >>> 'My other channel' in html1, 'Brand new channel' in html1
  (True, False)
  >>> 'Unsubscribe' in html1
  True

We can subscribe to "Brand new channel":

  >>> html2 = browser.contents[browser.contents.index('You can subscribe'):]
  >>> 'My other channel' in html2, 'Brand new channel' in html2
  (False, True)

"Brand new channel" has two ways to subscribe.  This reflects the two
composers that we used:

  >>> 'HTML E-Mail' in html2
  True
  >>> 'Hypertext E-Mail with selectable font-size' in html2
  True

Let's use the form now to subscribe 'root' to both news items and events:

  >>> subscription = tuple(channel.subscriptions.query(secret=secret))[0]
  >>> [c.title for c in subscription.collector_data['selected_collectors']]
  [u'Events']
  >>> browser.getControl('News').click()
  >>> browser.getControl('Apply').click()
  >>> 'Your subscription was updated' in browser.contents
  True
  >>> sorted([c.title for c in subscription.collector_data['selected_collectors']])
  [u'Events', u'News']

Next, we'll subscribe to "Brand new channel".  We'll use the secretond
format, which is Hypertext with variable font-size:

  >>> len(new_channel.subscriptions.query(secret=secret))
  0
  >>> browser.getControl('E-mail address', index=1).value = u"root@localhost"
  >>> browser.getControl('Font size').getControl('16').click()
  >>> browser.getControl('Subscribe', index=1).click()
  >>> "You subscribed successfully" in browser.contents
  True
  >>> len(new_channel.subscriptions.query(secret=secret))
  1
  >>> new_subscription = tuple(
  ...     new_channel.subscriptions.query(secret=secret))[0]
  >>> new_subscription.composer_data['font_size']
  16
  >>> new_subscription.metadata.get('pending', False)
  False

No confirmation mail?  That's right.  Remember that we provided the
secret when we visited the subscriptions page.

Now let's unsubscribe from "My other channel":

  >>> browser.getControl('Unsubscribe').click() # doctest: +ELLIPSIS
  Traceback (most recent call last):
  ...
  AmbiguityError: label 'Unsubscribe'
  >>> browser.getControl('Unsubscribe', index=0).click()
  >>> "You unsubscribed successfully" in browser.contents
  True
  >>> len(channel.subscriptions.query(secret=secret))
  0

We could now subscribe to "My other channel" again:

  >>> html2 = browser.contents[browser.contents.index('You can subscribe'):]
  >>> 'My other channel' in html2
  True

We can use this same form to subscribe to a channel without a secret.
This will trigger the confirmation mail to be sent out:

  >>> browser.open(portal.absolute_url() +
  ...              '/portal_newsletters/my-subscriptions.html')
  >>> browser.getControl('E-mail address', index=1).value = "daemon@localhost"
  >>> browser.getControl('Subscribe', index=1).click()
  *MyMailDelivery sending*:
  From: Site Administrator <>
  To: daemon@localhost
  Message follows:
  ...
  To confirm your subscription with My other channel, please click here:...
  >>> "Information on how to confirm your subscription has been sent to you." in browser.contents
  True

  >>> secret = channel.composers['html'].secret(dict(email='daemon@localhost'))
  >>> len(channel.subscriptions.query(secret=secret))
  1
  >>> tuple(channel.subscriptions.query(secret=secret))[0].metadata['pending']
  True

The unpersonalized version of "My subscription" contains a form that
lets us retrieve an e-mail message with a link to the same page that
contains our secret:

  >>> browser.getControl("Address").value = "daemon@localhost"
  >>> browser.getControl('Send').click()
  *MyMailDelivery sending*:
  From: Site Administrator <>
  To: daemon@localhost
  Message follows:
  ...
  Click here to manage your subscriptions...

This feature is also available from a separate view, which is linked for all subscribe
pages. So the user is not forced to try subscribing first when she knows she already is.

  >>> browser.open(portal.absolute_url() + 
  ...              '/portal_newsletters/channels/default-channel/subscribe.html')
  >>> browser.getLink('edit your subscriptions').click()
  >>> 'portal_newsletters/sendsecret.html' in browser.url
  True
  >>> browser.getControl("Address").value = "daemon@localhost"
  >>> browser.getControl('Send').click()
  *MyMailDelivery sending*:
  From: Site Administrator <>
  To: daemon@localhost
  Message follows:
  ...
  Click here to manage your subscriptions...

  
Periodic ticking
----------------

Periodic tick'ing and dispatching happens from an outside time-source,
i.e. cron or zope clock-server. Really, anything that can call our
utility view as Manager.  The view will return a status message for
each channel.

  >>> browser.open(portal.absolute_url() + '/@@dancing.utils/tick_and_dispatch')
  >>> print browser.contents # doctest +NORMALIZE_WHITESPACE
  default-channel: 0 messages queued, dispatched: (0, 0)
  my-other-channel: 0 messages queued, dispatched: (0, 0)
  brand-new-channel: 0 messages queued, dispatched: (0, 0)
