def handle_forgot(self, action): data, errors = self.parent.extractData() if errors: self.parent.status = form.EditForm.formErrorsMessage return comp_data = {} for key, value in data.items(): name = key.split('.')[-1] if key.startswith('composer.'): comp_data[name] = value secret = self.parent._secret(comp_data, self.request) subs = self.context.subscriptions subscriptions = subs.query(secret=secret) if len(subscriptions) == 0: self.status = _(u"Your subscription isn't known to us.") else: subscription = tuple(subscriptions)[0] composer = self.context.composers[self.parent.format()] msg = composer.render_forgot_secret(subscription) status, status_msg = message.dispatch(msg) if status != u'sent': raise RuntimeError( "There was an error with sending your e-mail. Please try " "again later.") else: self.parent.status = _(u"Thanks. We sent you a message.")
class TimedScheduler(persistent.Persistent, AbstractPeriodicScheduler): title = _(u"Timed scheduler") active = True triggered_last = datetime.datetime(1970, 1, 1) def __init__(self): super(TimedScheduler, self).__init__() self.items = persistent.list.PersistentList() def tick(self, channel, request): return self.trigger(channel, request, manual=False) def trigger(self, channel, request, manual=True): count = 0 now = datetime.datetime.now() assembler = interfaces.IMessageAssemble(channel) if self.active or manual: self.triggered_last = now for when, content, override_vars in tuple(self.items): if manual or when < now: self.items.remove((when, content, override_vars)) if content is not None: count += assembler(request, (content(), ), override_vars=override_vars) else: count += assembler(request, override_vars=override_vars) return count def __eq__(self, other): return isinstance(other, TimedScheduler) def __ne__(self, other): return not self == other
class SubscribeStep(wizard.Step): prefix = 'composer' @property def fields(self): format = self.parent.format() return field.Fields(self.context.composers[format].schema) def update(self): self.subforms = (CollectorDataForm(self.context, self.request),) self.subforms[0].update() super(SubscribeStep, self).update() def extractData(self): sub_data, sub_errors = utils.extract_data_prefixed(self.subforms) data, errors = self.widgets.extract() data.update(sub_data) errors = errors + sub_errors return data, errors def _show_forgot_button(self): btn = self.buttons['forgot'] form = self.request.form button_name = '%s.buttons.%s' % (self.prefix, btn.__name__) return (self.parent.status == self.parent.already_subscribed_message or form.get(button_name)) @button.buttonAndHandler( _('Send my subscription details'), name='forgot', condition=lambda form: form._show_forgot_button()) def handle_forgot(self, action): data, errors = self.parent.extractData() if errors: self.parent.status = form.EditForm.formErrorsMessage return comp_data = {} for key, value in data.items(): name = key.split('.')[-1] if key.startswith('composer.'): comp_data[name] = value secret = self.parent._secret(comp_data, self.request) subs = self.context.subscriptions subscriptions = subs.query(secret=secret) if len(subscriptions) == 0: self.status = _(u"Your subscription isn't known to us.") else: subscription = tuple(subscriptions)[0] composer = self.context.composers[self.parent.format()] msg = composer.render_forgot_secret(subscription) status, status_msg = message.dispatch(msg) if status != u'sent': raise RuntimeError( "There was an error with sending your e-mail. Please try " "again later.") else: self.parent.status = _(u"Thanks. We sent you a message.")
class IScheduler(interface.Interface): """A scheduler triggers the sending of messages periodically. """ title = schema.TextLine(title=_(u"Title"), ) triggered_last = schema.Datetime(title=_(u"Triggered the last time"), ) active = schema.Bool(title=_(u"Active"), ) def tick(channel, request): """Check if messages need to be assembled and sent; return the number of messages queued or None. This method is guaranteed to be called periodically. """ def trigger(channel, request): """Assemble and queue messages; return the number of messages
def add_subscription( self, channel, secret, composerd, collectord, metadata): subscription = self.subscription_factory( channel, secret, composerd, collectord, metadata) data = ISubscriptionCatalogData(subscription) contained_name = u'%s-%s' % (data.key, data.format) if contained_name in self: raise ValueError(_("There's already a subscription for ${name}", mapping=dict(name=contained_name))) self[contained_name] = subscription return self[contained_name]
class IComposer(interface.Interface): """Composers will typically provide a user interface that lets you modify the look of the message rendered through it. """ name = schema.TextLine(title=_(u"The Composer's format, e.g. 'html'"), ) title = schema.TextLine( title=_(u"The Composer's title, e.g. 'HTML E-Mail'"), ) schema = schema.Object( title=_(u"A schema instance for use in the subscription form"), description=_(u"Values are stored via the IComposerData adapter per " "subscriber."), schema=IInterface, ) def render(subscription, items=(), override_vars=None): """Given a subscription and a list of items, I will create an IMessage and return it. The ``items`` argument is a list of 2-tuples of the form ``(formatted, original)``, where ``original`` is the item as it was retrieved from the collector, and ``formatted`` is the result of running the item through all applicable formatters and transforms. Making use of the ``original`` item will obviously bind the implementation of the composer to that of the collector. However, it's considered useful for custom implementations that need total control and that know what collector they'll be using. """ def render_confirmation(subscription): """Given a subscription, I will create an IMessage that the user has to react to in order to confirm the subscription, and return it. """ def render_forgot_secret(subscription): """Given a subscription, I will create an IMessage that links
class ISubscription(IAnnotatable): """A subscription to a channel. """ channel = schema.Object( title=_(u"The channel that we're subscribed to"), schema=IInterface, # should be really IChannel ) secret = schema.ASCIILine( title=_(u"The subscriber's secret, ideally unique across channels"), description=u"""\ Might be a hash of the subscriber's e-mail and a secret on the server in case we're dealing with an anonymous subscription. Used for unsubscription or for providing an overview of all of a subscriber's subscriptions. """, ) composer_data = schema.Dict(title=_(u"Composer data")) collector_data = schema.Dict(title=_(u"Collector data")) metadata = schema.Dict(title=_(u"Metadata"))
def add_subscription(self, channel, secret, composerd, collectord, metadata): subscription = self.subscription_factory(channel, secret, composerd, collectord, metadata) data = ISubscriptionCatalogData(subscription) contained_name = u'%s-%s' % (data.key, data.format) if contained_name in self: raise ValueError( _("There's already a subscription for ${name}", mapping=dict(name=contained_name))) self[contained_name] = subscription return self[contained_name]
def fields(self): composers = self.context.composers terms = Terms([ zope.schema.vocabulary.SimpleTerm( name, title=composers[name].title) for name in sorted(composers.keys()) ]) format = zope.schema.Choice( __name__='format', title=_(u'Format'), vocabulary=terms) return field.Fields(format)
class CollectorDataForm(utils.OverridableTemplate, form.Form): """A subform for the collector specific data. """ index = viewpagetemplatefile.ViewPageTemplateFile('sub-edit.pt') prefix = 'collector' label = _(u"Filters") ignoreContext = True @property def fields(self): collector = self.context.collector if collector is not None: return field.Fields(collector.schema) else: return field.Fields()
class IMessage(interface.Interface): """Messages are objects ready for sending. """ payload = schema.Field( title=_(u"The message's payload, e.g. the e-mail message."), ) subscription = schema.Object( title=_(u"Subscription, referenced for bookkeeping purposes only."), schema=ISubscription, ) status = schema.Choice( title=_(u"State"), description=_(u"IMessageChanged is fired automatically when this " u"is set"), values=MESSAGE_STATES) status_message = schema.Text(title=_(u"Status details"), required=False) status_changed = schema.Datetime( title=_(u"Last time this message changed its status"), )
class ForgotSecret(utils.OverridableTemplate, form.Form): ignoreContext = True index = viewpagetemplatefile.ViewPageTemplateFile('form.pt') label = _(u"Retrieve a link to your personalized subscription settings") successMessage = _(u"Thanks. We sent you a message.") notKnownMessage = _(u"Your subscription isn't known to us.") fields = field.Fields( schema.TextLine( __name__='address', title=_(u"Address"), description=_(u"The address you're already subscribed with"), ), ) @button.buttonAndHandler(_('Send'), name='send') def handle_send(self, action): data, errors = self.extractData() if errors: self.status = form.EditForm.formErrorsMessage return address = data['address'].lower() for channel in channel_lookup(): subscriptions = channel.subscriptions.query(key=address) if len(subscriptions): subscription = tuple(subscriptions)[0] composer = channel.composers[subscription.metadata['format']] msg = composer.render_forgot_secret(subscription) status, status_msg = message.dispatch(msg) if status != u'sent': raise RuntimeError( "There was an error with sending your e-mail. Please " "try again later.") self.status = self.successMessage break else: self.status = self.notKnownMessage
class ICollector(interface.Interface): """Collectors are useful for automatic newsletters. They are responsible for assembling a list of items for publishing. """ title = schema.TextLine(title=_(u"Title"), ) optional = schema.Bool(title=_(u"Subscriber optional"), ) significant = schema.Bool( title=_(u"Significant"), description=_(u"Include items from this collector even if there are " u"no items returned by significant siblings.")) schema = schema.Object( title=_(u"A schema instance for use in the subscription form"), description=_(u"Values are stored via the ICollectorData adapter per " "subscriber."), required=False, schema=IInterface, ) def get_items(cue=None, subscription=None): """Return a tuple '(items, cue)' where 'items' is the items
class IMessageChanged(zope.lifecycleevent.interfaces.IObjectModifiedEvent): """An object event on the message that signals that the status has changed. """ old_status = schema.TextLine(title=_(u"Old status of message"))
class ManualScheduler(persistent.Persistent, AbstractPeriodicScheduler): title = _(u"Manual scheduler") delta = datetime.timedelta() def tick(self, channel, request): pass
class SubjectsCollectorBase(persistent.Persistent): """A template class that allows you to create a simple collector that presents one field with a vocabulary to the user. You can provide the vocabulary and the title of the field by overriding methods and attributes. """ interface.implements(ISubjectsCollectorBase) title = _(u"Subjects collector") field_name = 'subjects' field_title = _(u"Subjects") def __init__(self, id, title): self.id = id self.title = title super(SubjectsCollectorBase, self).__init__() @property def full_schema(self): vocabulary = self.vocabulary() field = schema.Set(__name__=self.field_name, title=self.field_title, value_type=schema.Choice(vocabulary=vocabulary)) interface.directlyProvides( field, collective.singing.interfaces.IDynamicVocabularyCollection) return zope.interface.interface.InterfaceClass( 'Schema', bases=(collective.singing.interfaces.ICollectorSchema, ), attrs={field.__name__: field}) @property def schema(self): vocabulary = self._vocabulary() field = schema.Set(__name__=self.field_name, title=self.field_title, value_type=schema.Choice(vocabulary=vocabulary)) interface.directlyProvides( field, collective.singing.interfaces.IDynamicVocabularyCollection) return zope.interface.interface.InterfaceClass( 'Schema', bases=(collective.singing.interfaces.ICollectorSchema, ), attrs={field.__name__: field}) def get_items(self, cue=None, subscription=None): if subscription is not None: data = subscription.collector_data.get(self.field_name, set()) else: data = set() return self.get_items_for_selection(cue, data), self.now() def now(self): return datetime.datetime.now() def _vocabulary(self): return self.vocabulary() def get_items_for_selection(self, cue, data): """Override this method and return a list of items that match the set of choices from the vocabulary given in ``data``. Do not return items that are older than ``cue``. """ raise NotImplementedError() def vocabulary(self): """Override this method and return a zope.schema.vocabulary vocabulary. """ raise NotImplementedError()
class Subscribe(wizard.Wizard): """The add subscription wizard. ``context`` is required to be of type ``IChannel``. """ steps = ChooseFormatStep, SubscribeStep success_message = _( u"Thanks for your subscription; " u"we sent you a message for confirmation.") already_subscribed_message = _(u"You are already subscribed.") @property def description(self): if hasattr(self.context, 'description'): return self.context.description def format(self): return self.before_steps[0].widgets.extract()[0]['format'] def update_steps(self): super(Subscribe, self).update_steps() if len(self.before_steps) == 0: # If there's only one format, we'll skip the first step formats = self.context.composers.keys() if len(formats) == 1: self.current_index += 1 format_key = '%s.widgets.%s' % (self.current_step.prefix, 'format') self.request.form[format_key] = [formats[0]] super(Subscribe, self).update_steps() def finish(self, data): comp_data = {} coll_data = {} for key, value in data.items(): name = key.split('.')[-1] if key.startswith('composer.collector.'): coll_data[name] = value elif key.startswith('composer.'): comp_data[name] = value # Create the data necessary to create a subscription: secret = self._secret(comp_data, self.request) metadata = dict(format=self.format(), date=datetime.datetime.now(), pending=True) # We assume here that the language of the request is the # desired language of the subscription: pl = component.queryAdapter( self.request, zope.i18n.interfaces.IUserPreferredLanguages) if pl is not None: metadata['languages'] = pl.getPreferredLanguages() try: subscription = self.context.subscriptions.add_subscription( self.context, secret, comp_data, coll_data, metadata) except ValueError: self.status = self.already_subscribed_message self.finished = False self.current_step.updateActions() return # Ask the composer to render a confirmation message composer = self.context.composers[self.format()] msg = composer.render_confirmation(subscription) status, status_msg = message.dispatch(msg) if status != u'sent': raise RuntimeError( "There was an error with sending your e-mail. Please try " "again later.") def _secret(self, data, request): """Convenience method for looking up secrets. """ composer = self.context.composers[self.format()] return subscribe.secret(self.context, composer, data, self.request)
class IMessageQueues(IMapping): """A dict that contains one ``zc.queue.interfaces.IQueue`` per message status. """ messages_sent = schema.Int( title=_(u"Total number of messages sent through this queue"), )
class IChannel(interface.Interface): """A Channel is what we can subscribe to. A Channel is a hub of configuration. It doesn't do anything by itself. Rather, it provides a number of components to configure and work with. It is also the container of subscriptions to it. """ name = schema.ASCIILine( title=_(u"Unique identifier for this channel across the site."), ) title = schema.TextLine(title=_(u"Title"), ) description = schema.Text(title=_(u"Description")) subscribeable = schema.Bool(title=_(u"Subscribeable"), default=False) scheduler = schema.Object( title=_(u"Scheduler (when)"), required=False, schema=IScheduler, ) collector = schema.Object( title=_(u"Collector (what)"), required=False, schema=ICollector, ) composers = schema.Dict( title=_(u"The channel's composers, keyed by format."), key_type=schema.TextLine(title=_(u"Format")), value_type=schema.Object(title=_(u"Composer"), schema=IComposer), ) subscriptions = schema.Object( title=_(u"The channel's subscriptions"), schema=ISubscriptions, ) queue = schema.Object( title=_(u"This channel's message queues, keyed by message status"), schema=IMessageQueues, ) keep_sent_messages = schema.Bool( title=_(u"Keep a record of sent messages."), description=_(u"This is not currently recommended for large volumes " u"of messages due to storage requirements."), default=False, )
class WeeklyScheduler(persistent.Persistent, AbstractPeriodicScheduler): title = _(u"Weekly scheduler") delta = datetime.timedelta(weeks=1)
class Wizard(utils.OverridableTemplate, form.Form): success_message = _(u"Information submitted successfully.") errors_message = _(u"There were errors.") index = viewpagetemplatefile.ViewPageTemplateFile('wizard.pt') finished = False steps = () # Set this to be form classes label = u"" description = u"" ignoreContext = True fields = field.Fields(schema.Int(__name__='step', default=-1)) def update(self): self.updateWidgets() self.before_steps = [] self.current_step = None self.current_index = int(self.widgets['step'].value) # Don't attempt to extract from the current step if we're # viewing the form for the first time boot = False if self.current_index == -1: self.current_index = 0 boot = True self.update_steps() # If we're viewing the form for the first time, let's set the # step to 0 if boot: self.widgets['step'].value = str(0) # Hide all widgets from previous steps self._hide_widgets() self.widgets['step'].mode = z3c.form.interfaces.HIDDEN_MODE self.updateActions() self.actions.execute() def _hide_widgets(self): for step in self.before_steps: for widget in step.widgets.values(): widget.mode = z3c.form.interfaces.HIDDEN_MODE def update_steps(self): self.before_steps = [] for index in range(self.current_index): step = self.steps[index](self.context, self.request, self) step.update() self.before_steps.append(step) self.current_step = self.steps[self.current_index](self.context, self.request, self) self.current_step.update() def is_last_step(self): return len(self.before_steps) == len(self.steps) - 1 @button.buttonAndHandler(_(u'Proceed'), name='proceed', condition=lambda form: not form.is_last_step()) def handle_proceed(self, action): data, errors = self.current_step.extractData() if errors: self.status = self.errors_message else: self.current_index = current_index = self.current_index + 1 self.widgets['step'].value = current_index self.update_steps() self._hide_widgets() # Proceed can change the conditions for the finish button, # so we need to reconstruct the button actions, since we # do not redirect. self.updateActions() @button.buttonAndHandler(_(u'Finish'), name='finish', condition=lambda form: form.is_last_step()) def handle_finish(self, action): data, errors = self.current_step.extractData() if errors: self.status = self.errors_message return else: self.status = self.success_message self.finished = True data, errors = self.extractData() self.finish(data) def extractData(self): steps = self.before_steps + [self.current_step] return utils.extract_data_prefixed(steps) def finish(self, data): raise NotImplementedError
class DailyScheduler(persistent.Persistent, AbstractPeriodicScheduler): title = _(u"Daily scheduler") delta = datetime.timedelta(days=1)