import cStringIO
import datetime
from DateTime import DateTime
import sys
import sets
import csv
from zope import interface
from zope import component
from zope import schema
import zope.interface
import zope.schema.interfaces
import zope.schema.vocabulary
import zope.i18n
from z3c.form import field, form
import z3c.form.interfaces
import z3c.form.datamanager
import z3c.form.term
import z3c.form.button
import OFS.SimpleItem
from Globals import DevelopmentMode
from Products.Five import BrowserView
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.CMFCore.interfaces import IPropertiesTool
import Products.CMFPlone.utils
from collective.singing.interfaces import IChannel, IFormLayer, ICollectorSchema
from plone.z3cform import z2
from plone.z3cform.crud import crud
from plone.app.z3cform import wysiwyg
import collective.singing.scheduler
import collective.singing.subscribe
import collective.singing.channel
from zope.app.pagetemplate import viewpagetemplatefile
from collective.dancing import MessageFactory as _
from collective.dancing import collector
from collective.dancing import utils
from collective.dancing import channel
from collective.dancing.composer import check_email
from collective.dancing.browser import controlpanel
from collective.dancing.browser.interfaces import ISendAndPreviewForm
def simpleitem_wrap(klass, name):
class SimpleItemWrapper(klass, OFS.SimpleItem.SimpleItem):
__doc__ = OFS.SimpleItem.SimpleItem.__doc__
id = name
def Title(self):
return klass.title
klassname = klass.__name__
SimpleItemWrapper.__name__ = klassname
module = sys.modules[__name__]
assert not hasattr (module, klassname), "%r already a name in this module."
setattr(module, klassname, SimpleItemWrapper)
return SimpleItemWrapper
schedulers = [
simpleitem_wrap(klass, 'scheduler')
for klass in collective.singing.scheduler.schedulers]
csv_delimiter = ","
class FactoryChoice(schema.Choice):
def _validate(self, value):
if self._init_field:
return
super(schema.Choice, self)._validate(value)
# We'll skip validating against the vocabulary
def scheduler_vocabulary(context):
terms = []
for factory in schedulers:
terms.append(
zope.schema.vocabulary.SimpleTerm(
value=factory(),
token='%s.%s' % (factory.__module__, factory.__name__),
title=factory.title))
return utils.LaxVocabulary(terms)
zope.interface.alsoProvides(scheduler_vocabulary,
zope.schema.interfaces.IVocabularyFactory)
class ChannelEditForm(crud.EditForm):
def _update_subforms(self):
self.subforms = []
for channel in collective.singing.channel.channel_lookup():
subform = crud.EditSubForm(self, self.request)
subform.content = channel
subform.content_id = channel.name
subform.update()
self.subforms.append(subform)
class ManageChannelsForm(crud.CrudForm):
"""Crud form for channels."""
description = _("Add or edit mailing-lists that will use collectors to "
"gather and email specific sets of information from "
"your site, to subscribed email addresses, at scheduled "
"times.")
editform_factory = ChannelEditForm
@property
def add_schema(self):
if len(channel.channels) > 1:
return self.update_schema + field.Fields(
schema.Choice(
__name__='factory',
title=_(u"Type"),
vocabulary=zope.schema.vocabulary.SimpleVocabulary(
[zope.schema.vocabulary.SimpleTerm(value=c, title=c.type_name)
for c in channel.channels])
))
return self.update_schema
@property
def update_schema(self):
fields = field.Fields(IChannel).select('title')
collector = schema.Choice(
__name__='collector',
title=IChannel['collector'].title,
required=False,
vocabulary='Collector Vocabulary')
scheduler = FactoryChoice(
__name__='scheduler',
title=IChannel['scheduler'].title,
required=False,
vocabulary='Scheduler Vocabulary')
fields += field.Fields(collector, scheduler)
fields += field.Fields(IChannel).select('subscribeable')
return fields
@property
def view_schema(self):
return self.update_schema.copy()
def get_items(self):
return collective.singing.channel.channel_lookup()
def add(self, data):
name = Products.CMFPlone.utils.normalizeString(
data['title'].encode('utf-8'), encoding='utf-8')
factory = data.get('factory', None) or channel.channels[0]
self.context[name] = factory(
name, data['title'],
collector=data['collector'],
scheduler=data['scheduler'])
return self.context[name]
def remove(self, (id, item)):
self.context.manage_delObjects([id])
def link(self, item, field):
if field == 'title':
return item.absolute_url()
elif field == 'collector' and item.collector is not None:
collector_id = item.collector.getId()
collector = getattr(self.context.aq_inner.aq_parent.collectors,
collector_id)
return collector.absolute_url()
elif field == 'scheduler':
if item.scheduler is not None:
return item.scheduler.absolute_url()
class ChannelAdministrationView(BrowserView):
__call__ = ViewPageTemplateFile('controlpanel.pt')
label = _(u"label_channel_administration",
default="Mailing-list administration")
back_link = controlpanel.back_to_controlpanel
def contents(self):
# A call to 'switch_on' is required before we can render z3c.forms.
z2.switch_on(self)
return ManageChannelsForm(self.context.channels, self.request)()
class SubscriptionsSearchForm(z3c.form.form.Form):
prefix = 'search.'
ignoreContext = True
fields = field.Fields(
schema.TextLine(
__name__='fulltext',
title=_(u"Search subscribers"),
))
@z3c.form.button.buttonAndHandler(_('Search'), name='search')
def handle_search(self, action):
pass
class ManageSubscriptionsFormEdit(crud.EditForm):
def update(self):
super(ManageSubscriptionsFormEdit, self).update()
self.search = SubscriptionsSearchForm(self.context, self.request)
self.search.update()
def render(self):
table = super(ManageSubscriptionsFormEdit, self).render()
name = self.search.widgets['fulltext'].name
search = self.request.form.get(name)
if search and table.strip():
idx = table.find('')
hidden = ('' %
(name, search))
table = table[:idx] + hidden + table[idx:]
return ('
%s
' % self.search.render() +
table)
class ManageSubscriptionsForm(crud.CrudForm):
"""Crud form for subscriptions.
"""
# These are set by the SubscriptionsAdministrationView
format = None
composer = None
description = _(u"Manage or add subscriptions.")
editform_factory = ManageSubscriptionsFormEdit
@property
def batch_size(self):
if self._fulltext_query():
# We don't support batching when we search
return 0
else:
return 30
@property
def prefix(self):
return self.format
def _composer_fields(self):
return field.Fields(self.composer.schema)
def _collector_fields(self):
if self.context.collector is not None:
return field.Fields(self.context.collector.schema)
return field.Fields()
def _fulltext_query(self):
return self.request.form.get('search.widgets.fulltext')
@property
def update_schema(self):
fields = self._composer_fields()
fields += self._collector_fields()
return fields
def get_items(self):
items = []
query = dict(format=self.format)
search = self._fulltext_query()
if search:
query['fulltext'] = search
try:
subscriptions = self.context.subscriptions.query(**query)
except:
print 'fragment that is not cataloged'
query['fulltext'] = query['fulltext'] + '*'
subscriptions = self.context.subscriptions.query(**query)
emails = [i.composer_data['email'] for i in subscriptions]
emails.sort()
for email in emails:
for i in subscriptions:
if i.composer_data['email'] == email:
subscription = i
if subscription.metadata['format'] == self.format:
items.append((str(subscription.secret), subscription))
return items
def add(self, data):
secret = collective.singing.subscribe.secret(
self.context, self.composer, data, self.request)
composer_data = dict(
[(name, value) for (name, value) in data.items()
if name in self._composer_fields()])
collector_data = dict(
[(name, value) for (name, value) in data.items()
if name in self._collector_fields()])
metadata = dict(format=self.format,
date=datetime.datetime.now())
try:
return self.context.subscriptions.add_subscription(
self.context, secret, composer_data, collector_data, metadata)
except ValueError, e:
raise schema.ValidationError(e.args[0])
def remove(self, (secret, item)):
subs = self.context.subscriptions.query(secret=secret,
format=item.metadata['format'])
for subscription in subs:
self.context.subscriptions.remove_subscription(subscription)
class SubscriptionChoiceFieldDataManager(z3c.form.datamanager.AttributeField):
# This nasty hack allows us to have the default IDataManager to
# use a different schema for adapting the context. This is
# necessary because the schema that
# ``collector.SmartFolderCollector.schema`` produces is a
# dynamically generated interface.
#
# ``collector.SmartFolderCollector.schema`` should rather produce
# an interface with fields that already have the right interface
# to adapt to as their ``interface`` attribute.
component.adapts(
collective.singing.subscribe.SimpleSubscription,
zope.schema.interfaces.IField)
def __init__(self, context, field):
super(SubscriptionChoiceFieldDataManager, self).__init__(context, field)
if self.field.interface is not None:
if issubclass(self.field.interface, ICollectorSchema):
self.field.interface = ICollectorSchema
class ChannelPreviewForm(z3c.form.form.Form):
"""Channel preview form.
Currently only allows an in-browser preview.
"""
interface.implements(ISendAndPreviewForm)
template = viewpagetemplatefile.ViewPageTemplateFile('form.pt')
description = _(u"See an in-browser preview of the newsletter.")
fields = z3c.form.field.Fields(ISendAndPreviewForm).select(
'include_collector_items')
include_collector_items = True
def getContent(self):
return self
@z3c.form.button.buttonAndHandler(_(u"Generate"), name='preview')
def handle_preview(self, action):
data, errors = self.extractData()
if errors:
self.status = form.EditForm.formErrorsMessage
return
collector_items = int(bool(data['include_collector_items']))
return self.request.response.redirect(
self.context.absolute_url()+\
'/preview-newsletter.html?include_collector_items=%d' % \
collector_items)
class EditChannelForm(z3c.form.form.EditForm):
"""Channel edit form.
As opposed to the crud form, this allows editing of all channel
settings.
Actions are also provided to preview and send the newsletter.
"""
template = viewpagetemplatefile.ViewPageTemplateFile('form.pt')
description = _(u"Edit the properties of the mailing-list.")
@property
def fields(self):
fields = z3c.form.field.Fields(IChannel).select('title')
collector = schema.Choice(
__name__='collector',
title=IChannel['collector'].title,
required=False,
vocabulary='Collector Vocabulary')
scheduler = FactoryChoice(
__name__='scheduler',
title=IChannel['scheduler'].title,
required=False,
vocabulary='Scheduler Vocabulary')
fields += field.Fields(collector, scheduler)
fields += field.Fields(IChannel).select('description', 'subscribeable')
fields['description'].widgetFactory[
z3c.form.interfaces.INPUT_MODE] = wysiwyg.WysiwygFieldWidget
return fields
def parseSubscriberCSVFile(subscriberdata, composer,
header_row_present=False,
delimiter=csv_delimiter):
"""parses file containing subscriber data
returns list of dictionaries with subscriber data according to composer"""
properties = component.getUtility(IPropertiesTool)
charset = properties.site_properties.getProperty('default_charset',
'utf-8').upper()
try:
data = cStringIO.StringIO(subscriberdata)
reader = csv.reader(data, delimiter=str(delimiter))
subscriberslist = []
errorcandidates = []
for index, parsedline in enumerate(reader):
if index == 0:
if header_row_present:
fields = parsedline
continue
else:
fields = field.Fields(composer.schema).keys()
if len(parsedline) 0:
msg = _(u"${numberadded} subscriptions updated successfully, "
u"${numberremoved} removed!",
mapping=dict(numberadded=str(added),
numberremoved=str(removed))
)
else:
msg = _(u"${numberadded} subscriptions updated successfully!",
mapping=dict(numberadded=str(added),))
return msg
@z3c.form.button.buttonAndHandler(_('Upload'), name='upload')
def handle_add(self, action):
data, errors = self.extractData()
onlyremove = data.get('onlyremove', False)
remove = data.get('removenonexisting', False)
cannot_remove_onlyremove = (remove and onlyremove)
if errors or cannot_remove_onlyremove:
self.status = form.EditForm.formErrorsMessage
if cannot_remove_onlyremove:
self.status = _(
u'You can not add things in purge only mode!'
)
return
try:
self.status = self._addItem(data)
except Exception, e:
if DevelopmentMode:
raise
self.status = e
@z3c.form.button.buttonAndHandler(_('Download'), name='download')
def handle_download(self, action):
self.status = _(u"Subscribers exported.")
return self.request.response.redirect(self.mychannel.absolute_url() + \
'/export')
class ManageUploadForm(crud.CrudForm):
description = _(u"Upload list of subscribers.")
format = None
composer = None
editform_factory = crud.NullForm
addform_factory = UploadForm
@property
def prefix(self):
return self.format
class EditComposersForm(z3c.form.form.EditForm):
"""
"""
template = viewpagetemplatefile.ViewPageTemplateFile(
'form-with-subforms.pt')
subforms = []
ignoreContext = True
semiSuccesMessage = _(u"Only some of your changes were saved")
def update(self):
super(EditComposersForm, self).update()
self.update_subforms()
def update_subforms(self):
self.subforms = []
for format, item in self.context.composers.items():
subform = component.getMultiAdapter(
(item, self.request, self),
z3c.form.interfaces.ISubForm)
subform.format = format
subform.update()
self.subforms.append(subform)
@z3c.form.button.buttonAndHandler(_('Save'), name='save')
def handleSave(self, action):
self.status = ''
data, errors = self.extractData()
if errors:
self.status = self.formErrorsMessage
return
changes = self.applyChanges(data)
if not changes:
self.update_subforms()
stati = [f.status for f in self.subforms]
if self.successMessage in stati:
if self.formErrorsMessage not in stati:
self.status = self.successMessage
else:
self.status = self.semiSuccesMessage
elif self.formErrorsMessage in stati:
self.status = self.formErrorsMessage
if not self.status:
if changes:
self.status = self.successMessage
else:
self.status = self.noChangesMessage
class ManageChannelView(BrowserView):
"""Manage channel view.
Shows subscription, preview and edit options.
"""
__call__ = ViewPageTemplateFile('controlpanel.pt')
preview_form = ChannelPreviewForm
edit_form = EditChannelForm
composers_form = EditComposersForm
@property
def back_link(self):
return dict(label=_(u"Up to Mailing-lists administration"),
url=self.context.aq_inner.aq_parent.absolute_url())
@property
def label(self):
return _(u'Edit Mailing-list ${channel}',
mapping={'channel':self.context.title})
def contents(self):
# A call to 'switch_on' is required before we can render z3c.forms.
z2.switch_on(self,
request_layer=collective.singing.interfaces.IFormLayer)
fieldsets = []
# Add the subscriptions tab:
forms = []
for format, composer in self.context.composers.items():
form = ManageSubscriptionsForm(self.context, self.request)
form.format = format
form.composer = composer
forms.append(form)
form = ManageUploadForm(self.context, self.request)
form.format = format
form.composer = composer
forms.append(form)
fieldsets.append((_(u"Subscriptions"), '\n'.join([form() for form in forms])))
# Add edit, composers and preview tabs:
fieldsets.append((_(u"Edit"), self.edit_form(self.context, self.request)()))
fieldsets.append((_(u"Composers"), self.composers_form(self.context, self.request)()))
fieldsets.append((_(u"Preview"), self.preview_form(self.context, self.request)()))
wrapper = """
%s
"""
template = """
%s
%s
"""
return wrapper % \
("\n".join((template % (id(msg), zope.i18n.translate(msg, context=self.request), id(msg), html)
for (msg, html) in fieldsets)))