New feature: As a logged-in user, I can enable/disable email notification of additional comments on this content object.

svn path=/plone.app.discussion/trunk/; revision=40949
This commit is contained in:
Timo Stollenwerk 2010-10-30 15:02:05 +00:00
parent 5fe339215b
commit 82afd3ef15
8 changed files with 172 additions and 175 deletions

View File

@ -4,6 +4,10 @@ Changelog
1.0b11 (unreleased) 1.0b11 (unreleased)
------------------- -------------------
- New feature: As a logged-in user, I can enable/disable email notification of
additional comments on this content object.
[timo]
- Disable the plone.app.registry check on schema elements, so no error is - Disable the plone.app.registry check on schema elements, so no error is
raised on upgrades. This fixes https://dev.plone.org/plone/ticket/11195. raised on upgrades. This fixes https://dev.plone.org/plone/ticket/11195.
[timo] [timo]

View File

@ -15,6 +15,7 @@ from zope.interface import alsoProvides
from z3c.form import form, field, button, interfaces from z3c.form import form, field, button, interfaces
from z3c.form.interfaces import IFormLayer from z3c.form.interfaces import IFormLayer
from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
@ -34,6 +35,7 @@ from plone.app.discussion.interfaces import ICaptcha
from plone.app.discussion.browser.validator import CaptchaValidator from plone.app.discussion.browser.validator import CaptchaValidator
from plone.z3cform import z2 from plone.z3cform import z2
from plone.z3cform.widget import SingleCheckBoxWidget
from plone.z3cform.fieldsets import extensible from plone.z3cform.fieldsets import extensible
# starting from 0.6.0 version plone.z3cform has IWrappedForm interface # starting from 0.6.0 version plone.z3cform has IWrappedForm interface
@ -62,8 +64,8 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
def updateFields(self): def updateFields(self):
super(CommentForm, self).updateFields() super(CommentForm, self).updateFields()
#self.fields['author_notification'].widgetFactory = self.fields['user_notification'].widgetFactory = \
# SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
def updateWidgets(self): def updateWidgets(self):
super(CommentForm, self).updateWidgets() super(CommentForm, self).updateWidgets()
@ -71,7 +73,7 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
# Widgets # Widgets
self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE
self.widgets['text'].addClass("autoresize") self.widgets['text'].addClass("autoresize")
#self.widgets['author_notification'].label = _(u"") self.widgets['user_notification'].label = _(u"")
# Anonymous / Logged-in # Anonymous / Logged-in
portal_membership = getToolByName(self.context, 'portal_membership') portal_membership = getToolByName(self.context, 'portal_membership')
@ -83,12 +85,13 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
# current state, we hide it by default. But we keep the field for # current state, we hide it by default. But we keep the field for
# integrators or later use. # integrators or later use.
self.widgets['author_email'].mode = interfaces.HIDDEN_MODE self.widgets['author_email'].mode = interfaces.HIDDEN_MODE
# XXX: Author notification code registry = queryUtility(IRegistry)
#registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False)
#settings = registry.forInterface(IDiscussionSettings, check=False) portal_membership = getToolByName(self.context, 'portal_membership')
#if not settings.user_notification_enabled:
# self.widgets['author_notification'].mode = interfaces.HIDDEN_MODE if not settings.user_notification_enabled or portal_membership.isAnonymousUser():
self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE
def updateActions(self): def updateActions(self):
super(CommentForm, self).updateActions() super(CommentForm, self).updateActions()
@ -105,11 +108,11 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
data, errors = self.extractData() data, errors = self.extractData()
if errors: if errors:
return return
text = u"" text = u""
author_name = u"" author_name = u""
author_email = u"" author_email = u""
#author_notification = None user_notification = None
# Captcha check for anonymous users (if Captcha is enabled and # Captcha check for anonymous users (if Captcha is enabled and
# anonymous commenting is allowed) # anonymous commenting is allowed)
@ -135,8 +138,8 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
author_name = data['author_name'] author_name = data['author_name']
if 'author_email' in data: if 'author_email' in data:
author_email = data['author_email'] author_email = data['author_email']
#if 'author_notification' in data: if 'user_notification' in data:
# author_notification = data['author_notification'] user_notification = data['user_notification']
# The add-comment view is called on the conversation object # The add-comment view is called on the conversation object
conversation = IConversation(self.__parent__) conversation = IConversation(self.__parent__)
@ -163,7 +166,7 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
comment.creator = author_name comment.creator = author_name
comment.author_name = author_name comment.author_name = author_name
comment.author_email = author_email comment.author_email = author_email
#comment.author_notification = author_notification comment.user_notification = user_notification
comment.creation_date = comment.modification_date = datetime.utcnow() comment.creation_date = comment.modification_date = datetime.utcnow()
elif not portal_membership.isAnonymousUser(): elif not portal_membership.isAnonymousUser():
# Member # Member
@ -182,7 +185,7 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
comment.author_username = username comment.author_username = username
comment.author_name = fullname comment.author_name = fullname
comment.author_email = email comment.author_email = email
#comment.author_notification = comment.author_notification comment.user_notification = user_notification
comment.creation_date = comment.modification_date = datetime.utcnow() comment.creation_date = comment.modification_date = datetime.utcnow()
else: else:
raise Unauthorized, "Anonymous user tries to post a comment, but \ raise Unauthorized, "Anonymous user tries to post a comment, but \
@ -340,7 +343,7 @@ class CommentsViewlet(ViewletBase):
def anonymous_discussion_allowed(self): def anonymous_discussion_allowed(self):
# Check if anonymous comments are allowed in the registry # Check if anonymous comments are allowed in the registry
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings) settings = registry.forInterface(IDiscussionSettings, check=False)
return settings.anonymous_comments return settings.anonymous_comments
def show_commenter_image(self): def show_commenter_image(self):

