Example #1
0
    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.")
Example #2
0
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
Example #3
0
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.")
Example #4
0
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]
Example #6
0
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
Example #7
0
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"))
Example #8
0
    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]
Example #9
0
    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)
Example #10
0
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()
Example #11
0
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"), )
Example #12
0
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
Example #13
0
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
Example #14
0
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"))
Example #15
0
class ManualScheduler(persistent.Persistent, AbstractPeriodicScheduler):
    title = _(u"Manual scheduler")
    delta = datetime.timedelta()

    def tick(self, channel, request):
        pass
Example #16
0
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()
Example #17
0
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)
Example #18
0
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"), )
Example #19
0
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,
    )
Example #20
0
class WeeklyScheduler(persistent.Persistent, AbstractPeriodicScheduler):
    title = _(u"Weekly scheduler")
    delta = datetime.timedelta(weeks=1)
Example #21
0
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
Example #22
0
class DailyScheduler(persistent.Persistent, AbstractPeriodicScheduler):
    title = _(u"Daily scheduler")
    delta = datetime.timedelta(days=1)