6b4fd0f0c1
svn path=/plone.app.discussion/trunk/; revision=40083
354 lines
14 KiB
Python
354 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
from Acquisition import aq_inner
|
|
|
|
from AccessControl import Unauthorized
|
|
from AccessControl import getSecurityManager
|
|
|
|
from datetime import datetime
|
|
from DateTime import DateTime
|
|
|
|
from urllib import quote as url_quote
|
|
|
|
from zope.component import createObject, queryUtility
|
|
|
|
from zope.interface import alsoProvides
|
|
|
|
from z3c.form import form, field, button, interfaces
|
|
from z3c.form.interfaces import IFormLayer
|
|
|
|
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
|
|
from Products.CMFCore.utils import getToolByName
|
|
from Products.statusmessages.interfaces import IStatusMessage
|
|
|
|
from plone.registry.interfaces import IRegistry
|
|
|
|
from plone.app.layout.viewlets.common import ViewletBase
|
|
|
|
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
|
|
|
|
# starting from 0.6.0 version plone.z3cform has IWrappedForm interface
|
|
try:
|
|
from plone.z3cform.interfaces import IWrappedForm
|
|
HAS_WRAPPED_FORM = True
|
|
except ImportError: # pragma: no cover
|
|
HAS_WRAPPED_FORM = False
|
|
|
|
class CommentForm(extensible.ExtensibleForm, form.Form):
|
|
|
|
ignoreContext = True # don't use context to get widget data
|
|
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')
|
|
|
|
def updateFields(self):
|
|
super(CommentForm, self).updateFields()
|
|
#self.fields['author_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['author_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
|
|
|
|
# XXX: 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
|
|
|
|
# XXX: Author notification code
|
|
#registry = queryUtility(IRegistry)
|
|
#settings = registry.forInterface(IDiscussionSettings)
|
|
#if not settings.user_notification_enabled:
|
|
# self.widgets['author_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)
|
|
wf = getToolByName(context, 'portal_workflow')
|
|
|
|
data, errors = self.extractData()
|
|
if errors:
|
|
return
|
|
|
|
title = u""
|
|
text = u""
|
|
author_name = u""
|
|
author_email = u""
|
|
#author_notification = None
|
|
|
|
# Captcha check for anonymous users (if Captcha is enabled and
|
|
# anonymous commenting is allowed)
|
|
registry = queryUtility(IRegistry)
|
|
settings = registry.forInterface(IDiscussionSettings)
|
|
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'])
|
|
|
|
# Fetch data from request
|
|
if 'title' in data:
|
|
title = data['title']
|
|
if 'text' in data:
|
|
text = data['text']
|
|
if 'author_name' in data:
|
|
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']
|
|
|
|
# The add-comment view is called on the conversation object
|
|
conversation = IConversation(self.__parent__)
|
|
|
|
# Check if conversation is enabled on this content object
|
|
if not conversation.enabled():
|
|
raise Unauthorized, "Discussion is not enabled for this content\
|
|
object."
|
|
|
|
if data['in_reply_to']:
|
|
# Fetch the comment we want to reply to
|
|
conversation_to_reply_to = conversation.get(data['in_reply_to'])
|
|
replies = IReplies(conversation_to_reply_to)
|
|
|
|
# Create the comment
|
|
comment = createObject('plone.Comment')
|
|
comment.title = title
|
|
comment.text = text
|
|
|
|
portal_membership = getToolByName(self.context, 'portal_membership')
|
|
|
|
if portal_membership.isAnonymousUser() and \
|
|
settings.anonymous_comments:
|
|
comment.creator = None
|
|
comment.author_name = author_name
|
|
comment.author_email = author_email
|
|
#comment.author_notification = author_notification
|
|
comment.creation_date = comment.modification_date = datetime.utcnow()
|
|
elif not portal_membership.isAnonymousUser():
|
|
member = portal_membership.getAuthenticatedMember()
|
|
comment.creator = member.id
|
|
comment.author_username = member.getUserName()
|
|
comment.author_name = member.getProperty('fullname')
|
|
comment.author_email = member.getProperty('email')
|
|
#comment.author_notification = comment.author_notification
|
|
comment.creation_date = comment.modification_date = datetime.utcnow()
|
|
else:
|
|
raise Unauthorized, "Anonymous user tries to post a comment, but \
|
|
anonymous commenting is disabled."
|
|
|
|
# Check if the added comment is a reply to an existing comment
|
|
# or just a regular reply to the content object.
|
|
if data['in_reply_to']:
|
|
# Add a reply to an existing comment
|
|
comment_id = replies.addComment(comment)
|
|
else:
|
|
# Add a comment to the conversation
|
|
comment_id = conversation.addComment(comment)
|
|
|
|
# If a user post a comment and moderation is enabled, a message is shown
|
|
# to the user that his/her comment awaits moderation. If the user has
|
|
# manage right, he/she is redirected directly to the comment.
|
|
can_manage = getSecurityManager().checkPermission('Manage portal',
|
|
context)
|
|
if wf.getChainForPortalType('Discussion Item') == \
|
|
('comment_review_workflow',) and not can_manage:
|
|
# 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
|
|
|
|
|
|
class CommentsViewlet(ViewletBase):
|
|
|
|
form = CommentForm
|
|
index = ViewPageTemplateFile('comments.pt')
|
|
|
|
def update(self):
|
|
super(CommentsViewlet, self).update()
|
|
z2.switch_on(self, request_layer=IFormLayer)
|
|
self.form = self.form(aq_inner(self.context), self.request)
|
|
if HAS_WRAPPED_FORM:
|
|
alsoProvides(self.form, IWrappedForm)
|
|
self.form.update()
|
|
|
|
# view methods
|
|
|
|
def cook(self, text):
|
|
transforms = getToolByName(self, 'portal_transforms')
|
|
targetMimetype = 'text/html'
|
|
registry = queryUtility(IRegistry)
|
|
settings = registry.forInterface(IDiscussionSettings)
|
|
mimetype = settings.text_transform
|
|
return transforms.convertTo(targetMimetype,
|
|
text,
|
|
context=self,
|
|
mimetype=mimetype).getData()
|
|
|
|
def can_reply(self):
|
|
return getSecurityManager().checkPermission('Reply to item',
|
|
aq_inner(self.context))
|
|
|
|
def can_manage(self):
|
|
return getSecurityManager().checkPermission('Manage portal',
|
|
aq_inner(self.context))
|
|
|
|
def is_discussion_allowed(self):
|
|
context = aq_inner(self.context)
|
|
conversation = IConversation(context)
|
|
return conversation.enabled()
|
|
|
|
def has_replies(self, workflow_actions=False):
|
|
"""Returns true if there are replies.
|
|
"""
|
|
if self.get_replies(workflow_actions) is not None:
|
|
try:
|
|
self.get_replies(workflow_actions).next()
|
|
return True
|
|
except StopIteration:
|
|
pass
|
|
return False
|
|
|
|
def get_replies(self, workflow_actions=False):
|
|
"""Returns all replies to a content object.
|
|
|
|
If workflow_actions is false, only published
|
|
comments are returned.
|
|
|
|
If workflow actions is true, comments are
|
|
returned with workflow actions.
|
|
"""
|
|
context = aq_inner(self.context)
|
|
conversation = IConversation(context)
|
|
|
|
wf = getToolByName(context, 'portal_workflow')
|
|
|
|
# workflow_actions is only true when user
|
|
# has 'Manage portal' permission
|
|
|
|
def replies_with_workflow_actions():
|
|
# Generator that returns replies dict with workflow actions
|
|
for r in conversation.getThreads():
|
|
comment_obj = r['comment']
|
|
# list all possible workflow actions
|
|
actions = [a for a in wf.listActionInfos(object=comment_obj)
|
|
if a['category'] == 'workflow' and a['allowed']]
|
|
r = r.copy()
|
|
r['actions'] = actions
|
|
yield r
|
|
|
|
def published_replies():
|
|
# Generator that returns replies dict with workflow status.
|
|
for r in conversation.getThreads():
|
|
comment_obj = r['comment']
|
|
workflow_status = wf.getInfoFor(comment_obj, 'review_state')
|
|
if workflow_status == 'published':
|
|
r = r.copy()
|
|
r['workflow_status'] = workflow_status
|
|
yield r
|
|
|
|
# Return all direct replies
|
|
if conversation.total_comments > 0:
|
|
if workflow_actions:
|
|
return replies_with_workflow_actions()
|
|
else:
|
|
return published_replies()
|
|
|
|
def get_commenter_home_url(self, username=None):
|
|
if username is None:
|
|
return None
|
|
else:
|
|
return "%s/author/%s" % (self.context.portal_url(), username)
|
|
|
|
def get_commenter_portrait(self, username=None):
|
|
|
|
if username is None:
|
|
# return the default user image if no username is given
|
|
return 'defaultUser.gif'
|
|
else:
|
|
portal_membership = getToolByName(self.context,
|
|
'portal_membership',
|
|
None)
|
|
return portal_membership.getPersonalPortrait(username)\
|
|
.absolute_url()
|
|
|
|
def anonymous_discussion_allowed(self):
|
|
# Check if anonymous comments are allowed in the registry
|
|
registry = queryUtility(IRegistry)
|
|
settings = registry.forInterface(IDiscussionSettings)
|
|
return settings.anonymous_comments
|
|
|
|
def show_commenter_image(self):
|
|
# Check if showing commenter image is enabled in the registry
|
|
registry = queryUtility(IRegistry)
|
|
settings = registry.forInterface(IDiscussionSettings)
|
|
return settings.show_commenter_image
|
|
|
|
def is_anonymous(self):
|
|
portal_membership = getToolByName(self.context,
|
|
'portal_membership',
|
|
None)
|
|
return portal_membership.isAnonymousUser()
|
|
|
|
def login_action(self):
|
|
return '%s/login_form?came_from=%s' % \
|
|
(self.navigation_root_url,
|
|
url_quote(self.request.get('URL', '')),)
|
|
|
|
def format_time(self, time):
|
|
# We have to transform Python datetime into Zope DateTime
|
|
# before we can call toLocalizedTime.
|
|
util = getToolByName(self.context, 'translation_service')
|
|
zope_time = DateTime(time.isoformat())
|
|
return util.toLocalizedTime(zope_time, long_format=True)
|