View File

@ -32,8 +32,8 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
self.fields['moderator_notification_enabled'].widgetFactory = \ self.fields['moderator_notification_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
#self.fields['user_notification_enabled'].widgetFactory = \ self.fields['user_notification_enabled'].widgetFactory = \
# SingleCheckBoxFieldWidget SingleCheckBoxFieldWidget
def updateWidgets(self): def updateWidgets(self):
super(DiscussionSettingsEditForm, self).updateWidgets() super(DiscussionSettingsEditForm, self).updateWidgets()
@ -42,8 +42,8 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
self.widgets['show_commenter_image'].label = _(u"Commenter Image") self.widgets['show_commenter_image'].label = _(u"Commenter Image")
self.widgets['moderator_notification_enabled'].label = \ self.widgets['moderator_notification_enabled'].label = \
_(u"Moderator Email Notification") _(u"Moderator Email Notification")
#self.widgets['user_notification_enabled'].label = \ self.widgets['user_notification_enabled'].label = \
# _(u"User Email Notification") _(u"User Email Notification")
class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper): class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):

View File

@ -49,7 +49,7 @@ COMMENT_TITLE = _(u"comment_title",
default=u"${creator} on ${content}") default=u"${creator} on ${content}")
MAIL_NOTIFICATION_MESSAGE = _(u"mail_notification_message", MAIL_NOTIFICATION_MESSAGE = _(u"mail_notification_message",
default=u"A comment with the title '${title}' " default=u"A comment on '${title}' "
"has been posted here: ${link}") "has been posted here: ${link}")
@ -87,7 +87,7 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
author_name = None author_name = None
author_email = None author_email = None
author_notification = None user_notification = None
# Note: we want to use zope.component.createObject() to instantiate # Note: we want to use zope.component.createObject() to instantiate
# comments as far as possible. comment_id and __parent__ are set via # comments as far as possible. comment_id and __parent__ are set via
@ -180,9 +180,7 @@ def notify_content_object_deleted(obj, event):
for comment in conversation.getComments(): for comment in conversation.getComments():
del conversation[comment.id] del conversation[comment.id]
# XXX: This method is not enabled yet. Remove "pragma: no cover" as soon as the def notify_user(obj, event):
# commented out notify user code has been removed.
def notify_user(obj, event): # pragma: no cover
"""Tell users when a comment has been added. """Tell users when a comment has been added.
This method composes and sends emails to all users that have added a This method composes and sends emails to all users that have added a
@ -209,17 +207,17 @@ def notify_user(obj, event): # pragma: no cover
return return
# Compose and send emails to all users that have add a comment to this # Compose and send emails to all users that have add a comment to this
# conversation and enabled author_notification. # conversation and enabled user_notification.
conversation = aq_parent(obj) conversation = aq_parent(obj)
content_object = aq_parent(conversation) content_object = aq_parent(conversation)
for comment in conversation.getComments(): for comment in conversation.getComments():
if obj != comment and \ if obj != comment and \
comment.author_notification and comment.author_email: comment.user_notification and comment.author_email:
subject = translate(_(u"A comment has been posted."), subject = translate(_(u"A comment has been posted."),
context=obj.REQUEST) context=obj.REQUEST)
message = translate(Message(MAIL_NOTIFICATION_MESSAGE, message = translate(Message(MAIL_NOTIFICATION_MESSAGE,
mapping={'title': obj.title, mapping={'title': content_object.title,
'link': content_object.absolute_url()}), 'link': content_object.absolute_url()}),
context=obj.REQUEST) context=obj.REQUEST)
mail_host.send(message, comment.author_email, sender, subject) mail_host.send(message, comment.author_email, sender, subject)
@ -267,7 +265,7 @@ def notify_moderator(obj, event):
#comment = conversation.getComments().next() #comment = conversation.getComments().next()
subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) subject = translate(_(u"A comment has been posted."), context=obj.REQUEST)
message = translate(Message(MAIL_NOTIFICATION_MESSAGE, message = translate(Message(MAIL_NOTIFICATION_MESSAGE,
mapping={'title': obj.title, mapping={'title': content_object.title,
'link': content_object.absolute_url()}), 'link': content_object.absolute_url()}),
context=obj.REQUEST) context=obj.REQUEST)

