From 82afd3ef15ee43022f2953a6e0a27921e3ec96ef Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Sat, 30 Oct 2010 15:02:05 +0000 Subject: [PATCH] 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 --- CHANGES.txt | 4 + plone/app/discussion/browser/comments.py | 35 +-- plone/app/discussion/browser/controlpanel.py | 8 +- plone/app/discussion/comment.py | 16 +- plone/app/discussion/interfaces.py | 31 ++- plone/app/discussion/notifications.zcml | 4 - .../discussion/tests/test_notifications.py | 236 +++++++++--------- versions.plone4.cfg | 13 +- 8 files changed, 172 insertions(+), 175 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 5ea8026..fcc428e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,10 @@ Changelog 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 raised on upgrades. This fixes https://dev.plone.org/plone/ticket/11195. [timo] diff --git a/plone/app/discussion/browser/comments.py b/plone/app/discussion/browser/comments.py index 41ca894..cdf1a81 100644 --- a/plone/app/discussion/browser/comments.py +++ b/plone/app/discussion/browser/comments.py @@ -15,6 +15,7 @@ from zope.interface import alsoProvides from z3c.form import form, field, button, interfaces from z3c.form.interfaces import IFormLayer +from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile 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.z3cform import z2 +from plone.z3cform.widget import SingleCheckBoxWidget from plone.z3cform.fieldsets import extensible # starting from 0.6.0 version plone.z3cform has IWrappedForm interface @@ -62,8 +64,8 @@ class CommentForm(extensible.ExtensibleForm, form.Form): def updateFields(self): super(CommentForm, self).updateFields() - #self.fields['author_notification'].widgetFactory = - # SingleCheckBoxFieldWidget + self.fields['user_notification'].widgetFactory = \ + SingleCheckBoxFieldWidget def updateWidgets(self): super(CommentForm, self).updateWidgets() @@ -71,7 +73,7 @@ class CommentForm(extensible.ExtensibleForm, form.Form): # Widgets self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE self.widgets['text'].addClass("autoresize") - #self.widgets['author_notification'].label = _(u"") + self.widgets['user_notification'].label = _(u"") # Anonymous / Logged-in 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 # integrators or later use. self.widgets['author_email'].mode = interfaces.HIDDEN_MODE - - # XXX: Author notification code - #registry = queryUtility(IRegistry) - #settings = registry.forInterface(IDiscussionSettings, check=False) - #if not settings.user_notification_enabled: - # self.widgets['author_notification'].mode = interfaces.HIDDEN_MODE + + registry = queryUtility(IRegistry) + settings = registry.forInterface(IDiscussionSettings, check=False) + portal_membership = getToolByName(self.context, 'portal_membership') + + if not settings.user_notification_enabled or portal_membership.isAnonymousUser(): + self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE def updateActions(self): super(CommentForm, self).updateActions() @@ -105,11 +108,11 @@ class CommentForm(extensible.ExtensibleForm, form.Form): data, errors = self.extractData() if errors: return - + text = u"" author_name = u"" author_email = u"" - #author_notification = None + user_notification = None # Captcha check for anonymous users (if Captcha is enabled and # anonymous commenting is allowed) @@ -135,8 +138,8 @@ class CommentForm(extensible.ExtensibleForm, form.Form): author_name = data['author_name'] if 'author_email' in data: author_email = data['author_email'] - #if 'author_notification' in data: - # author_notification = data['author_notification'] + if 'user_notification' in data: + user_notification = data['user_notification'] # The add-comment view is called on the conversation object conversation = IConversation(self.__parent__) @@ -163,7 +166,7 @@ class CommentForm(extensible.ExtensibleForm, form.Form): comment.creator = author_name comment.author_name = author_name comment.author_email = author_email - #comment.author_notification = author_notification + comment.user_notification = user_notification comment.creation_date = comment.modification_date = datetime.utcnow() elif not portal_membership.isAnonymousUser(): # Member @@ -182,7 +185,7 @@ class CommentForm(extensible.ExtensibleForm, form.Form): comment.author_username = username comment.author_name = fullname comment.author_email = email - #comment.author_notification = comment.author_notification + comment.user_notification = user_notification comment.creation_date = comment.modification_date = datetime.utcnow() else: raise Unauthorized, "Anonymous user tries to post a comment, but \ @@ -340,7 +343,7 @@ class CommentsViewlet(ViewletBase): def anonymous_discussion_allowed(self): # Check if anonymous comments are allowed in the registry registry = queryUtility(IRegistry) - settings = registry.forInterface(IDiscussionSettings) + settings = registry.forInterface(IDiscussionSettings, check=False) return settings.anonymous_comments def show_commenter_image(self): diff --git a/plone/app/discussion/browser/controlpanel.py b/plone/app/discussion/browser/controlpanel.py index 9f29bdd..e774627 100644 --- a/plone/app/discussion/browser/controlpanel.py +++ b/plone/app/discussion/browser/controlpanel.py @@ -32,8 +32,8 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm): SingleCheckBoxFieldWidget self.fields['moderator_notification_enabled'].widgetFactory = \ SingleCheckBoxFieldWidget - #self.fields['user_notification_enabled'].widgetFactory = \ - # SingleCheckBoxFieldWidget + self.fields['user_notification_enabled'].widgetFactory = \ + SingleCheckBoxFieldWidget def updateWidgets(self): super(DiscussionSettingsEditForm, self).updateWidgets() @@ -42,8 +42,8 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm): self.widgets['show_commenter_image'].label = _(u"Commenter Image") self.widgets['moderator_notification_enabled'].label = \ _(u"Moderator Email Notification") - #self.widgets['user_notification_enabled'].label = \ - # _(u"User Email Notification") + self.widgets['user_notification_enabled'].label = \ + _(u"User Email Notification") class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper): diff --git a/plone/app/discussion/comment.py b/plone/app/discussion/comment.py index 9d6fa12..929447e 100644 --- a/plone/app/discussion/comment.py +++ b/plone/app/discussion/comment.py @@ -49,7 +49,7 @@ COMMENT_TITLE = _(u"comment_title", default=u"${creator} on ${content}") 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}") @@ -87,7 +87,7 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable, author_name = None author_email = None - author_notification = None + user_notification = None # Note: we want to use zope.component.createObject() to instantiate # 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(): del conversation[comment.id] -# XXX: This method is not enabled yet. Remove "pragma: no cover" as soon as the -# commented out notify user code has been removed. -def notify_user(obj, event): # pragma: no cover +def notify_user(obj, event): """Tell users when a comment has been added. 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 # 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) content_object = aq_parent(conversation) for comment in conversation.getComments(): 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."), context=obj.REQUEST) message = translate(Message(MAIL_NOTIFICATION_MESSAGE, - mapping={'title': obj.title, + mapping={'title': content_object.title, 'link': content_object.absolute_url()}), context=obj.REQUEST) mail_host.send(message, comment.author_email, sender, subject) @@ -267,7 +265,7 @@ def notify_moderator(obj, event): #comment = conversation.getComments().next() subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) message = translate(Message(MAIL_NOTIFICATION_MESSAGE, - mapping={'title': obj.title, + mapping={'title': content_object.title, 'link': content_object.absolute_url()}), context=obj.REQUEST) diff --git a/plone/app/discussion/interfaces.py b/plone/app/discussion/interfaces.py index cae5509..b2b6aaf 100644 --- a/plone/app/discussion/interfaces.py +++ b/plone/app/discussion/interfaces.py @@ -71,13 +71,12 @@ class IDiscussionSettings(Interface): title=_(u"label_show_commenter_image", default=u"Show commenter image"), description=_(u"help_show_commenter_image", - default=u"If selected, an image " - "of the user is shown " - "next to the comment."), + default=u"If selected, an image of the user is shown next to " + "the comment."), required=False, default=True, ) - + moderator_notification_enabled = schema.Bool( title=_(u"label_moderator_notification_enabled", default=u"Enable moderator email notification"), @@ -88,16 +87,14 @@ class IDiscussionSettings(Interface): default=False, ) - #user_notification_enabled = schema.Bool( - # title=_(u"label_user_notification_enabled", - # default=u"Enable user email notification"), - # description=_(u"help_user_notification_enabled", - # default=u"If selected, users can " - # "choose to be notified " - # "of new comments by " - # "email."), - # required=False, - # default=False) + user_notification_enabled = schema.Bool( + title=_(u"label_user_notification_enabled", + default=u"Enable user email notification"), + description=_(u"help_user_notification_enabled", + default=u"If selected, users can choose to be notified " + "of new comments by email."), + required=False, + default=False) class IConversation(IIterableMapping): @@ -269,9 +266,9 @@ class IComment(Interface): text = schema.Text(title=_(u"label_comment", default=u"Comment")) - #author_notification = schema.Bool(title=_(u"Notify me of new comments via " - # "email."), - # required=False) + user_notification = schema.Bool(title=_(u"Notify me of new comments via " + "email."), + required=False) creator = schema.TextLine(title=_(u"Author name (for display)")) creation_date = schema.Date(title=_(u"Creation date")) diff --git a/plone/app/discussion/notifications.zcml b/plone/app/discussion/notifications.zcml index e72aee0..e03e7da 100644 --- a/plone/app/discussion/notifications.zcml +++ b/plone/app/discussion/notifications.zcml @@ -7,13 +7,11 @@ - - >> 'Subject: =?utf-8?q?Some_t=C3=A4st_subject=2E?=' in msg -# #True +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.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 -# # (in this case quoted-printable): -# #>>> msg -# #'...Another t=C3=A4st message...You are receiving this mail \ -# #because T=C3=A4st user\ntest@plone.test...is sending feedback \ -# #about the site you administer at... -# -# def test_do_not_notify_user_when_notification_is_disabled(self): -# # Disable user notification and make sure no email is send to the user. -# registry = queryUtility(IRegistry) -# settings = registry.forInterface(IDiscussionSettings) -# registry['plone.app.discussion.interfaces.IDiscussionSettings.\ -# user_notification_enabled'] = False -# -# 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), 0) -# -# def test_do_not_notify_user_when_email_address_is_given(self): -# comment = createObject('plone.Comment') -# comment.title = 'Comment 1' -# comment.text = 'Comment text' -# comment.author_notification = True -# 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), 0) -# -# 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 -# # the moderator. -# self.portal.email_from_address = None -# -# 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), 0) + # (in this case quoted-printable): + #>>> msg + #'...Another t=C3=A4st message...You are receiving this mail \ + #because T=C3=A4st user\ntest@plone.test...is sending feedback \ + #about the site you administer at... + + def test_do_not_notify_user_when_notification_is_disabled(self): + # Disable user notification and make sure no email is send to the user. + registry = queryUtility(IRegistry) + settings = registry.forInterface(IDiscussionSettings) + registry['plone.app.discussion.interfaces.IDiscussionSettings.' + + 'user_notification_enabled'] = False + + 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), 0) + + def test_do_not_notify_user_when_email_address_is_given(self): + comment = createObject('plone.Comment') + comment.title = 'Comment 1' + comment.text = 'Comment text' + comment.user_notification = True + 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), 0) + + 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 + # the moderator. + self.portal.email_from_address = None + + 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), 0) + class TestModeratorNotificationUnit(PloneTestCase): diff --git a/versions.plone4.cfg b/versions.plone4.cfg index 864c823..f3d7f45 100644 --- a/versions.plone4.cfg +++ b/versions.plone4.cfg @@ -3,13 +3,13 @@ Products.PDBDebugMode = 1.1 Products.PrintingMailHost = 0.7 Sphinx-PyPI-upload = 0.2.1 collective.recipe.omelette = 0.9 -collective.recipe.sphinxbuilder = 0.6.3.3 collective.recipe.template = 1.8 collective.xmltestreport = 1.0b3 collective.z3cform.datetimewidget = 1.0.2 coverage = 3.4 +hexagonit.recipe.download = 1.4.1 interlude = 1.0 -ipython = 0.10 +ipython = 0.10.1 logilab.pylintinstaller = 0.15.2 mr.developer = 1.16 plone.app.testing = 1.0a2 @@ -18,24 +18,23 @@ plone.supermodel = 1.0b5 plone.testing = 1.0a2 recaptcha-client = 1.0.5 repoze.sphinx.autointerface = 0.4 -zc.recipe.cmmi = 1.3.2 zest.releaser = 3.15 zptlint = 0.2.3 #Required by: -#collective.akismet 1.0a3 +#collective.akismet 1.0b2dev akismet = 0.2.0 #Required by: -#plone.app.discussion 1.0b9dev +#plone.app.discussion 1.0b11 collective.autopermission = 1.0b1 #Required by: -#plone.app.discussion 1.0b9dev +#plone.app.discussion 1.0b11 plone.app.registry = 1.0b2 #Required by: -#plone.app.discussion 1.0b9dev +#plone.app.discussion 1.0b11 plone.registry = 1.0b2 #Required by: