From f0fb6514c782a4b23bddb241f1a59c123e6176e3 Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 11 Feb 2010 21:05:28 +0000 Subject: [PATCH] Moderator email notification added: As a moderator, I am notified when new comments require my attention. svn path=/plone.app.discussion/branches/notification/; revision=33921 --- plone/app/discussion/browser/controlpanel.py | 2 + plone/app/discussion/comment.py | 66 ++++++++-- plone/app/discussion/interfaces.py | 13 ++ plone/app/discussion/subscribers.zcml | 12 ++ plone/app/discussion/tests/test_migration.py | 2 + .../discussion/tests/test_notifications.py | 124 ++++++++++++++++++ 6 files changed, 208 insertions(+), 11 deletions(-) create mode 100644 plone/app/discussion/tests/test_notifications.py diff --git a/plone/app/discussion/browser/controlpanel.py b/plone/app/discussion/browser/controlpanel.py index 8bd3892..7f1e01d 100644 --- a/plone/app/discussion/browser/controlpanel.py +++ b/plone/app/discussion/browser/controlpanel.py @@ -29,6 +29,7 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm): self.fields['globally_enabled'].widgetFactory = SingleCheckBoxFieldWidget self.fields['anonymous_comments'].widgetFactory = SingleCheckBoxFieldWidget self.fields['show_commenter_image'].widgetFactory = SingleCheckBoxFieldWidget + self.fields['moderator_notification_enabled'].widgetFactory = SingleCheckBoxFieldWidget self.fields['notification_enabled'].widgetFactory = SingleCheckBoxFieldWidget def updateWidgets(self): @@ -36,6 +37,7 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm): self.widgets['globally_enabled'].label = _(u"Enable Comments") self.widgets['anonymous_comments'].label = _(u"Anonymous Comments") self.widgets['show_commenter_image'].label = _(u"Commenter Image") + self.widgets['moderator_notification_enabled'].label = _(u"Moderator Email Notification") self.widgets['notification_enabled'].label = _(u"Email Notification") diff --git a/plone/app/discussion/comment.py b/plone/app/discussion/comment.py index 9a6cb61..1f7ac35 100644 --- a/plone/app/discussion/comment.py +++ b/plone/app/discussion/comment.py @@ -1,10 +1,14 @@ """The default comment class and factory. """ from datetime import datetime + from zope.interface import implements + from zope.component.factory import Factory +from zope.component import queryUtility from Acquisition import aq_parent, Implicit + from AccessControl.Role import RoleManager from AccessControl.Owned import Owned @@ -15,7 +19,9 @@ from Products.CMFCore.utils import getToolByName from OFS.Traversable import Traversable -from plone.app.discussion.interfaces import IComment +from plone.registry.interfaces import IRegistry + +from plone.app.discussion.interfaces import IComment, IDiscussionSettings try: # Plone 4: @@ -127,26 +133,64 @@ def notify_content_object(obj, event): """Tell the content object when a comment is added """ content_obj = aq_parent(aq_parent(obj)) - content_obj.reindexObject(idxs=('total_comments', 'last_comment_date', 'commentators',)) + content_obj.reindexObject(idxs=('total_comments', + 'last_comment_date', + 'commentators',)) def notify_user(obj, event): """Tell the user when a comment is added """ - acl_users = getToolByName(obj, 'acl_users') + conversation = aq_parent(obj) + content_object = aq_parent(conversation) mail_host = getToolByName(obj, 'MailHost') portal_url = getToolByName(obj, 'portal_url') portal = portal_url.getPortalObject() sender = portal.getProperty('email_from_address') - + if not sender: return - subject = "Is this you?" - message = "A presenter called %s was added here %s" % (obj.title, obj.absolute_url(),) + for comment in conversation.getComments(): + if comment.author_notification and comment.author_email: + subject = "A comment has been posted." + message = "A comment with the title '%s' has been posted here: %s" \ + % (obj.title, + content_object.absolute_url(),) + mail_host.send(message, comment.author_email, sender, subject) + +def notify_moderator(obj, index): + """Tell the moderator when a comment needs attention + """ - matching_users = acl_users.searchUsers(fullname=obj.title) - for user_info in matching_users: - email = user_info.get('email', None) - if email is not None: - mail_host.secureSend(message, email, sender, subject) \ No newline at end of file + # check if notification is enabled + registry = queryUtility(IRegistry) + settings = registry.forInterface(IDiscussionSettings) + if not settings.moderator_notification_enabled: + return + + # check if comment review workflow is enabled + wf = getToolByName(obj, 'portal_workflow') + if wf.getChainForPortalType('Discussion Item') != \ + ('comment_review_workflow',): + return + + mail_host = getToolByName(obj, 'MailHost') + portal_url = getToolByName(obj, 'portal_url') + portal = portal_url.getPortalObject() + sender = portal.getProperty('email_from_address') + mto = portal.getProperty('email_from_address') + + # check if a sender address is available + if not sender: + return + + conversation = aq_parent(obj) + content_object = aq_parent(conversation) + + comment = conversation.getComments().next() + subject = "A comment has been posted." + message = "A comment with the title '%s' has been posted here: %s" \ + % (obj.title, + content_object.absolute_url(),) + mail_host.send(message, mto, sender, subject) diff --git a/plone/app/discussion/interfaces.py b/plone/app/discussion/interfaces.py index 38b284a..9740270 100644 --- a/plone/app/discussion/interfaces.py +++ b/plone/app/discussion/interfaces.py @@ -55,6 +55,19 @@ class IDiscussionSettings(Interface): required=False, default=True) + moderator_notification_enabled = schema.Bool( + title=_(u"label_moderator_notification_enabled", + default=u"Enable moderator email notification"), + description=_(u"help_moderator_notification_enabled", + default=u"If selected, " + "the moderator " + "is notified " + "if a comment " + "needs " + "attention."), + required=False, + default=False) + notification_enabled = schema.Bool( title=_(u"label_notification_enabled", default=u"Enable email notification"), diff --git a/plone/app/discussion/subscribers.zcml b/plone/app/discussion/subscribers.zcml index 1be80e1..8b23638 100644 --- a/plone/app/discussion/subscribers.zcml +++ b/plone/app/discussion/subscribers.zcml @@ -37,6 +37,12 @@ handler=".tool.unindex_object" /> + + @@ -71,6 +77,12 @@ handler=".tool.unindex_object" /> + + diff --git a/plone/app/discussion/tests/test_migration.py b/plone/app/discussion/tests/test_migration.py index 32b8793..a5802d7 100644 --- a/plone/app/discussion/tests/test_migration.py +++ b/plone/app/discussion/tests/test_migration.py @@ -17,6 +17,8 @@ from plone.app.discussion.interfaces import IConversation, IComment class MigrationTest(PloneTestCase): + layer = DiscussionLayer + def afterSetUp(self): self.loginAsPortalOwner() typetool = self.portal.portal_types diff --git a/plone/app/discussion/tests/test_notifications.py b/plone/app/discussion/tests/test_notifications.py new file mode 100644 index 0000000..035869f --- /dev/null +++ b/plone/app/discussion/tests/test_notifications.py @@ -0,0 +1,124 @@ +import unittest + +from Acquisition import aq_base + +from zope.app.container.contained import ObjectAddedEvent +from zope.app.container.interfaces import IObjectAddedEvent + +from zope.component import createObject +from zope.component import getSiteManager +from zope.component import queryUtility + +from Products.PloneTestCase.ptc import PloneTestCase + +from Products.MailHost.interfaces import IMailHost +from Products.CMFPlone.tests.utils import MockMailHost + +from plone.registry.interfaces import IRegistry + +from plone.app.discussion.comment import notify_user +from plone.app.discussion.interfaces import IComment, IConversation, IReplies +from plone.app.discussion.interfaces import IDiscussionSettings +from plone.app.discussion.tests.layer import DiscussionLayer + + +class TestModeratorNotificationUnit(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 comment moderation + self.portal.portal_types['Document'].allow_discussion = True + self.portal.portal_workflow.setChainForPortalTypes( + ('Discussion Item',), + ('comment_review_workflow',)) + + # Enable moderator notification setting + registry = queryUtility(IRegistry) + settings = registry.forInterface(IDiscussionSettings) + registry['plone.app.discussion.interfaces.IDiscussionSettings.moderator_notification_enabled'] = True + + 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_moderator(self): + # Add a comment and make sure an email is send to the moderator. + comment = createObject('plone.Comment') + comment.title = 'Comment 1' + 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: portal@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_moderator_when_no_sender_is_available(self): + # Set sender mail address to nonw 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' + self.conversation.addComment(comment) + self.assertEquals(len(self.mailhost.messages), 0) + + def test_do_not_notify_moderator_when_notification_is_disabled(self): + # Disable moderator notification setting and make sure no email is send + # to the moderator. + registry = queryUtility(IRegistry) + settings = registry.forInterface(IDiscussionSettings) + registry['plone.app.discussion.interfaces.IDiscussionSettings.moderator_notification_enabled'] = False + + comment = createObject('plone.Comment') + comment.title = 'Comment 1' + comment.text = 'Comment text' + self.conversation.addComment(comment) + self.assertEquals(len(self.mailhost.messages), 0) + + def test_do_not_notify_moderator_when_moderation_workflow_is_disabled(self): + # Disable comment moderation and make sure no email is send to the + # moderator. + self.portal.portal_types['Document'].allow_discussion = True + self.portal.portal_workflow.setChainForPortalTypes( + ('Discussion Item',), + ('simple_publication_workflow',)) + + comment = createObject('plone.Comment') + comment.title = 'Comment 1' + comment.text = 'Comment text' + self.conversation.addComment(comment) + self.assertEquals(len(self.mailhost.messages), 0) + +def test_suite(): + return unittest.defaultTestLoader.loadTestsFromName(__name__) + \ No newline at end of file