View File

@ -71,13 +71,12 @@ class IDiscussionSettings(Interface):
title=_(u"label_show_commenter_image", title=_(u"label_show_commenter_image",
default=u"Show commenter image"), default=u"Show commenter image"),
description=_(u"help_show_commenter_image", description=_(u"help_show_commenter_image",
default=u"If selected, an image " default=u"If selected, an image of the user is shown next to "
"of the user is shown " "the comment."),
"next to the comment."),
required=False, required=False,
default=True, default=True,
) )
moderator_notification_enabled = schema.Bool( moderator_notification_enabled = schema.Bool(
title=_(u"label_moderator_notification_enabled", title=_(u"label_moderator_notification_enabled",
default=u"Enable moderator email notification"), default=u"Enable moderator email notification"),
@ -88,16 +87,14 @@ class IDiscussionSettings(Interface):
default=False, default=False,
) )
#user_notification_enabled = schema.Bool( user_notification_enabled = schema.Bool(
# title=_(u"label_user_notification_enabled", title=_(u"label_user_notification_enabled",
# default=u"Enable user email notification"), default=u"Enable user email notification"),
# description=_(u"help_user_notification_enabled", description=_(u"help_user_notification_enabled",
# default=u"If selected, users can " default=u"If selected, users can choose to be notified "
# "choose to be notified " "of new comments by email."),
# "of new comments by " required=False,
# "email."), default=False)
# required=False,
# default=False)
class IConversation(IIterableMapping): class IConversation(IIterableMapping):
@ -269,9 +266,9 @@ class IComment(Interface):
text = schema.Text(title=_(u"label_comment", text = schema.Text(title=_(u"label_comment",
default=u"Comment")) default=u"Comment"))
#author_notification = schema.Bool(title=_(u"Notify me of new comments via " user_notification = schema.Bool(title=_(u"Notify me of new comments via "
# "email."), "email."),
# required=False) required=False)
creator = schema.TextLine(title=_(u"Author name (for display)")) creator = schema.TextLine(title=_(u"Author name (for display)"))
creation_date = schema.Date(title=_(u"Creation date")) creation_date = schema.Date(title=_(u"Creation date"))

