class EditCommentForm(CommentForm): """Form to edit an existing comment.""" ignoreContext = True id = 'edit-comment-form' label = _(u'edit_comment_form_title', default=u'Edit comment') def updateWidgets(self): super(EditCommentForm, self).updateWidgets() self.widgets['text'].value = self.context.text # We have to rename the id, otherwise TinyMCE can't initialize # because there are two textareas with the same id. self.widgets['text'].id = 'overlay-comment-text' def _redirect(self, target=''): if not target: portal_state = getMultiAdapter((self.context, self.request), name=u'plone_portal_state') target = portal_state.portal_url() self.request.response.redirect(target) @button.buttonAndHandler(_(u'edit_comment_form_button', default=u'Edit comment'), name='comment') def handleComment(self, action): # Validate form data, errors = self.extractData() if errors: return # Check permissions can_edit = getSecurityManager().checkPermission( 'Edit comments', self.context) mtool = getToolByName(self.context, 'portal_membership') if mtool.isAnonymousUser() or not can_edit: return # Update text self.context.text = data['text'] # Notify that the object has been modified notify(ObjectModifiedEvent(self.context)) # Redirect to comment IStatusMessage(self.request).add(_(u'comment_edit_notification', default='Comment was edited'), type='info') return self._redirect( target=self.action.replace('@@edit-comment', '@@view')) @button.buttonAndHandler(_(u'cancel_form_button', default=u'Cancel'), name='cancel') def handle_cancel(self, action): IStatusMessage(self.request).add( _(u'comment_edit_cancel_notification', default=u'Edit comment cancelled'), type='info') return self._redirect(target=self.context.absolute_url())
class Id(CommentSubstitution): """ Comment id string substitution """ category = _(u'Comments') description = _(u'Comment id') def safe_call(self): """ Safe call """ return getattr(self.comment, 'comment_id', u'')
class AuthorUserName(CommentSubstitution): """ Comment author user name string substitution """ category = _(u'Comments') description = _(u'Comment author user name') def safe_call(self): """ Safe call """ return self.comment.get('author_username', u'')
class Text(CommentSubstitution): """ Comment text """ category = _(u'Comments') description = _(u'Comment text') def safe_call(self): """ Safe call """ return getattr(self.comment, 'text', u'')
class AuthorFullName(CommentSubstitution): """ Comment author full name string substitution """ category = _(u'Comments') description = _(u'Comment author full name') def safe_call(self): """ Safe call """ return getattr(self.comment, 'author_name', u'')
class AuthorEmail(CommentSubstitution): """ Comment author email string substitution """ category = _(u'Comments') description = _(u'Comment author email') def safe_call(self): """ Safe call """ return getattr(self.comment, 'author_email', u'')
def Title(self): # The title of the comment. if self.title: return self.title if not self.author_name: author_name = translate( Message(_( u'label_anonymous', default=u'Anonymous', ), ), ) else: author_name = self.author_name # Fetch the content object (the parent of the comment is the # conversation, the parent of the conversation is the content object). content = aq_base(self.__parent__.__parent__) title = translate( Message(COMMENT_TITLE, mapping={ 'author_name': safe_unicode(author_name), 'content': safe_unicode(content.Title()) })) return title
def handleComment(self, action): # Validate form data, errors = self.extractData() if errors: return # Check permissions can_edit = getSecurityManager().checkPermission( 'Edit comments', self.context) mtool = getToolByName(self.context, 'portal_membership') if mtool.isAnonymousUser() or not can_edit: return # Update text self.context.text = data['text'] # Notify that the object has been modified notify(ObjectModifiedEvent(self.context)) # Redirect to comment IStatusMessage(self.request).add(_(u'comment_edit_notification', default='Comment was edited'), type='info') return self._redirect( target=self.action.replace('@@edit-comment', '@@view'))
def updateWidgets(self): super(CommentForm, self).updateWidgets() # Widgets self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE self.widgets['text'].addClass("autoresize") self.widgets['user_notification'].label = _(u"") # Rename the id of the text widgets because there can be css-id # clashes with the text field of documents when using and overlay # with TinyMCE. self.widgets['text'].id = "form-widgets-comment-text" # Anonymous / Logged-in mtool = getToolByName(self.context, 'portal_membership') if not mtool.isAnonymousUser(): self.widgets['author_name'].mode = interfaces.HIDDEN_MODE self.widgets['author_email'].mode = interfaces.HIDDEN_MODE registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if mtool.isAnonymousUser() and not settings.anonymous_email_enabled: self.widgets['author_email'].mode = interfaces.HIDDEN_MODE member = mtool.getAuthenticatedMember() member_email = member.getProperty('email') # Hide the user_notification checkbox if user notification is disabled # or the user is not logged in. Also check if the user has a valid # email address if member_email == '' or \ not settings.user_notification_enabled or \ mtool.isAnonymousUser(): self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE
def updateWidgets(self): super(CommentForm, self).updateWidgets() # Widgets self.widgets["in_reply_to"].mode = interfaces.HIDDEN_MODE self.widgets["text"].addClass("autoresize") self.widgets["user_notification"].label = _(u"") # Anonymous / Logged-in mtool = getToolByName(self.context, "portal_membership") if not mtool.isAnonymousUser(): self.widgets["author_name"].mode = interfaces.HIDDEN_MODE self.widgets["author_email"].mode = interfaces.HIDDEN_MODE # Todo: Since we are not using the author_email field in the # 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 registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) member = mtool.getAuthenticatedMember() member_email = member.getProperty("email") # Hide the user_notification checkbox if user notification is disabled # or the user is not logged in. Also check if the user has a valid # email address if member_email == "" or not settings.user_notification_enabled or mtool.isAnonymousUser(): self.widgets["user_notification"].mode = interfaces.HIDDEN_MODE
def updateWidgets(self): super(CommentForm, self).updateWidgets() # Widgets self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE self.widgets['text'].addClass("autoresize") self.widgets['user_notification'].label = _(u"") # Anonymous / Logged-in portal_membership = getToolByName(self.context, 'portal_membership') if not portal_membership.isAnonymousUser(): self.widgets['author_name'].mode = interfaces.HIDDEN_MODE self.widgets['author_email'].mode = interfaces.HIDDEN_MODE # Todo: Since we are not using the author_email field in the # 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 registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) mtool = getToolByName(self.context, 'portal_membership') member = mtool.getAuthenticatedMember() member_email = member.getProperty('email') # Hide the user_notification checkbox if user notification is disabled # or the user is not logged in. Also check if the user has a valid email # address if member_email == '' or \ not settings.user_notification_enabled or \ mtool.isAnonymousUser(): self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE
def handleComment(self, action): context = aq_inner(self.context) # Check if conversation is enabled on this content object if not self.__parent__.restrictedTraverse( '@@conversation_view').enabled(): raise Unauthorized( 'Discussion is not enabled for this content object.') # Validation form data, errors = self.extractData() if errors: return # Validate Captcha registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) portal_membership = getToolByName(self.context, 'portal_membership') captcha_enabled = settings.captcha != 'disabled' anonymous_comments = settings.anonymous_comments anon = portal_membership.isAnonymousUser() if captcha_enabled and anonymous_comments and anon: if 'captcha' not in data: data['captcha'] = u'' captcha = CaptchaValidator(self.context, self.request, None, ICaptcha['captcha'], None) captcha.validate(data['captcha']) # Create comment comment = self.create_comment(data) # Add comment to conversation conversation = IConversation(self.__parent__) if data['in_reply_to']: # Add a reply to an existing comment conversation_to_reply_to = conversation.get(data['in_reply_to']) replies = IReplies(conversation_to_reply_to) comment_id = replies.addComment(comment) else: # Add a comment to the conversation comment_id = conversation.addComment(comment) # Redirect after form submit: # If a user posts a comment and moderation is enabled, a message is # shown to the user that his/her comment awaits moderation. If the user # has 'review comments' permission, he/she is redirected directly # to the comment. can_review = getSecurityManager().checkPermission( 'Review comments', context) workflowTool = getToolByName(context, 'portal_workflow') comment_review_state = workflowTool.getInfoFor(comment, 'review_state', None) if comment_review_state == 'pending' and not can_review: # Show info message when comment moderation is enabled IStatusMessage(self.context.REQUEST).addStatusMessage( _('Your comment awaits moderator approval.'), type='info') self.request.response.redirect(self.action) else: # Redirect to comment (inside a content object page) self.request.response.redirect(self.action + '#' + str(comment_id))
def getText(self, targetMimetype=None): """The body text of a comment. """ transforms = getToolByName(self, 'portal_transforms') if targetMimetype is None: targetMimetype = 'text/x-html-safe' sourceMimetype = getattr(self, 'mime_type', None) if sourceMimetype is None: registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) sourceMimetype = settings.text_transform text = self.text if text is None: return '' if isinstance(text, unicode): text = text.encode('utf8') transform = transforms.convertTo( targetMimetype, text, context=self, mimetype=sourceMimetype) if transform: return transform.getData() else: logger = logging.getLogger("plone.app.discussion") logger.error(_( u"Transform '%s' => '%s' not available." % ( sourceMimetype, targetMimetype ) + u"Failed to transform comment '%s'." % self.absolute_url() )) return text
def Title(self): """The title of the comment. """ if self.title: return self.title if not self.author_name: author_name = translate( Message(_( u"label_anonymous", default=u"Anonymous" )) ) else: author_name = self.author_name # Fetch the content object (the parent of the comment is the # conversation, the parent of the conversation is the content object). content = aq_base(self.__parent__.__parent__) title = translate( Message(COMMENT_TITLE, mapping={'author_name': safe_unicode(author_name), 'content': safe_unicode(content.Title())})) return title
def getText(self, targetMimetype=None): """The body text of a comment. """ transforms = getToolByName(self, 'portal_transforms') if targetMimetype is None: targetMimetype = 'text/x-html-safe' sourceMimetype = getattr(self, 'mime_type', None) if sourceMimetype is None: registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) sourceMimetype = settings.text_transform text = self.text if text is None: return '' if isinstance(text, unicode): text = text.encode('utf8') transform = transforms.convertTo(targetMimetype, text, context=self, mimetype=sourceMimetype) if transform: return transform.getData() else: logger = logging.getLogger("plone.app.discussion") logger.error( _(u"Transform '%s' => '%s' not available." % (sourceMimetype, targetMimetype) + u"Failed to transform comment '%s'." % self.absolute_url())) return text
def notify_moderator(obj, event): """Tell the moderator when a comment needs attention. This method sends an email to the moderator if comment moderation a new comment has been added that needs to be approved. The moderator_notification setting has to be enabled in the discussion control panel. Configure the moderator e-mail address in the discussion control panel. If no moderator is configured but moderator notifications are turned on, the site admin email (from the mail control panel) will be used. """ # Check if moderator notification is enabled registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if not settings.moderator_notification_enabled: return # Get informations that are necessary to send an email mail_host = getToolByName(obj, 'MailHost') portal_url = getToolByName(obj, 'portal_url') portal = portal_url.getPortalObject() sender = portal.getProperty('email_from_address') if settings.moderator_email: mto = settings.moderator_email else: mto = sender # Check if a sender address is available if not sender: return conversation = aq_parent(obj) content_object = aq_parent(conversation) # Compose email subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) message = translate(Message(MAIL_NOTIFICATION_MESSAGE_MODERATOR, mapping={ 'title': safe_unicode(content_object.title), 'link': content_object.absolute_url() + '/view#' + obj.id, 'text': obj.text, 'link_approve': obj.absolute_url() + '/@@moderate-publish-comment', 'link_delete': obj.absolute_url() + '/@@moderate-delete-comment', }), context=obj.REQUEST) # Send email try: mail_host.send(message, mto, sender, subject, charset='utf-8') except SMTPException, e: logger.error('SMTP exception (%s) while trying to send an ' + 'email notification to the comment moderator ' + '(from %s to %s, message: %s)', e, sender, mto, message)
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 comment to this conversation and enabled user notification. This requires the user_notification setting to be enabled in the discussion control panel. """ # Check if user notification is enabled registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if not settings.user_notification_enabled: return # Get informations that are necessary to send an email mail_host = getToolByName(obj, "MailHost") portal_url = getToolByName(obj, "portal_url") portal = portal_url.getPortalObject() sender = portal.getProperty("email_from_address") # Check if a sender address is available if not sender: return # Compose and send emails to all users that have add a comment to this # conversation and enabled user_notification. conversation = aq_parent(obj) content_object = aq_parent(conversation) # Avoid sending multiple notification emails to the same person # when he has commented multiple times. emails = set() for comment in conversation.getComments(): if obj != comment and comment.user_notification and comment.author_email: emails.add(comment.author_email) if not emails: return subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) message = translate( Message( MAIL_NOTIFICATION_MESSAGE, mapping={ "title": safe_unicode(content_object.title), "link": content_object.absolute_url() + "/view#" + obj.id, "text": obj.text, }, ), context=obj.REQUEST, ) for email in emails: # Send email try: mail_host.send(message, email, sender, subject, charset="utf-8") except SMTPException: logger.error("SMTP exception while trying to send an " + "email from %s to %s", sender, email)
def updateWidgets(self): super(CommentForm, self).updateWidgets() # Widgets self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE self.widgets['text'].addClass('autoresize') self.widgets['user_notification'].label = _(u'') # Reset widget field settings to their defaults, which may be changed # further on. Otherwise, the email field might get set to required # when an anonymous user visits, and then remain required when an # authenticated user visits, making it impossible for an authenticated # user to fill in the form without validation error. Or when in the # control panel the field is set as not required anymore, that change # would have no effect until the instance was restarted. Note that the # widget is new each time, but the field is the same item in memory as # the previous time. self.widgets['author_email'].field.required = False # The widget is new, but its 'required' setting is based on the # previous value on the field, so we need to reset it here. Changing # the field in updateFields does not help. self.widgets['author_email'].required = False # Rename the id of the text widgets because there can be css-id # clashes with the text field of documents when using and overlay # with TinyMCE. self.widgets['text'].id = 'form-widgets-comment-text' # Anonymous / Logged-in mtool = getToolByName(self.context, 'portal_membership') anon = mtool.isAnonymousUser() registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if anon: if settings.anonymous_email_enabled: # according to IDiscussionSettings.anonymous_email_enabled: # 'If selected, anonymous user will have to give their email.' self.widgets['author_email'].field.required = True self.widgets['author_email'].required = True else: self.widgets['author_email'].mode = interfaces.HIDDEN_MODE else: self.widgets['author_name'].mode = interfaces.HIDDEN_MODE self.widgets['author_email'].mode = interfaces.HIDDEN_MODE member = mtool.getAuthenticatedMember() member_email = member.getProperty('email') # Hide the user_notification checkbox if user notification is disabled # or the user is not logged in. Also check if the user has a valid # email address member_email_is_empty = member_email == '' user_notification_disabled = not settings.user_notification_enabled if member_email_is_empty or user_notification_disabled or anon: self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE
def notify_owner(obj, event): registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) # Get informations that are necessary to send an email mail_host = getToolByName(obj, 'MailHost') portal_url = getToolByName(obj, 'portal_url') portal = portal_url.getPortalObject() mtool = getToolByName(obj, 'portal_membership') sender = '"KSP Comment Notification" <*****@*****.**>' # Check if a sender address is available if not sender: return conversation = aq_parent(obj) content_object = aq_parent(conversation) creator = content_object.Creator() member = mtool.getMemberById(creator) mto = member.getProperty('email', '') if not mto: return # Compose email subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) message = translate(Message(MAIL_NOTIFICATION_MESSAGE, mapping={ 'title': safe_unicode(content_object.title), 'link': content_object.absolute_url() + '/view#' + obj.id, 'text': obj.text, 'name': obj.author_name }), context=obj.REQUEST) # Send email try: mail_host.send(message, mto, sender, subject, charset='utf-8') except SMTPException, e: logger.error('SMTP exception (%s) while trying to send an ' + 'email notification to the comment moderator ' + '(from %s to %s, message: %s)', e, sender, mto, message)
def updateWidgets(self): super(CommentForm, self).updateWidgets() # Widgets self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE self.widgets['text'].addClass('autoresize') self.widgets['user_notification'].label = _(u'') # Rename the id of the text widgets because there can be css-id # clashes with the text field of documents when using and overlay # with TinyMCE. self.widgets['text'].id = 'form-widgets-comment-text' # Anonymous / Logged-in mtool = getToolByName(self.context, 'portal_membership') anon = mtool.isAnonymousUser() registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if anon: if settings.anonymous_email_enabled: # according to IDiscussionSettings.anonymous_email_enabled: # 'If selected, anonymous user will have to give their email.' self.widgets['author_email'].field.required = True self.widgets['author_email'].required = True else: self.widgets['author_email'].mode = interfaces.HIDDEN_MODE else: self.widgets['author_name'].mode = interfaces.HIDDEN_MODE self.widgets['author_email'].mode = interfaces.HIDDEN_MODE member = mtool.getAuthenticatedMember() member_email = member.getProperty('email') # Hide the user_notification checkbox if user notification is disabled # or the user is not logged in. Also check if the user has a valid # email address member_email_is_empty = member_email == '' user_notification_disabled = not settings.user_notification_enabled if member_email_is_empty or user_notification_disabled or anon: self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE
def Title(self): """The title of the comment. """ if self.title: return self.title if not self.creator: creator = translate(Message(_(u"label_anonymous", default=u"Anonymous"))) else: creator = self.creator creator = creator # Fetch the content object (the parent of the comment is the # conversation, the parent of the conversation is the content object). content = aq_base(self.__parent__.__parent__) title = translate( Message(COMMENT_TITLE, mapping={"creator": creator, "content": safe_unicode(content.Title())}) ) return title
def Title(self): """The title of the comment. """ if self.title: return self.title if not self.creator: creator = translate(Message(_(u"label_anonymous", default=u"Anonymous"))) else: creator = self.creator creator = creator # Fetch the content object (the parent of the comment is the # conversation, the parent of the conversation is the content object). content = aq_base(self.__parent__.__parent__) title = translate( Message(COMMENT_TITLE, mapping={'creator': creator, 'content': safe_unicode(content.Title())})) return title
def handleComment(self, action): context = aq_inner(self.context) # Check if conversation is enabled on this content object if not self.__parent__.restrictedTraverse( '@@conversation_view', ).enabled(): raise Unauthorized( 'Discussion is not enabled for this content object.', ) # Validation form data, errors = self.extractData() if errors: return # Validate Captcha registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) portal_membership = getToolByName(self.context, 'portal_membership') captcha_enabled = settings.captcha != 'disabled' anonymous_comments = settings.anonymous_comments anon = portal_membership.isAnonymousUser() if captcha_enabled and anonymous_comments and anon: if 'captcha' not in data: data['captcha'] = u'' captcha = CaptchaValidator(self.context, self.request, None, ICaptcha['captcha'], None) captcha.validate(data['captcha']) # Create comment comment = self.create_comment(data) # Add comment to conversation conversation = IConversation(self.__parent__) if data['in_reply_to']: # Add a reply to an existing comment conversation_to_reply_to = conversation.get(data['in_reply_to']) replies = IReplies(conversation_to_reply_to) comment_id = replies.addComment(comment) else: # Add a comment to the conversation comment_id = conversation.addComment(comment) # Redirect after form submit: # If a user posts a comment and moderation is enabled, a message is # shown to the user that his/her comment awaits moderation. If the user # has 'review comments' permission, he/she is redirected directly # to the comment. can_review = getSecurityManager().checkPermission('Review comments', context) workflowTool = getToolByName(context, 'portal_workflow') comment_review_state = workflowTool.getInfoFor( comment, 'review_state', None, ) if comment_review_state == 'pending' and not can_review: # Show info message when comment moderation is enabled IStatusMessage(self.context.REQUEST).addStatusMessage( _('Your comment awaits moderator approval.'), type='info') self.request.response.redirect(self.action) else: # Redirect to comment (inside a content object page) self.request.response.redirect(self.action + '#' + str(comment_id))
def handleComment(self, action): context = aq_inner(self.context) # Check if conversation is enabled on this content object if not self.__parent__.restrictedTraverse( '@@conversation_view').enabled(): raise Unauthorized("Discussion is not enabled for this content " "object.") # Validation form data, errors = self.extractData() if errors: return # Validate Captcha registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) portal_membership = getToolByName(self.context, 'portal_membership') if settings.captcha != 'disabled' and \ settings.anonymous_comments and \ portal_membership.isAnonymousUser(): if not 'captcha' in data: data['captcha'] = u"" captcha = CaptchaValidator(self.context, self.request, None, ICaptcha['captcha'], None) captcha.validate(data['captcha']) # some attributes are not always set author_name = u"" # Create comment comment = createObject('plone.Comment') # Set comment mime type to current setting in the discussion registry comment.mime_type = settings.text_transform # Set comment attributes (including extended comment form attributes) for attribute in self.fields.keys(): setattr(comment, attribute, data[attribute]) # Make sure author_name is properly encoded if 'author_name' in data: author_name = data['author_name'] if isinstance(author_name, str): author_name = unicode(author_name, 'utf-8') # Set comment author properties for anonymous users or members can_reply = getSecurityManager().checkPermission('Reply to item', context) portal_membership = getToolByName(self.context, 'portal_membership') if portal_membership.isAnonymousUser() and \ settings.anonymous_comments: # Anonymous Users comment.author_name = author_name comment.author_email = u"" comment.user_notification = None comment.creation_date = datetime.utcnow() comment.modification_date = datetime.utcnow() elif not portal_membership.isAnonymousUser() and can_reply: # Member member = portal_membership.getAuthenticatedMember() username = member.getUserName() email = member.getProperty('email') fullname = member.getProperty('fullname') if not fullname or fullname == '': fullname = member.getUserName() # memberdata is stored as utf-8 encoded strings elif isinstance(fullname, str): fullname = unicode(fullname, 'utf-8') if email and isinstance(email, str): email = unicode(email, 'utf-8') comment.creator = username comment.author_username = username comment.author_name = fullname comment.author_email = email comment.creation_date = datetime.utcnow() comment.modification_date = datetime.utcnow() else: # pragma: no cover raise Unauthorized("Anonymous user tries to post a comment, but " "anonymous commenting is disabled. Or user does not have the " "'reply to item' permission.") # Add comment to conversation conversation = IConversation(self.__parent__) if data['in_reply_to']: # Add a reply to an existing comment conversation_to_reply_to = conversation.get(data['in_reply_to']) replies = IReplies(conversation_to_reply_to) comment_id = replies.addComment(comment) else: # Add a comment to the conversation comment_id = conversation.addComment(comment) # Redirect after form submit: # If a user posts a comment and moderation is enabled, a message is # shown to the user that his/her comment awaits moderation. If the user # has 'review comments' permission, he/she is redirected directly # to the comment. can_review = getSecurityManager().checkPermission('Review comments', context) workflowTool = getToolByName(context, 'portal_workflow') comment_review_state = workflowTool.getInfoFor( comment, 'review_state', None ) if comment_review_state == 'pending' and not can_review: # Show info message when comment moderation is enabled IStatusMessage(self.context.REQUEST).addStatusMessage( _("Your comment awaits moderator approval."), type="info") self.request.response.redirect(self.action) else: # Redirect to comment (inside a content object page) self.request.response.redirect(self.action + '#' + str(comment_id))
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 comment to this conversation and enabled user notification. This requires the user_notification setting to be enabled in the discussion control panel. """ # Check if user notification is enabled registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if not settings.user_notification_enabled: return # Get informations that are necessary to send an email mail_host = getToolByName(obj, 'MailHost') registry = getUtility(IRegistry) mail_settings = registry.forInterface(IMailSchema, prefix='plone') sender = mail_settings.email_from_address # Check if a sender address is available if not sender: return # Compose and send emails to all users that have add a comment to this # conversation and enabled user_notification. conversation = aq_parent(obj) content_object = aq_parent(conversation) # Avoid sending multiple notification emails to the same person # when he has commented multiple times. emails = set() for comment in conversation.getComments(): obj_is_not_the_comment = obj != comment valid_user_email = comment.user_notification and comment.author_email if obj_is_not_the_comment and valid_user_email: emails.add(comment.author_email) if not emails: return subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) message = translate( Message( MAIL_NOTIFICATION_MESSAGE, mapping={ 'title': safe_unicode(content_object.title), 'link': content_object.absolute_url() + '/view#' + obj.id, 'text': obj.text } ), context=obj.REQUEST ) for email in emails: # Send email try: mail_host.send(message, email, sender, subject, charset='utf-8') except SMTPException: logger.error('SMTP exception while trying to send an ' + 'email from %s to %s', sender, email)
from plone.app.discussion import PloneAppDiscussionMessageFactory as _ from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IDiscussionSettings from Products.CMFCore.CMFCatalogAware import CatalogAware from Products.CMFCore.CMFCatalogAware import WorkflowAware from OFS.role import RoleManager from AccessControl import ClassSecurityInfo from Products.CMFCore import permissions COMMENT_TITLE = _( u"comment_title", default=u"${author_name} on ${content}") MAIL_NOTIFICATION_MESSAGE = _( u"mail_notification_message", default=u"A comment on '${title}' " u"has been posted here: ${link}\n\n" u"---\n" u"${text}\n" u"---\n") MAIL_NOTIFICATION_MESSAGE_MODERATOR = _( u"mail_notification_message_moderator", default=u"A comment on '${title}' " u"has been posted here: ${link}\n\n" u"---\n"
class ICaptcha(Interface): """Captcha/ReCaptcha text field to extend the existing comment form. """ captcha = schema.TextLine(title=_(u"Captcha"), required=False)
class IConversation(IIterableMapping): """A conversation about a content object. This is a persistent object in its own right and manages all comments. The dict interface allows access to all comments. They are stored by long integer key, in the order they were added. Note that __setitem__() is not supported - use addComment() instead. However, comments can be deleted using __delitem__(). To get replies at the top level, adapt the conversation to IReplies. The conversation can be traversed to via the ++comments++ namespace. For example, path/to/object/++comments++/123 retrieves comment 123. The __parent__ of the conversation (and the acquisition parent during traversal) is the content object. The conversation is the __parent__ (and acquisition parent) for all comments, regardless of threading. """ total_comments = schema.Int( title=_(u"Total number of public comments on this item"), min=0, readonly=True, ) last_comment_date = schema.Date( title=_(u"Date of the most recent public comment"), readonly=True, ) commentators = schema.Set( title=_(u"The set of unique commentators (usernames)"), readonly=True, ) public_commentators = schema.Set( title=_(u"The set of unique commentators (usernames) of" u" published_comments"), readonly=True, ) def addComment(comment): """Adds a new comment to the list of comments, and returns the comment id that was assigned. The comment_id property on the comment will be set accordingly. """ def __delitem__(key): """Delete the comment with the given key. The key is a long id. """ def getComments(start=0, size=None): """Return an iterator of comment objects for rendering. The 'start' parameter is the id of the comment from which to start the batch. If no such comment exists, the next higher id will be used. This means that you can use max key from a previous batch + 1 safely. The 'size' parameter is the number of comments to return in the batch. The comments are returned in creation date order, in the exact batch size specified. """ def getThreads(start=0, size=None, root=0, depth=None): """Return a batch of comment objects for rendering.
from plone.registry.interfaces import IRegistry from plone.app.discussion import PloneAppDiscussionMessageFactory as _ from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IDiscussionSettings from Products.CMFCore.CMFCatalogAware import CatalogAware from Products.CMFCore.CMFCatalogAware import WorkflowAware from OFS.role import RoleManager COMMENT_TITLE = _( u"comment_title", default=u"${creator} on ${content}") MAIL_NOTIFICATION_MESSAGE = _( u"mail_notification_message", default=u"A comment on '${title}' " "has been posted here: ${link}\n\n" "---\n" "${text}\n" "---\n") MAIL_NOTIFICATION_MESSAGE_MODERATOR = _( u"mail_notification_message_moderator", default=u"A comment on '${title}' " "has been posted here: ${link}\n\n" "---\n"
def handle_cancel(self, action): IStatusMessage(self.request).add( _(u'comment_edit_cancel_notification', default=u'Edit comment cancelled'), type='info') return self._redirect(target=self.context.absolute_url())
def isEmail(value): portal = getUtility(ISiteRoot) reg_tool = getToolByName(portal, 'portal_registration') if not (value and reg_tool.isValidEmail(value)): raise Invalid(_('Invalid email address.')) return True
class CommentFormWithHoneyPot(CommentForm): fields = field.Fields(ICommentWithHoneyPot).omit( "portal_type", "__parent__", "__name__", "comment_id", "mime_type", "creator", "creation_date", "modification_date", "author_username", "title", ) def updateFields(self): super(CommentFormWithHoneyPot, self).updateFields() self.fields["honeypot"].widgetFactory = HiddenHoneyPotFieldWidget def updateWidgets(self): super(CommentFormWithHoneyPot, self).updateWidgets() self.widgets["honeypot"].label = u"" @button.buttonAndHandler(_(u"Cancel")) def handleCancel(self, action): # This method should never be called, it's only there to show # a cancel button that is handled by a jQuery method. pass # pragma: no cover @button.buttonAndHandler(_(u"add_comment_button", default=u"Comment"), name="comment") def handleComment(self, action): if self.request.form["form.widgets.honeypot"]: return context = aq_inner(self.context) # Check if conversation is enabled on this content object if not self.__parent__.restrictedTraverse( "@@conversation_view", ).enabled(): raise Unauthorized( "Discussion is not enabled for this content object.", ) # Validation form data, errors = self.extractData() if errors: return # Validate Captcha registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) portal_membership = getToolByName(self.context, "portal_membership") captcha_enabled = settings.captcha != "disabled" anonymous_comments = settings.anonymous_comments anon = portal_membership.isAnonymousUser() if captcha_enabled and anonymous_comments and anon: if "captcha" not in data: data["captcha"] = u"" captcha = CaptchaValidator(self.context, self.request, None, ICaptcha["captcha"], None) captcha.validate(data["captcha"]) # Create comment comment = self.create_comment(data) # Add comment to conversation conversation = IConversation(self.__parent__) if data["in_reply_to"]: # Add a reply to an existing comment conversation_to_reply_to = conversation.get(data["in_reply_to"]) replies = IReplies(conversation_to_reply_to) comment_id = replies.addComment(comment) else: # Add a comment to the conversation comment_id = conversation.addComment(comment) # Redirect after form submit: # If a user posts a comment and moderation is enabled, a message is # shown to the user that his/her comment awaits moderation. If the user # has 'review comments' permission, he/she is redirected directly # to the comment. can_review = getSecurityManager().checkPermission( "Review comments", context) workflowTool = getToolByName(context, "portal_workflow") comment_review_state = workflowTool.getInfoFor( comment, "review_state", None, ) if comment_review_state == "pending" and not can_review: # Show info message when comment moderation is enabled IStatusMessage(self.context.REQUEST).addStatusMessage( _("Your comment awaits moderator approval."), type="info") self.request.response.redirect(self.action) else: # Redirect to comment (inside a content object page) self.request.response.redirect(self.action + "#" + str(comment_id))
import logging from zope.component import queryUtility from Products.CMFCore.utils import getToolByName from smtplib import SMTPException from plone.app.discussion.interfaces import IDiscussionSettings from plone.registry.interfaces import IRegistry from zope.i18n import translate from plone.app.discussion import PloneAppDiscussionMessageFactory as _ from Acquisition import aq_parent, aq_base, Implicit from zope.i18nmessageid import Message from Products.CMFPlone.utils import safe_unicode MAIL_NOTIFICATION_MESSAGE = _( u"mail_notification_message", default=u"A comment on '${title}' " u"has been posted here: ${link}\n\n" u"---\n" u"${text}\n" u"---\n") logger = logging.getLogger("plone.app.discussion") 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 comment to this conversation and enabled user notification. This requires the user_notification setting to be enabled in the discussion control panel. """
from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IReplies from plone.app.discussion.interfaces import IDiscussionSettings 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 from plone.z3cform.interfaces import IWrappedForm COMMENT_DESCRIPTION_PLAIN_TEXT = _( u"comment_description_plain_text", default=u"You can add a comment by filling out the form below. " + "Plain text formatting.", ) COMMENT_DESCRIPTION_INTELLIGENT_TEXT = _( u"comment_description_intelligent_text", default=u"You can add a comment by filling out the form below. " + "Plain text formatting. Web and email addresses are transformed " + "into clickable links.", ) COMMENT_DESCRIPTION_MODERATION_ENABLED = _( u"comment_description_moderation_enabled", default=u"Comments are moderated." ) class CommentForm(extensible.ExtensibleForm, form.Form):
class CommentForm(extensible.ExtensibleForm, form.Form): ignoreContext = True # don't use context to get widget data id = None label = _(u'Add a comment') fields = field.Fields(IComment).omit('portal_type', '__parent__', '__name__', 'comment_id', 'mime_type', 'creator', 'creation_date', 'modification_date', 'author_username', 'title') def updateFields(self): super(CommentForm, self).updateFields() self.fields['user_notification'].widgetFactory = \ SingleCheckBoxFieldWidget def updateWidgets(self): super(CommentForm, self).updateWidgets() # Widgets self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE self.widgets['text'].addClass('autoresize') self.widgets['user_notification'].label = _(u'') # Reset widget field settings to their defaults, which may be changed # further on. Otherwise, the email field might get set to required # when an anonymous user visits, and then remain required when an # authenticated user visits, making it impossible for an authenticated # user to fill in the form without validation error. Or when in the # control panel the field is set as not required anymore, that change # would have no effect until the instance was restarted. Note that the # widget is new each time, but the field is the same item in memory as # the previous time. self.widgets['author_email'].field.required = False # The widget is new, but its 'required' setting is based on the # previous value on the field, so we need to reset it here. Changing # the field in updateFields does not help. self.widgets['author_email'].required = False # Rename the id of the text widgets because there can be css-id # clashes with the text field of documents when using and overlay # with TinyMCE. self.widgets['text'].id = 'form-widgets-comment-text' # Anonymous / Logged-in mtool = getToolByName(self.context, 'portal_membership') anon = mtool.isAnonymousUser() registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if anon: if settings.anonymous_email_enabled: # according to IDiscussionSettings.anonymous_email_enabled: # 'If selected, anonymous user will have to give their email.' self.widgets['author_email'].field.required = True self.widgets['author_email'].required = True else: self.widgets['author_email'].mode = interfaces.HIDDEN_MODE else: self.widgets['author_name'].mode = interfaces.HIDDEN_MODE self.widgets['author_email'].mode = interfaces.HIDDEN_MODE member = mtool.getAuthenticatedMember() member_email = member.getProperty('email') # Hide the user_notification checkbox if user notification is disabled # or the user is not logged in. Also check if the user has a valid # email address member_email_is_empty = member_email == '' user_notification_disabled = not settings.user_notification_enabled if member_email_is_empty or user_notification_disabled or anon: self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE def updateActions(self): super(CommentForm, self).updateActions() self.actions['cancel'].addClass('standalone') self.actions['cancel'].addClass('hide') self.actions['comment'].addClass('context') def get_author(self, data): context = aq_inner(self.context) # some attributes are not always set author_name = u'' # Make sure author_name/ author_email is properly encoded if 'author_name' in data: author_name = safe_unicode(data['author_name']) if 'author_email' in data: author_email = safe_unicode(data['author_email']) # Set comment author properties for anonymous users or members portal_membership = getToolByName(context, 'portal_membership') anon = portal_membership.isAnonymousUser() if not anon and getSecurityManager().checkPermission( 'Reply to item', context): # Member member = portal_membership.getAuthenticatedMember() email = safe_unicode(member.getProperty('email')) fullname = member.getProperty('fullname') if not fullname or fullname == '': fullname = member.getUserName() fullname = safe_unicode(fullname) author_name = fullname email = safe_unicode(email) # XXX: according to IComment interface author_email must not be # noqa T000 # set for logged in users, cite: # 'for anonymous comments only, set to None for logged in comments' author_email = email # /XXX # noqa T000 return author_name, author_email def create_comment(self, data): context = aq_inner(self.context) comment = createObject('plone.Comment') registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) anonymous_comments = settings.anonymous_comments # Set comment mime type to current setting in the discussion registry comment.mime_type = settings.text_transform # Set comment attributes (including extended comment form attributes) for attribute in self.fields.keys(): setattr(comment, attribute, data[attribute]) # Set dates comment.creation_date = datetime.utcnow() comment.modification_date = datetime.utcnow() # Get author name and email comment.author_name, comment.author_email = self.get_author(data) # Set comment author properties for anonymous users or members portal_membership = getToolByName(context, 'portal_membership') anon = portal_membership.isAnonymousUser() if anon and anonymous_comments: # Anonymous Users comment.user_notification = None elif not anon and getSecurityManager().checkPermission( 'Reply to item', context): # Member member = portal_membership.getAuthenticatedMember() memberid = member.getId() user = member.getUser() comment.changeOwnership(user, recursive=False) comment.manage_setLocalRoles(memberid, ['Owner']) comment.creator = memberid comment.author_username = memberid else: # pragma: no cover raise Unauthorized( u'Anonymous user tries to post a comment, but anonymous ' u'commenting is disabled. Or user does not have the ' u"'reply to item' permission.", ) return comment @button.buttonAndHandler(_(u'add_comment_button', default=u'Comment'), name='comment') def handleComment(self, action): context = aq_inner(self.context) # Check if conversation is enabled on this content object if not self.__parent__.restrictedTraverse( '@@conversation_view', ).enabled(): raise Unauthorized( 'Discussion is not enabled for this content object.', ) # Validation form data, errors = self.extractData() if errors: return # Validate Captcha registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) portal_membership = getToolByName(self.context, 'portal_membership') captcha_enabled = settings.captcha != 'disabled' anonymous_comments = settings.anonymous_comments anon = portal_membership.isAnonymousUser() if captcha_enabled and anonymous_comments and anon: if 'captcha' not in data: data['captcha'] = u'' captcha = CaptchaValidator(self.context, self.request, None, ICaptcha['captcha'], None) captcha.validate(data['captcha']) # Create comment comment = self.create_comment(data) # Add comment to conversation conversation = IConversation(self.__parent__) if data['in_reply_to']: # Add a reply to an existing comment conversation_to_reply_to = conversation.get(data['in_reply_to']) replies = IReplies(conversation_to_reply_to) comment_id = replies.addComment(comment) else: # Add a comment to the conversation comment_id = conversation.addComment(comment) # Redirect after form submit: # If a user posts a comment and moderation is enabled, a message is # shown to the user that his/her comment awaits moderation. If the user # has 'review comments' permission, he/she is redirected directly # to the comment. can_review = getSecurityManager().checkPermission('Review comments', context) workflowTool = getToolByName(context, 'portal_workflow') comment_review_state = workflowTool.getInfoFor( comment, 'review_state', None, ) if comment_review_state == 'pending' and not can_review: # Show info message when comment moderation is enabled IStatusMessage(self.context.REQUEST).addStatusMessage( _('Your comment awaits moderator approval.'), type='info') self.request.response.redirect(self.action) else: # Redirect to comment (inside a content object page) self.request.response.redirect(self.action + '#' + str(comment_id)) @button.buttonAndHandler(_(u'Cancel')) def handleCancel(self, action): # This method should never be called, it's only there to show # a cancel button that is handled by a jQuery method. pass # pragma: no cover
def handleComment(self, action): context = aq_inner(self.context) # Check if conversation is enabled on this content object if not self.__parent__.restrictedTraverse( '@@conversation_view').enabled(): raise Unauthorized("Discussion is not enabled for this content " "object.") # Validation form data, errors = self.extractData() if errors: return # Validate Captcha registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) portal_membership = getToolByName(self.context, 'portal_membership') if settings.captcha != 'disabled' and \ settings.anonymous_comments and \ portal_membership.isAnonymousUser(): if not 'captcha' in data: data['captcha'] = u"" captcha = CaptchaValidator(self.context, self.request, None, ICaptcha['captcha'], None) captcha.validate(data['captcha']) # some attributes are not always set author_name = u"" # Create comment comment = createObject('plone.Comment') # Set comment mime type to current setting in the discussion registry comment.mime_type = settings.text_transform # Set comment attributes (including extended comment form attributes) for attribute in self.fields.keys(): setattr(comment, attribute, data[attribute]) # Make sure author_name is properly encoded if 'author_name' in data: author_name = data['author_name'] if isinstance(author_name, str): author_name = unicode(author_name, 'utf-8') # Set comment author properties for anonymous users or members can_reply = getSecurityManager().checkPermission( 'Reply to item', context) portal_membership = getToolByName(self.context, 'portal_membership') if portal_membership.isAnonymousUser() and \ settings.anonymous_comments: # Anonymous Users comment.author_name = author_name comment.author_email = u"" comment.user_notification = None comment.creation_date = datetime.utcnow() comment.modification_date = datetime.utcnow() elif not portal_membership.isAnonymousUser() and can_reply: # Member member = portal_membership.getAuthenticatedMember() username = member.getUserName() email = member.getProperty('email') fullname = member.getProperty('fullname') if not fullname or fullname == '': fullname = member.getUserName() # memberdata is stored as utf-8 encoded strings elif isinstance(fullname, str): fullname = unicode(fullname, 'utf-8') if email and isinstance(email, str): email = unicode(email, 'utf-8') comment.creator = username comment.author_username = username comment.author_name = fullname comment.author_email = email comment.creation_date = datetime.utcnow() comment.modification_date = datetime.utcnow() else: # pragma: no cover raise Unauthorized( "Anonymous user tries to post a comment, but " "anonymous commenting is disabled. Or user does not have the " "'reply to item' permission.") # Add comment to conversation conversation = IConversation(self.__parent__) if data['in_reply_to']: # Add a reply to an existing comment conversation_to_reply_to = conversation.get(data['in_reply_to']) replies = IReplies(conversation_to_reply_to) comment_id = replies.addComment(comment) else: # Add a comment to the conversation comment_id = conversation.addComment(comment) # Redirect after form submit: # If a user posts a comment and moderation is enabled, a message is # shown to the user that his/her comment awaits moderation. If the user # has 'review comments' permission, he/she is redirected directly # to the comment. can_review = getSecurityManager().checkPermission( 'Review comments', context) workflowTool = getToolByName(context, 'portal_workflow') comment_review_state = workflowTool.getInfoFor(comment, 'review_state') if comment_review_state == 'pending' and not can_review: # Show info message when comment moderation is enabled IStatusMessage(self.context.REQUEST).addStatusMessage( _("Your comment awaits moderator approval."), type="info") self.request.response.redirect(self.action) else: # Redirect to comment (inside a content object page) self.request.response.redirect(self.action + '#' + str(comment_id))
class IDiscussionSettings(Interface): """Global discussion settings. This describes records stored in the configuration registry and obtainable via plone.registry. """ # Todo: Write a short hint, that other discussion related options can # be found elsewhere in the Plone control panel: # # - Types control panel: Allow comments on content types # - Search control panel: Show comments in search results globally_enabled = schema.Bool( title=_(u'label_globally_enabled', default=u'Globally enable comments'), description=_( u'help_globally_enabled', default=u'If selected, users are able to post comments on the ' u'site. However, you will still need to enable comments ' u'for specific content types, folders or content ' u'objects before users will be able to post comments.', ), required=False, default=False, ) anonymous_comments = schema.Bool( title=_(u'label_anonymous_comments', default='Enable anonymous comments'), description=_( u'help_anonymous_comments', default=u'If selected, anonymous users are able to post ' u'comments without logging in. It is highly ' u'recommended to use a captcha solution to prevent ' u'spam if this setting is enabled.', ), required=False, default=False, ) anonymous_email_enabled = schema.Bool( title=_(u'label_anonymous_email_enabled', default=u'Enable anonymous email field'), description=_( u'help_anonymous_email_enabled', default=u'If selected, anonymous user will have to ' u'give their email.', ), required=False, default=False, ) moderation_enabled = schema.Bool( title=_( u'label_moderation_enabled', default='Enable comment moderation', ), description=_( u'help_moderation_enabled', default=u'If selected, comments will enter a "Pending" state ' u'in which they are invisible to the public. A user ' u'with the "Review comments" permission ("Reviewer" ' u'or "Manager") can approve comments to make them ' u'visible to the public. If you want to enable a ' u'custom comment workflow, you have to go to the ' u'types control panel.', ), required=False, default=False, ) edit_comment_enabled = schema.Bool( title=_(u'label_edit_comment_enabled', default='Enable editing of comments'), description=_(u'help_edit_comment_enabled', default=u'If selected, supports editing ' 'of comments for users with the "Edit comments" ' 'permission.'), required=False, default=False, ) delete_own_comment_enabled = schema.Bool( title=_(u'label_delete_own_comment_enabled', default='Enable deleting own comments'), description=_(u'help_delete_own_comment_enabled', default=u'If selected, supports deleting ' 'of own comments for users with the ' '"Delete own comments" permission.'), required=False, default=False, ) text_transform = schema.Choice( title=_(u'label_text_transform', default='Comment text transform'), description=_( u'help_text_transform', default=u'Use this setting to choose if the comment text ' u'should be transformed in any way. You can choose ' u'between "Plain text" and "Intelligent text". ' u'"Intelligent text" converts plain text into HTML ' u'where line breaks and indentation is preserved, ' u'and web and email addresses are made into ' u'clickable links.'), required=True, default='text/plain', vocabulary='plone.app.discussion.vocabularies.TextTransformVocabulary', ) captcha = schema.Choice( title=_(u'label_captcha', default='Captcha'), description=_(u'help_captcha', default=u'Use this setting to enable or disable Captcha ' u'validation for comments. Install ' u'plone.formwidget.captcha, ' u'plone.formwidget.recaptcha, collective.akismet, or ' u'collective.z3cform.norobots if there are no options ' u'available.'), required=True, default='disabled', vocabulary='plone.app.discussion.vocabularies.CaptchaVocabulary', ) show_commenter_image = schema.Bool( 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 ' u'the comment.'), 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 ' u'needs attention. The moderator email address can ' u'be set below.'), required=False, default=False, ) moderator_email = schema.ASCIILine( title=_( u'label_moderator_email', default=u'Moderator Email Address', ), description=_(u'help_moderator_email', default=u'Address to which moderator notifications ' u'will be sent.'), required=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 ' u'of new comments by email.'), required=False, default=False, )
class IComment(Interface): """A comment. Comments are indexed in the catalog and subject to workflow and security. """ portal_type = schema.ASCIILine( title=_(u"Portal type"), default="Discussion Item", ) __parent__ = schema.Object(title=_(u"Conversation"), schema=Interface) __name__ = schema.TextLine(title=_(u"Name")) comment_id = schema.Int( title=_(u"A comment id unique to this conversation")) in_reply_to = schema.Int( title=_(u"Id of comment this comment is in reply to"), required=False, ) # for logged in comments - set to None for anonymous author_username = schema.TextLine(title=_(u"Name"), required=False) # for anonymous comments only, set to None for logged in comments author_name = schema.TextLine(title=_(u"Name"), required=False) author_email = schema.TextLine(title=_(u"Email"), required=False) title = schema.TextLine(title=_(u"label_subject", default=u"Subject")) mime_type = schema.ASCIILine(title=_(u"MIME type"), default="text/plain") text = schema.Text(title=_(u"label_comment", default=u"Comment")) user_notification = schema.Bool( title=_(u"Notify me of new comments via email."), required=False) creator = schema.TextLine(title=_(u"Username of the commenter")) creation_date = schema.Date(title=_(u"Creation date")) modification_date = schema.Date(title=_(u"Modification date"))
from Products.CMFPlone.utils import safe_unicode from smtplib import SMTPException from zope.annotation.interfaces import IAnnotatable from zope.component import getUtility from zope.component import queryUtility from zope.component.factory import Factory from zope.event import notify from zope.i18n import translate from zope.i18nmessageid import Message from zope.interface import implementer import logging COMMENT_TITLE = _( u'comment_title', default=u'${author_name} on ${content}') MAIL_NOTIFICATION_MESSAGE = _( u'mail_notification_message', default=u'A comment on "${title}" ' u'has been posted here: ${link}\n\n' u'---\n' u'${text}\n' u'---\n') MAIL_NOTIFICATION_MESSAGE_MODERATOR = _( u'mail_notification_message_moderator', default=u'A comment on "${title}" ' u'has been posted here: ${link}\n\n' u'---\n'
class IDiscussionSettings(Interface): """Global discussion settings. This describes records stored in the configuration registry and obtainable via plone.registry. """ # Todo: Write a short hint, that other discussion related options can # be found elsewhere in the Plone control panel: # # - Types control panel: Allow comments on content types # - Search control panel: Show comments in search results globally_enabled = schema.Bool( title=_(u"label_globally_enabled", default=u"Globally enable comments"), description=_( u"help_globally_enabled", default=u"If selected, users are able to post comments on the " u"site. Though, you have to enable comments for " u"specific content types, folders or content objects " u"before users will be able to post comments."), required=False, default=False, ) anonymous_comments = schema.Bool( title=_(u"label_anonymous_comments", default="Enable anonymous comments"), description=_(u"help_anonymous_comments", default=u"If selected, anonymous users are able to post " u"comments without loggin in. It is highly " u"recommended to use a captcha solution to prevent " u"spam if this setting is enabled."), required=False, default=False, ) anonymous_email_enabled = schema.Bool( title=_(u"label_anonymous_email_enabled", default=u"Enable anonymous email field"), description=_(u"help_anonymous_email_enabled", default=u"If selected, anonymous user will have to " u"give their email."), required=False, default=False) moderation_enabled = schema.Bool( title=_(u"label_moderation_enabled", default="Enable comment moderation"), description=_( u"help_moderation_enabled", default=u"If selected, comments will enter a 'Pending' state " u"in which they are invisible to the public. A user " u"with the 'Review comments' permission ('Reviewer' " u"or 'Manager') can approve comments to make them " u"visible to the public. If you want to enable a " u"custom comment workflow, you have to go to the " u"types control panel."), required=False, default=False, ) edit_comment_enabled = schema.Bool( title=_(u"label_edit_comment_enabled", default="Enable editing of comments"), description=_(u"help_edit_comment_enabled", default=u"If selected, supports editing " "of comments for users with the 'Edit comments' " "permission."), required=False, default=False, ) delete_own_comment_enabled = schema.Bool( title=_(u"label_delete_own_comment_enabled", default="Enable deleting own comments"), description=_(u"help_delete_own_comment_enabled", default=u"If selected, supports deleting " "of own comments for users with the " "'Delete own comments' permission."), required=False, default=False, ) text_transform = schema.Choice( title=_(u"label_text_transform", default="Comment text transform"), description=_( u"help_text_transform", default=u"Use this setting to choose if the comment text " + u"should be transformed in any way. You can choose " u"between 'Plain text' and 'Intelligent text'. " + u"'Intelligent text' converts plain text into HTML " + u"where line breaks and indentation is preserved, " + u"and web and email addresses are made into " + u"clickable links."), required=True, default='text/plain', vocabulary='plone.app.discussion.vocabularies.TextTransformVocabulary', ) captcha = schema.Choice( title=_(u"label_captcha", default="Captcha"), description=_(u"help_captcha", default=u"Use this setting to enable or disable Captcha " u"validation for comments. Install " u"plone.formwidget.captcha, " u"plone.formwidget.recaptcha, collective.akismet, or " u"collective.z3cform.norobots if there are no options " u"available."), required=True, default='disabled', vocabulary='plone.app.discussion.vocabularies.CaptchaVocabulary', ) show_commenter_image = schema.Bool( 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 " u"the comment."), 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 " u"needs attention. The moderator email address can " + u"be set below."), required=False, default=False, ) moderator_email = schema.ASCIILine( title=_(u'label_moderator_email', default=u'Moderator Email Address'), description=_(u'help_moderator_email', default=u"Address to which moderator notifications " u"will be sent."), required=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 " u"of new comments by email."), required=False, default=False)
def notify_user(obj, event): # Check if user notification is enabled registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if not settings.user_notification_enabled: return # Get informations that are necessary to send an email mail_host = getToolByName(obj, 'MailHost') portal_url = getToolByName(obj, 'portal_url') portal = portal_url.getPortalObject() # Compose and send emails to all users that have add a comment to this # conversation and enabled user_notification. conversation = aq_parent(obj) content_object = aq_parent(conversation) sender = '"KSP Comment Notification" <*****@*****.**>' # Check if a sender address is available if not sender: return # Avoid sending multiple notification emails to the same person # when he has commented multiple times. emails = set() for comment in conversation.getComments(): if (obj != comment and comment.user_notification and comment.author_email): emails.add(comment.author_email) if obj.author_email and obj.author_email in emails: emails.remove(obj.author_email) if not emails: return subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) message = translate(Message( MAIL_NOTIFICATION_MESSAGE, mapping={'title': safe_unicode(content_object.title), 'link': content_object.absolute_url() + '/view#' + obj.id, 'name': obj.author_name, 'text': obj.text}), context=obj.REQUEST) for email in emails: # Send email try: mail_host.send(message, email, sender, subject, charset='utf-8') except SMTPException: padcomment.logger.error('SMTP exception while trying to send an ' + 'email from %s to %s', sender, email)
from plone.app.discussion import PloneAppDiscussionMessageFactory as _ from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IReplies from plone.app.discussion.interfaces import IDiscussionSettings from plone.app.discussion.interfaces import ICaptcha from plone.app.discussion.browser.validator import CaptchaValidator from plone.z3cform import z2 from plone.z3cform.fieldsets import extensible from plone.z3cform.interfaces import IWrappedForm COMMENT_DESCRIPTION_PLAIN_TEXT = _( u"comment_description_plain_text", default=u"You can add a comment by filling out the form below. " + "Plain text formatting.") COMMENT_DESCRIPTION_MARKDOWN = _( u"comment_description_markdown", default=u"You can add a comment by filling out the form below. " + "Plain text formatting. You can use the Markdown syntax for " + "links and images.") COMMENT_DESCRIPTION_INTELLIGENT_TEXT = _( u"comment_description_intelligent_text", default=u"You can add a comment by filling out the form below. " + "Plain text formatting. Web and email addresses are " + "transformed into clickable links.") COMMENT_DESCRIPTION_MODERATION_ENABLED = _(
class CommentForm(extensible.ExtensibleForm, form.Form): ignoreContext = True # don't use context to get widget data id = None label = _(u"Add a comment") fields = field.Fields(IComment).omit('portal_type', '__parent__', '__name__', 'comment_id', 'mime_type', 'creator', 'creation_date', 'modification_date', 'author_username', 'title') def updateFields(self): super(CommentForm, self).updateFields() self.fields['user_notification'].widgetFactory = \ SingleCheckBoxFieldWidget def updateWidgets(self): super(CommentForm, self).updateWidgets() # Widgets self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE self.widgets['text'].addClass("autoresize") self.widgets['user_notification'].label = _(u"") # Rename the id of the text widgets because there can be css-id # clashes with the text field of documents when using and overlay # with TinyMCE. self.widgets['text'].id = "form-widgets-comment-text" # Anonymous / Logged-in mtool = getToolByName(self.context, 'portal_membership') if not mtool.isAnonymousUser(): self.widgets['author_name'].mode = interfaces.HIDDEN_MODE self.widgets['author_email'].mode = interfaces.HIDDEN_MODE # Todo: Since we are not using the author_email field in the # 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 registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) member = mtool.getAuthenticatedMember() member_email = member.getProperty('email') # Hide the user_notification checkbox if user notification is disabled # or the user is not logged in. Also check if the user has a valid # email address if member_email == '' or \ not settings.user_notification_enabled or \ mtool.isAnonymousUser(): self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE def updateActions(self): super(CommentForm, self).updateActions() self.actions['cancel'].addClass("standalone") self.actions['cancel'].addClass("hide") self.actions['comment'].addClass("context") @button.buttonAndHandler(_(u"add_comment_button", default=u"Comment"), name='comment') def handleComment(self, action): context = aq_inner(self.context) # Check if conversation is enabled on this content object if not self.__parent__.restrictedTraverse( '@@conversation_view').enabled(): raise Unauthorized("Discussion is not enabled for this content " "object.") # Validation form data, errors = self.extractData() if errors: return # Validate Captcha registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) portal_membership = getToolByName(self.context, 'portal_membership') if settings.captcha != 'disabled' and \ settings.anonymous_comments and \ portal_membership.isAnonymousUser(): if not 'captcha' in data: data['captcha'] = u"" captcha = CaptchaValidator(self.context, self.request, None, ICaptcha['captcha'], None) captcha.validate(data['captcha']) # some attributes are not always set author_name = u"" # Create comment comment = createObject('plone.Comment') # Set comment mime type to current setting in the discussion registry comment.mime_type = settings.text_transform # Set comment attributes (including extended comment form attributes) for attribute in self.fields.keys(): setattr(comment, attribute, data[attribute]) # Make sure author_name is properly encoded if 'author_name' in data: author_name = data['author_name'] if isinstance(author_name, str): author_name = unicode(author_name, 'utf-8') # Set comment author properties for anonymous users or members can_reply = getSecurityManager().checkPermission( 'Reply to item', context) portal_membership = getToolByName(self.context, 'portal_membership') if portal_membership.isAnonymousUser() and \ settings.anonymous_comments: # Anonymous Users comment.author_name = author_name comment.author_email = u"" comment.user_notification = None comment.creation_date = datetime.utcnow() comment.modification_date = datetime.utcnow() elif not portal_membership.isAnonymousUser() and can_reply: # Member member = portal_membership.getAuthenticatedMember() username = member.getUserName() email = member.getProperty('email') fullname = member.getProperty('fullname') if not fullname or fullname == '': fullname = member.getUserName() # memberdata is stored as utf-8 encoded strings elif isinstance(fullname, str): fullname = unicode(fullname, 'utf-8') if email and isinstance(email, str): email = unicode(email, 'utf-8') comment.creator = username comment.author_username = username comment.author_name = fullname comment.author_email = email comment.creation_date = datetime.utcnow() comment.modification_date = datetime.utcnow() else: # pragma: no cover raise Unauthorized( "Anonymous user tries to post a comment, but " "anonymous commenting is disabled. Or user does not have the " "'reply to item' permission.") # Add comment to conversation conversation = IConversation(self.__parent__) if data['in_reply_to']: # Add a reply to an existing comment conversation_to_reply_to = conversation.get(data['in_reply_to']) replies = IReplies(conversation_to_reply_to) comment_id = replies.addComment(comment) else: # Add a comment to the conversation comment_id = conversation.addComment(comment) # Redirect after form submit: # If a user posts a comment and moderation is enabled, a message is # shown to the user that his/her comment awaits moderation. If the user # has 'review comments' permission, he/she is redirected directly # to the comment. can_review = getSecurityManager().checkPermission( 'Review comments', context) workflowTool = getToolByName(context, 'portal_workflow') comment_review_state = workflowTool.getInfoFor(comment, 'review_state') if comment_review_state == 'pending' and not can_review: # Show info message when comment moderation is enabled IStatusMessage(self.context.REQUEST).addStatusMessage( _("Your comment awaits moderator approval."), type="info") self.request.response.redirect(self.action) else: # Redirect to comment (inside a content object page) self.request.response.redirect(self.action + '#' + str(comment_id)) @button.buttonAndHandler(_(u"Cancel")) def handleCancel(self, action): # This method should never be called, it's only there to show # a cancel button that is handled by a jQuery method. pass # pragma: no cover
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IReplies from plone.app.discussion.interfaces import IDiscussionSettings from plone.app.discussion.interfaces import ICaptcha from plone.app.discussion.browser.validator import CaptchaValidator from plone.z3cform import z2 from plone.z3cform.fieldsets import extensible from plone.z3cform.interfaces import IWrappedForm COMMENT_DESCRIPTION_PLAIN_TEXT = _( u"comment_description_plain_text", default=u"You can add a comment by filling out the form below. " + "Plain text formatting.") COMMENT_DESCRIPTION_MARKDOWN = _( u"comment_description_markdown", default=u"You can add a comment by filling out the form below. " + "Plain text formatting. You can use the Markdown syntax for " + "links and images.") COMMENT_DESCRIPTION_INTELLIGENT_TEXT = _( u"comment_description_intelligent_text", default=u"You can add a comment by filling out the form below. " + "Plain text formatting. Web and email addresses are " + "transformed into clickable links.") COMMENT_DESCRIPTION_MODERATION_ENABLED = _(
from z3c.form import button from z3c.form import field from z3c.form import form from z3c.form import interfaces from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget from z3c.form.interfaces import IFormLayer from zope.component import createObject from zope.component import queryUtility from zope.i18n import translate from zope.i18nmessageid import Message from zope.interface import alsoProvides COMMENT_DESCRIPTION_PLAIN_TEXT = _( u'comment_description_plain_text', default=u'You can add a comment by filling out the form below. ' u'Plain text formatting.', ) COMMENT_DESCRIPTION_MARKDOWN = _( u'comment_description_markdown', default=u'You can add a comment by filling out the form below. ' u'Plain text formatting. You can use the Markdown syntax for ' u'links and images.', ) COMMENT_DESCRIPTION_INTELLIGENT_TEXT = _( u'comment_description_intelligent_text', default=u'You can add a comment by filling out the form below. ' u'Plain text formatting. Web and email addresses are ' u'transformed into clickable links.',
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 comment to this conversation and enabled user notification. This requires the user_notification setting to be enabled in the discussion control panel. """ # Check if user notification is enabled registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if not settings.user_notification_enabled: return # Get informations that are necessary to send an email mail_host = getToolByName(obj, 'MailHost') portal_url = getToolByName(obj, 'portal_url') portal = portal_url.getPortalObject() sender = portal.getProperty('email_from_address') # Check if a sender address is available if not sender: return # Compose and send emails to all users that have add a comment to this # conversation and enabled user_notification. conversation = aq_parent(obj) content_object = aq_parent(conversation) # Avoid sending multiple notification emails to the same person # when he has commented multiple times. emails = set() for comment in conversation.getComments(): if (obj != comment and comment.user_notification and comment.author_email): emails.add(comment.author_email) if not emails: return subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) message = translate(Message( MAIL_NOTIFICATION_MESSAGE, mapping={'title': safe_unicode(content_object.title), 'link': content_object.absolute_url() + '/view#' + obj.id, 'text': obj.text}), context=obj.REQUEST) for email in emails: # Send email try: mail_host.send(message, email, sender, subject, charset='utf-8') except SMTPException: logger.error('SMTP exception while trying to send an ' + 'email from %s to %s', sender, email)