View File

@ -7,13 +7,11 @@
<configure zcml:condition="not-installed zope.app.container"> <configure zcml:condition="not-installed zope.app.container">
<!--
<subscriber <subscriber
for="plone.app.discussion.interfaces.IComment for="plone.app.discussion.interfaces.IComment
zope.lifecycleevent.interfaces.IObjectAddedEvent" zope.lifecycleevent.interfaces.IObjectAddedEvent"
handler=".comment.notify_user" handler=".comment.notify_user"
/> />
-->
<subscriber <subscriber
for="plone.app.discussion.interfaces.IComment for="plone.app.discussion.interfaces.IComment
@ -28,13 +26,11 @@
<configure zcml:condition="installed zope.app.container"> <configure zcml:condition="installed zope.app.container">
<!--
<subscriber <subscriber
for="plone.app.discussion.interfaces.IComment for="plone.app.discussion.interfaces.IComment
zope.app.container.interfaces.IObjectAddedEvent" zope.app.container.interfaces.IObjectAddedEvent"
handler=".comment.notify_user" handler=".comment.notify_user"
/> />
-->
<subscriber <subscriber
for="plone.app.discussion.interfaces.IComment for="plone.app.discussion.interfaces.IComment

View File

@ -14,129 +14,129 @@ from Products.CMFPlone.tests.utils import MockMailHost
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.tests.layer import DiscussionLayer from plone.app.discussion.tests.layer import DiscussionLayer
#class TestUserNotificationUnit(PloneTestCase): class TestUserNotificationUnit(PloneTestCase):
#
# layer = DiscussionLayer
#
# def afterSetUp(self):
# # Set up a mock mailhost
# self.portal._original_MailHost = self.portal.MailHost
# self.portal.MailHost = mailhost = MockMailHost('MailHost')
# sm = getSiteManager(context=self.portal)
# sm.unregisterUtility(provided=IMailHost)
# sm.registerUtility(mailhost, provided=IMailHost)
#
# # We need to fake a valid mail setup
# self.portal.email_from_address = "portal@plone.test"
# self.mailhost = self.portal.MailHost
#
# # Enable user notification setting
# registry = queryUtility(IRegistry)
# settings = registry.forInterface(IDiscussionSettings)
# registry['plone.app.discussion.interfaces.IDiscussionSettings.\
# user_notification_enabled'] = True
#
# # Create test content
# self.loginAsPortalOwner()
# self.portal.invokeFactory('Document', 'doc1')
# self.portal_discussion = self.portal.portal_discussion
# self.conversation = IConversation(self.portal.doc1)
#
# def beforeTearDown(self):
# self.portal.MailHost = self.portal._original_MailHost
# sm = getSiteManager(context=self.portal)
# sm.unregisterUtility(provided=IMailHost)
# sm.registerUtility(aq_base(self.portal._original_MailHost),
# provided=IMailHost)
#
# def test_notify_user(self):
# # Add a comment with user notification enabled. Add another comment
# # and make sure an email is send to the user of the first comment.
# comment = createObject('plone.Comment')
# comment.title = 'Comment 1'
# comment.text = 'Comment text'
# comment.author_notification = True
# comment.author_email = "john@plone.test"
# self.conversation.addComment(comment)
#
# comment = createObject('plone.Comment')
# comment.title = 'Comment 2'
# comment.text = 'Comment text'
# self.conversation.addComment(comment)
#
# self.assertEquals(len(self.mailhost.messages), 1)
# self.failUnless(self.mailhost.messages[0])
# msg = self.mailhost.messages[0]
# self.failUnless('To: john@plone.test' in msg)
# self.failUnless('From: portal@plone.test' in msg)
#
# # We expect the headers to be properly header encoded (7-bit):
# #>>> 'Subject: =?utf-8?q?Some_t=C3=A4st_subject=2E?=' in msg
# #True
layer = DiscussionLayer
def afterSetUp(self):
# Set up a mock mailhost
self.portal._original_MailHost = self.portal.MailHost
self.portal.MailHost = mailhost = MockMailHost('MailHost')
sm = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(mailhost, provided=IMailHost)
# We need to fake a valid mail setup
self.portal.email_from_address = "portal@plone.test"
self.mailhost = self.portal.MailHost
# Enable user notification setting
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings)
registry['plone.app.discussion.interfaces.IDiscussionSettings' +
'.user_notification_enabled'] = True
# Create test content
self.loginAsPortalOwner()
self.portal.invokeFactory('Document', 'doc1')
self.portal_discussion = self.portal.portal_discussion
self.conversation = IConversation(self.portal.doc1)
def beforeTearDown(self):
self.portal.MailHost = self.portal._original_MailHost
sm = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(aq_base(self.portal._original_MailHost),
provided=IMailHost)
def test_notify_user(self):
# Add a comment with user notification enabled. Add another comment
# and make sure an email is send to the user of the first comment.
comment = createObject('plone.Comment')
comment.title = 'Comment 1'
comment.text = 'Comment text'
comment.user_notification = True
comment.author_email = "john@plone.test"
self.conversation.addComment(comment)
comment = createObject('plone.Comment')
comment.title = 'Comment 2'
comment.text = 'Comment text'
self.conversation.addComment(comment)
self.assertEquals(len(self.mailhost.messages), 1)
self.failUnless(self.mailhost.messages[0])
msg = self.mailhost.messages[0]
self.failUnless('To: john@plone.test' in msg)
self.failUnless('From: portal@plone.test' in msg)
# We expect the headers to be properly header encoded (7-bit):
#>>> 'Subject: =?utf-8?q?Some_t=C3=A4st_subject=2E?=' in msg
#True
# # The output should be encoded in a reasonable manner # # The output should be encoded in a reasonable manner
# # (in this case quoted-printable): # (in this case quoted-printable):
# #>>> msg #>>> msg
# #'...Another t=C3=A4st message...You are receiving this mail \ #'...Another t=C3=A4st message...You are receiving this mail \
# #because T=C3=A4st user\ntest@plone.test...is sending feedback \ #because T=C3=A4st user\ntest@plone.test...is sending feedback \
# #about the site you administer at... #about the site you administer at...
#
# def test_do_not_notify_user_when_notification_is_disabled(self): def test_do_not_notify_user_when_notification_is_disabled(self):
# # Disable user notification and make sure no email is send to the user. # Disable user notification and make sure no email is send to the user.
# registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
# settings = registry.forInterface(IDiscussionSettings) settings = registry.forInterface(IDiscussionSettings)
# registry['plone.app.discussion.interfaces.IDiscussionSettings.\ registry['plone.app.discussion.interfaces.IDiscussionSettings.' +
# user_notification_enabled'] = False 'user_notification_enabled'] = False
#
# comment = createObject('plone.Comment') comment = createObject('plone.Comment')
# comment.title = 'Comment 1' comment.title = 'Comment 1'
# comment.text = 'Comment text' comment.text = 'Comment text'
# comment.author_notification = True comment.user_notification = True
# comment.author_email = "john@plone.test" comment.author_email = "john@plone.test"
# self.conversation.addComment(comment) self.conversation.addComment(comment)
#
# comment = createObject('plone.Comment') comment = createObject('plone.Comment')
# comment.title = 'Comment 2' comment.title = 'Comment 2'
# comment.text = 'Comment text' comment.text = 'Comment text'
# self.conversation.addComment(comment) self.conversation.addComment(comment)
#
# self.assertEquals(len(self.mailhost.messages), 0) self.assertEquals(len(self.mailhost.messages), 0)
#
# def test_do_not_notify_user_when_email_address_is_given(self): def test_do_not_notify_user_when_email_address_is_given(self):
# comment = createObject('plone.Comment') comment = createObject('plone.Comment')
# comment.title = 'Comment 1' comment.title = 'Comment 1'
# comment.text = 'Comment text' comment.text = 'Comment text'
# comment.author_notification = True comment.user_notification = True
# self.conversation.addComment(comment) self.conversation.addComment(comment)
#
# comment = createObject('plone.Comment') comment = createObject('plone.Comment')
# comment.title = 'Comment 2' comment.title = 'Comment 2'
# comment.text = 'Comment text' comment.text = 'Comment text'
# self.conversation.addComment(comment) self.conversation.addComment(comment)
#
# self.assertEquals(len(self.mailhost.messages), 0) self.assertEquals(len(self.mailhost.messages), 0)
#
# def test_do_not_notify_user_when_no_sender_is_available(self): def test_do_not_notify_user_when_no_sender_is_available(self):
# # Set sender mail address to none and make sure no email is send to # Set sender mail address to none and make sure no email is send to
# # the moderator. # the moderator.
# self.portal.email_from_address = None self.portal.email_from_address = None
#
# comment = createObject('plone.Comment') comment = createObject('plone.Comment')
# comment.title = 'Comment 1' comment.title = 'Comment 1'
# comment.text = 'Comment text' comment.text = 'Comment text'
# comment.author_notification = True comment.user_notification = True
# comment.author_email = "john@plone.test" comment.author_email = "john@plone.test"
# self.conversation.addComment(comment) self.conversation.addComment(comment)
#
# comment = createObject('plone.Comment') comment = createObject('plone.Comment')
# comment.title = 'Comment 2' comment.title = 'Comment 2'
# comment.text = 'Comment text' comment.text = 'Comment text'
# self.conversation.addComment(comment) self.conversation.addComment(comment)
#
# self.assertEquals(len(self.mailhost.messages), 0) self.assertEquals(len(self.mailhost.messages), 0)
class TestModeratorNotificationUnit(PloneTestCase): class TestModeratorNotificationUnit(PloneTestCase):

View File

@ -3,13 +3,13 @@ Products.PDBDebugMode = 1.1
Products.PrintingMailHost = 0.7 Products.PrintingMailHost = 0.7
Sphinx-PyPI-upload = 0.2.1 Sphinx-PyPI-upload = 0.2.1
collective.recipe.omelette = 0.9 collective.recipe.omelette = 0.9
collective.recipe.sphinxbuilder = 0.6.3.3
collective.recipe.template = 1.8 collective.recipe.template = 1.8
collective.xmltestreport = 1.0b3 collective.xmltestreport = 1.0b3
collective.z3cform.datetimewidget = 1.0.2 collective.z3cform.datetimewidget = 1.0.2
coverage = 3.4 coverage = 3.4
hexagonit.recipe.download = 1.4.1
interlude = 1.0 interlude = 1.0
ipython = 0.10 ipython = 0.10.1
logilab.pylintinstaller = 0.15.2 logilab.pylintinstaller = 0.15.2
mr.developer = 1.16 mr.developer = 1.16
plone.app.testing = 1.0a2 plone.app.testing = 1.0a2
@ -18,24 +18,23 @@ plone.supermodel = 1.0b5
plone.testing = 1.0a2 plone.testing = 1.0a2
recaptcha-client = 1.0.5 recaptcha-client = 1.0.5
repoze.sphinx.autointerface = 0.4 repoze.sphinx.autointerface = 0.4
zc.recipe.cmmi = 1.3.2
zest.releaser = 3.15 zest.releaser = 3.15
zptlint = 0.2.3 zptlint = 0.2.3
#Required by: #Required by:
#collective.akismet 1.0a3 #collective.akismet 1.0b2dev
akismet = 0.2.0 akismet = 0.2.0
#Required by: #Required by:
#plone.app.discussion 1.0b9dev #plone.app.discussion 1.0b11
collective.autopermission = 1.0b1 collective.autopermission = 1.0b1
#Required by: #Required by:
#plone.app.discussion 1.0b9dev #plone.app.discussion 1.0b11
plone.app.registry = 1.0b2 plone.app.registry = 1.0b2
#Required by: #Required by:
#plone.app.discussion 1.0b9dev #plone.app.discussion 1.0b11
plone.registry = 1.0b2 plone.registry = 1.0b2
#Required by: #Required by: