merged master
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
from Acquisition import aq_inner, aq_parent
|
||||
from AccessControl import getSecurityManager
|
||||
|
||||
from zope.component import getMultiAdapter
|
||||
from Products.statusmessages.interfaces import IStatusMessage
|
||||
from Products.Five.browser import BrowserView
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
|
||||
from plone.app.discussion import PloneAppDiscussionMessageFactory as _
|
||||
from comments import CommentForm
|
||||
from z3c.form import button
|
||||
from plone.z3cform.layout import wrap_form
|
||||
|
||||
|
||||
class View(BrowserView):
|
||||
"""Comment View.
|
||||
@@ -37,3 +45,64 @@ class View(BrowserView):
|
||||
url = "%s/view" % url
|
||||
|
||||
self.request.response.redirect('%s#%s' % (url, context.id))
|
||||
|
||||
|
||||
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']
|
||||
|
||||
# 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())
|
||||
|
||||
EditComment = wrap_form(EditCommentForm)
|
||||
|
||||
#EOF
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<tal:block tal:define="userHasReplyPermission view/can_reply;
|
||||
isDiscussionAllowed view/is_discussion_allowed;
|
||||
isAnonymousDiscussionAllowed view/anonymous_discussion_allowed;
|
||||
isEditCommentAllowed view/edit_comment_allowed;
|
||||
isAnon view/is_anonymous;
|
||||
canReview view/can_review;
|
||||
replies python:view.get_replies(canReview);
|
||||
@@ -14,8 +15,7 @@
|
||||
<div class="reply"
|
||||
tal:condition="python:isAnon and not isAnonymousDiscussionAllowed">
|
||||
<form tal:attributes="action view/login_action">
|
||||
<input class="standalone"
|
||||
style="margin-bottom: 1.25em;"
|
||||
<input class="standalone loginbutton"
|
||||
type="submit"
|
||||
value="Log in to add comments"
|
||||
i18n:attributes="value label_login_to_add_comments;"
|
||||
@@ -31,45 +31,48 @@
|
||||
<div class="comment"
|
||||
tal:define="reply reply_dict/comment;
|
||||
depth reply_dict/depth|python:0;
|
||||
depth python: depth > 10 and '10' or depth;
|
||||
author_home_url python:view.get_commenter_home_url(username=reply.author_username);
|
||||
has_author_link python:author_home_url and not isAnon;
|
||||
portrait_url python:view.get_commenter_portrait(reply.author_username);
|
||||
review_state python:wtool.getInfoFor(reply, 'review_state', 'none');"
|
||||
review_state python:wtool.getInfoFor(reply, 'review_state', 'none');
|
||||
canEdit python:view.can_edit(reply);
|
||||
canDelete python:view.can_delete(reply)"
|
||||
tal:attributes="class python:'comment replyTreeLevel'+str(depth)+' state-'+str(review_state);
|
||||
style string:margin-left: ${depth}em;
|
||||
id string:${reply/getId}"
|
||||
tal:condition="python:canReview or review_state == 'published'">
|
||||
|
||||
<div class="commentImage" tal:condition="showCommenterImage">
|
||||
<a href="" tal:condition="has_author_link"
|
||||
tal:attributes="href author_home_url">
|
||||
<img src="defaultUser.gif"
|
||||
<img src="defaultUser.png"
|
||||
alt=""
|
||||
border="0"
|
||||
class="defaultuserimg"
|
||||
height="32"
|
||||
tal:attributes="src portrait_url;
|
||||
alt reply/Creator" />
|
||||
alt reply/author_name" />
|
||||
</a>
|
||||
<img src="defaultUser.gif"
|
||||
<img src="defaultUser.png"
|
||||
alt=""
|
||||
border="0"
|
||||
class="defaultuserimg"
|
||||
height="32"
|
||||
tal:condition="not: has_author_link"
|
||||
tal:attributes="src portrait_url;
|
||||
alt reply/Creator" />
|
||||
alt reply/author_name" />
|
||||
</div>
|
||||
|
||||
<div class="documentByLine" i18n:domain="plone.app.discussion">
|
||||
<tal:name>
|
||||
<a href=""
|
||||
tal:condition="has_author_link"
|
||||
tal:content="reply/Creator"
|
||||
tal:content="reply/author_name"
|
||||
tal:attributes="href author_home_url">
|
||||
Poster Name
|
||||
</a>
|
||||
<span tal:condition="not: has_author_link"
|
||||
tal:replace="reply/Creator" />
|
||||
<span tal:condition="not: reply/Creator">Anonymous</span>
|
||||
tal:replace="reply/author_name" />
|
||||
<span tal:condition="not: reply/author_name"
|
||||
i18n:translate="label_anonymous">Anonymous</span>
|
||||
</tal:name>
|
||||
<tal:posted i18n:translate="label_says">says:</tal:posted>
|
||||
<div class="commentDate"
|
||||
@@ -86,7 +89,9 @@
|
||||
<form name="delete"
|
||||
action=""
|
||||
method="post"
|
||||
tal:condition="python: not view.can_review() and view.could_delete_own(reply)"
|
||||
style="display: inline;"
|
||||
class="commentactionsform"
|
||||
tal:condition="python: not canDelete and view.could_delete_own(reply)"
|
||||
tal:attributes="action string:${reply/absolute_url}/@@delete-own-comment;
|
||||
style python:view.can_delete_own(reply) and 'display: inline' or 'display: none'">
|
||||
<input name="form.button.DeleteComment"
|
||||
@@ -101,6 +106,8 @@
|
||||
method="post"
|
||||
style="display: inline;"
|
||||
tal:condition="canReview"
|
||||
class="commentactionsform"
|
||||
tal:condition="python:canDelete"
|
||||
tal:attributes="action string:${reply/absolute_url}/@@moderate-delete-comment">
|
||||
<input name="form.button.DeleteComment"
|
||||
class="destructive"
|
||||
@@ -110,11 +117,26 @@
|
||||
/>
|
||||
</form>
|
||||
|
||||
<form name="edit"
|
||||
action=""
|
||||
method="get"
|
||||
class="commentactionsform"
|
||||
tal:condition="python:isEditCommentAllowed and canEdit"
|
||||
tal:attributes="action string:${reply/absolute_url}/@@edit-comment">
|
||||
<input name="form.button.EditComment"
|
||||
class="context"
|
||||
type="submit"
|
||||
value="Edit"
|
||||
i18n:attributes="value label_edit;"
|
||||
/>
|
||||
</form>
|
||||
|
||||
|
||||
<!-- Workflow actions (e.g. 'publish') -->
|
||||
<form name=""
|
||||
action=""
|
||||
method="get"
|
||||
style="display: inline;"
|
||||
class="commentactionsform"
|
||||
tal:condition="canReview"
|
||||
tal:repeat="action reply_dict/actions|nothing"
|
||||
tal:attributes="action string:${reply/absolute_url}/@@moderate-publish-comment;
|
||||
@@ -152,8 +174,7 @@
|
||||
<div class="reply"
|
||||
tal:condition="python:has_replies and (isAnon and not isAnonymousDiscussionAllowed)">
|
||||
<form tal:attributes="action view/login_action">
|
||||
<input class="standalone"
|
||||
style="margin-bottom: 1.25em;"
|
||||
<input class="standalone loginbutton"
|
||||
type="submit"
|
||||
value="Log in to add comments"
|
||||
i18n:attributes="value label_login_to_add_comments;"
|
||||
|
||||
@@ -38,7 +38,6 @@ 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
|
||||
|
||||
|
||||
@@ -47,19 +46,19 @@ 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.")
|
||||
u"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.")
|
||||
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. " +
|
||||
"Plain text formatting. Web and email addresses are " +
|
||||
"transformed into clickable links.")
|
||||
u"Plain text formatting. Web and email addresses are " +
|
||||
u"transformed into clickable links.")
|
||||
|
||||
COMMENT_DESCRIPTION_MODERATION_ENABLED = _(
|
||||
u"comment_description_moderation_enabled",
|
||||
@@ -95,29 +94,40 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
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
|
||||
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
|
||||
if member_email == '' or \
|
||||
not settings.user_notification_enabled or \
|
||||
mtool.isAnonymousUser():
|
||||
self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE
|
||||
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()
|
||||
@@ -132,7 +142,8 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
|
||||
# Check if conversation is enabled on this content object
|
||||
if not self.__parent__.restrictedTraverse(
|
||||
'@@conversation_view').enabled():
|
||||
'@@conversation_view'
|
||||
).enabled():
|
||||
raise Unauthorized("Discussion is not enabled for this content "
|
||||
"object.")
|
||||
|
||||
@@ -145,9 +156,10 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
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():
|
||||
captcha_enabled = settings.captcha != 'disabled'
|
||||
anonymous_comments = settings.anonymous_comments
|
||||
anon = portal_membership.isAnonymousUser()
|
||||
if captcha_enabled and anonymous_comments and anon:
|
||||
if not 'captcha' in data:
|
||||
data['captcha'] = u""
|
||||
captcha = CaptchaValidator(self.context,
|
||||
@@ -159,38 +171,42 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
|
||||
# some attributes are not always set
|
||||
author_name = u""
|
||||
author_email = u""
|
||||
user_notification = None
|
||||
|
||||
# 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
|
||||
# Make sure author_name/ author_email 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')
|
||||
if 'author_email' in data:
|
||||
author_email = data['author_email']
|
||||
if isinstance(author_email, str):
|
||||
author_email = unicode(author_email, '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:
|
||||
if anon and anonymous_comments:
|
||||
# Anonymous Users
|
||||
comment.creator = author_name
|
||||
comment.author_name = author_name
|
||||
comment.author_email = author_email
|
||||
comment.user_notification = user_notification
|
||||
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()
|
||||
userid = member.getUserId()
|
||||
memberid = member.getId()
|
||||
user = member.getUser()
|
||||
email = member.getProperty('email')
|
||||
fullname = member.getProperty('fullname')
|
||||
if not fullname or fullname == '':
|
||||
@@ -200,21 +216,27 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
fullname = unicode(fullname, 'utf-8')
|
||||
if email and isinstance(email, str):
|
||||
email = unicode(email, 'utf-8')
|
||||
comment.creator = fullname
|
||||
comment.author_username = username
|
||||
comment.changeOwnership(user, recursive=False)
|
||||
comment.manage_setLocalRoles(memberid, ["Owner"])
|
||||
comment.creator = memberid
|
||||
comment.author_username = memberid
|
||||
comment.author_name = fullname
|
||||
|
||||
# XXX: according to IComment interface author_email must not be
|
||||
# set for logged in users, cite:
|
||||
# "for anonymous comments only, set to None for logged in comments"
|
||||
comment.author_email = email
|
||||
comment.user_notification = user_notification
|
||||
# /XXX
|
||||
|
||||
comment.creation_date = datetime.utcnow()
|
||||
comment.modification_date = datetime.utcnow()
|
||||
|
||||
# add local "Owner" role for current user
|
||||
comment.manage_setLocalRoles(userid, ['Owner'])
|
||||
|
||||
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.")
|
||||
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."
|
||||
)
|
||||
|
||||
# Add comment to conversation
|
||||
conversation = IConversation(self.__parent__)
|
||||
@@ -235,7 +257,11 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
can_review = getSecurityManager().checkPermission('Review comments',
|
||||
context)
|
||||
workflowTool = getToolByName(context, 'portal_workflow')
|
||||
comment_review_state = workflowTool.getInfoFor(comment, 'review_state')
|
||||
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(
|
||||
@@ -260,9 +286,13 @@ class CommentsViewlet(ViewletBase):
|
||||
|
||||
def update(self):
|
||||
super(CommentsViewlet, self).update()
|
||||
if self.is_discussion_allowed() and \
|
||||
(self.is_anonymous() and self.anonymous_discussion_allowed() \
|
||||
or self.can_reply()):
|
||||
discussion_allowed = self.is_discussion_allowed()
|
||||
anonymous_allowed_or_can_reply = (
|
||||
self.is_anonymous()
|
||||
and self.anonymous_discussion_allowed()
|
||||
or self.can_reply()
|
||||
)
|
||||
if discussion_allowed and anonymous_allowed_or_can_reply:
|
||||
z2.switch_on(self, request_layer=IFormLayer)
|
||||
self.form = self.form(aq_inner(self.context), self.request)
|
||||
alsoProvides(self.form, IWrappedForm)
|
||||
@@ -293,19 +323,35 @@ class CommentsViewlet(ViewletBase):
|
||||
comments without replies can be deleted.
|
||||
"""
|
||||
try:
|
||||
return comment.restrictedTraverse('@@delete-own-comment').can_delete()
|
||||
return comment.restrictedTraverse(
|
||||
'@@delete-own-comment').can_delete()
|
||||
except Unauthorized:
|
||||
return False
|
||||
|
||||
def could_delete_own(self, comment):
|
||||
"""Returns true if the current user could delete the comment if it had no
|
||||
replies. This is used to prepare hidden form buttons for JS.
|
||||
"""Returns true if the current user could delete the comment if it had
|
||||
no replies. This is used to prepare hidden form buttons for JS.
|
||||
"""
|
||||
try:
|
||||
return comment.restrictedTraverse('@@delete-own-comment').could_delete()
|
||||
return comment.restrictedTraverse(
|
||||
'@@delete-own-comment').could_delete()
|
||||
except Unauthorized:
|
||||
return False
|
||||
|
||||
def can_edit(self, reply):
|
||||
"""Returns true if current user has the 'Edit comments'
|
||||
permission.
|
||||
"""
|
||||
return getSecurityManager().checkPermission('Edit comments',
|
||||
aq_inner(reply))
|
||||
|
||||
def can_delete(self, reply):
|
||||
"""By default requires 'Review comments'.
|
||||
If 'delete own comments' is enabled, requires 'Edit comments'.
|
||||
"""
|
||||
return getSecurityManager().checkPermission('Delete comments',
|
||||
aq_inner(reply))
|
||||
|
||||
def is_discussion_allowed(self):
|
||||
context = aq_inner(self.context)
|
||||
return context.restrictedTraverse('@@conversation_view').enabled()
|
||||
@@ -366,7 +412,10 @@ class CommentsViewlet(ViewletBase):
|
||||
returned with workflow actions.
|
||||
"""
|
||||
context = aq_inner(self.context)
|
||||
conversation = IConversation(context)
|
||||
conversation = IConversation(context, None)
|
||||
|
||||
if conversation is None:
|
||||
return iter([])
|
||||
|
||||
wf = getToolByName(context, 'portal_workflow')
|
||||
# workflow_actions is only true when user
|
||||
@@ -377,8 +426,10 @@ class CommentsViewlet(ViewletBase):
|
||||
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']]
|
||||
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
|
||||
@@ -394,7 +445,7 @@ class CommentsViewlet(ViewletBase):
|
||||
yield r
|
||||
|
||||
# Return all direct replies
|
||||
if conversation.total_comments > 0:
|
||||
if len(conversation.objectIds()):
|
||||
if workflow_actions:
|
||||
return replies_with_workflow_actions()
|
||||
else:
|
||||
@@ -410,13 +461,14 @@ class CommentsViewlet(ViewletBase):
|
||||
|
||||
if username is None:
|
||||
# return the default user image if no username is given
|
||||
return 'defaultUser.gif'
|
||||
return 'defaultUser.png'
|
||||
else:
|
||||
portal_membership = getToolByName(self.context,
|
||||
'portal_membership',
|
||||
None)
|
||||
return portal_membership.getPersonalPortrait(username)\
|
||||
.absolute_url()
|
||||
return portal_membership\
|
||||
.getPersonalPortrait(username)\
|
||||
.absolute_url()
|
||||
|
||||
def anonymous_discussion_allowed(self):
|
||||
# Check if anonymous comments are allowed in the registry
|
||||
@@ -424,6 +476,12 @@ class CommentsViewlet(ViewletBase):
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
return settings.anonymous_comments
|
||||
|
||||
def edit_comment_allowed(self):
|
||||
# Check if editing comments is allowed in the registry
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
return settings.edit_comment_enabled
|
||||
|
||||
def show_commenter_image(self):
|
||||
# Check if showing commenter image is enabled in the registry
|
||||
registry = queryUtility(IRegistry)
|
||||
|
||||
@@ -71,13 +71,24 @@
|
||||
permission="zope2.View"
|
||||
/>
|
||||
|
||||
<!-- Delete comment views -->
|
||||
<!-- Edit comment view -->
|
||||
<browser:page
|
||||
for="plone.app.discussion.interfaces.IComment"
|
||||
name="edit-comment"
|
||||
layer="..interfaces.IDiscussionLayer"
|
||||
class=".comment.EditComment"
|
||||
permission="plone.app.discussion.EditComments"
|
||||
/>
|
||||
|
||||
<!-- Delete comment views
|
||||
has conditional security dependent on controlpanel settings.
|
||||
-->
|
||||
<browser:page
|
||||
for="plone.app.discussion.interfaces.IComment"
|
||||
name="moderate-delete-comment"
|
||||
layer="..interfaces.IDiscussionLayer"
|
||||
class=".moderation.DeleteComment"
|
||||
permission="plone.app.discussion.ReviewComments"
|
||||
permission="plone.app.discussion.DeleteComments"
|
||||
/>
|
||||
|
||||
<browser:page
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
tal:attributes="src string:${context/portal_url}/++resource++plone.app.discussion.javascripts/controlpanel.js">
|
||||
</script>
|
||||
|
||||
<dl class="portalMessage warning"
|
||||
<div class="portalMessage warning"
|
||||
tal:condition="view/mailhost_warning">
|
||||
<dt i18n:translate="">
|
||||
<strong i18n:translate="">
|
||||
Warning
|
||||
</dt>
|
||||
<dd i18n:translate="text_no_mailhost_configured">
|
||||
</strong>
|
||||
<span tal:omit-tag="" i18n:translate="text_no_mailhost_configured">
|
||||
You have not configured a mail host or a site 'From'
|
||||
address, various features including contact forms, email
|
||||
notification and password reset will not work. Go to the
|
||||
@@ -38,15 +38,15 @@
|
||||
>Mail control panel</a>
|
||||
</tal:link>
|
||||
to fix this.
|
||||
</dd>
|
||||
</dl>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<dl class="portalMessage warning"
|
||||
<div class="portalMessage warning"
|
||||
tal:condition="view/custom_comment_workflow_warning">
|
||||
<dt i18n:translate="">
|
||||
<strong i18n:translate="">
|
||||
Warning
|
||||
</dt>
|
||||
<dd i18n:translate="text_custom_comment_workflow">
|
||||
</strong>
|
||||
<span tal:omit-tag="" i18n:translate="text_custom_comment_workflow">
|
||||
You have configured a custom workflow for the 'Discussion Item'
|
||||
content type. You can enable/disable the comment moderation
|
||||
in this control panel only if you use one of the default
|
||||
@@ -58,15 +58,15 @@
|
||||
>Types control panel</a>
|
||||
</tal:link>
|
||||
to choose a workflow for the 'Discussion Item' type.
|
||||
</dd>
|
||||
</dl>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<dl class="portalMessage warning"
|
||||
<div class="portalMessage warning"
|
||||
tal:condition="view/unmigrated_comments_warning">
|
||||
<dt i18n:translate="">
|
||||
<strong i18n:translate="">
|
||||
Warning
|
||||
</dt>
|
||||
<dd i18n:translate="text_unmigrated_comments">
|
||||
</strong>
|
||||
<span tal:omit-tag="" i18n:translate="text_unmigrated_comments">
|
||||
You have comments that have not been migrated to the new
|
||||
commenting system that has been introduced in Plone 4.1.
|
||||
Please
|
||||
@@ -77,8 +77,8 @@
|
||||
>migrate your comments</a>
|
||||
</tal:link>
|
||||
to fix this.
|
||||
</dd>
|
||||
</dl>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div metal:use-macro="context/global_statusmessage/macros/portal_message">
|
||||
Portal status message
|
||||
@@ -89,12 +89,14 @@
|
||||
tal:attributes="href string:$portal_url/plone_control_panel"
|
||||
i18n:translate="">
|
||||
Site Setup
|
||||
</a> ›
|
||||
</a>
|
||||
|
||||
<h1 class="documentFirstHeading" tal:content="view/label">View Title</h1>
|
||||
|
||||
<div id="layout-contents">
|
||||
<span tal:replace="structure view/contents" />
|
||||
<div id="content-core">
|
||||
<div id="layout-contents">
|
||||
<span tal:replace="structure view/contents" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ from z3c.form import button
|
||||
from z3c.form.browser.checkbox import SingleCheckBoxFieldWidget
|
||||
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings, _
|
||||
from plone.app.discussion.upgrades import update_registry
|
||||
|
||||
|
||||
class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
|
||||
@@ -32,16 +33,18 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
|
||||
schema = IDiscussionSettings
|
||||
id = "DiscussionSettingsEditForm"
|
||||
label = _(u"Discussion settings")
|
||||
description = _(u"help_discussion_settings_editform",
|
||||
default=u"Some discussion related settings are not "
|
||||
"located in the Discussion Control Panel.\n"
|
||||
"To enable comments for a specific content type, "
|
||||
"go to the Types Control Panel of this type and "
|
||||
"choose \"Allow comments\".\n"
|
||||
"To enable the moderation workflow for comments, "
|
||||
"go to the Types Control Panel, choose "
|
||||
"\"Comment\" and set workflow to "
|
||||
"\"Comment Review Workflow\".")
|
||||
description = _(
|
||||
u"help_discussion_settings_editform",
|
||||
default=u"Some discussion related settings are not "
|
||||
u"located in the Discussion Control Panel.\n"
|
||||
u"To enable comments for a specific content type, "
|
||||
u"go to the Types Control Panel of this type and "
|
||||
u"choose \"Allow comments\".\n"
|
||||
u"To enable the moderation workflow for comments, "
|
||||
u"go to the Types Control Panel, choose "
|
||||
u"\"Comment\" and set workflow to "
|
||||
u"\"Comment Review Workflow\"."
|
||||
)
|
||||
|
||||
def updateFields(self):
|
||||
super(DiscussionSettingsEditForm, self).updateFields()
|
||||
@@ -49,6 +52,8 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['moderation_enabled'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['edit_comment_enabled'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['anonymous_comments'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['show_commenter_image'].widgetFactory = \
|
||||
@@ -59,7 +64,13 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
|
||||
SingleCheckBoxFieldWidget
|
||||
|
||||
def updateWidgets(self):
|
||||
super(DiscussionSettingsEditForm, self).updateWidgets()
|
||||
try:
|
||||
super(DiscussionSettingsEditForm, self).updateWidgets()
|
||||
except KeyError:
|
||||
# upgrade profile not visible in prefs_install_products_form
|
||||
# provide auto-upgrade
|
||||
update_registry(self.context)
|
||||
super(DiscussionSettingsEditForm, self).updateWidgets()
|
||||
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")
|
||||
@@ -108,12 +119,17 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
|
||||
output.append("globally_enabled")
|
||||
|
||||
# Comment moderation
|
||||
if 'one_state_workflow' not in workflow_chain and \
|
||||
'comment_review_workflow' not in workflow_chain:
|
||||
one_state_worklow_disabled = 'one_state_workflow' not in workflow_chain
|
||||
comment_review_workflow_disabled = \
|
||||
'comment_review_workflow' not in workflow_chain
|
||||
if one_state_worklow_disabled and comment_review_workflow_disabled:
|
||||
output.append("moderation_custom")
|
||||
elif settings.moderation_enabled:
|
||||
output.append("moderation_enabled")
|
||||
|
||||
if settings.edit_comment_enabled:
|
||||
output.append("edit_comment_enabled")
|
||||
|
||||
# Anonymous comments
|
||||
if settings.anonymous_comments:
|
||||
output.append("anonymous_comments")
|
||||
@@ -151,8 +167,10 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
|
||||
"""
|
||||
wftool = getToolByName(self.context, "portal_workflow", None)
|
||||
workflow_chain = wftool.getChainForPortalType('Discussion Item')
|
||||
if 'one_state_workflow' in workflow_chain \
|
||||
or 'comment_review_workflow' in workflow_chain:
|
||||
one_state_workflow_enabled = 'one_state_workflow' in workflow_chain
|
||||
comment_review_workflow_enabled = \
|
||||
'comment_review_workflow' in workflow_chain
|
||||
if one_state_workflow_enabled or comment_review_workflow_enabled:
|
||||
return
|
||||
return True
|
||||
|
||||
@@ -176,7 +194,7 @@ def notify_configuration_changed(event):
|
||||
# Discussion control panel setting changed
|
||||
if event.record.fieldName == 'moderation_enabled':
|
||||
# Moderation enabled has changed
|
||||
if event.record.value == True:
|
||||
if event.record.value is True:
|
||||
# Enable moderation workflow
|
||||
wftool.setChainForPortalTypes(('Discussion Item',),
|
||||
'comment_review_workflow')
|
||||
|
||||
@@ -14,10 +14,22 @@ from Products.CMFPlone.interfaces import INonStructuralFolder
|
||||
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
|
||||
try:
|
||||
from plone.dexterity.interfaces import IDexterityContent
|
||||
DEXTERITY_INSTALLED = True
|
||||
except:
|
||||
DEXTERITY_INSTALLED = False
|
||||
|
||||
|
||||
class ConversationView(object):
|
||||
|
||||
def enabled(self):
|
||||
if DEXTERITY_INSTALLED and IDexterityContent.providedBy(self.context):
|
||||
return self._enabled_for_dexterity_types()
|
||||
else:
|
||||
return self._enabled_for_archetypes()
|
||||
|
||||
def _enabled_for_archetypes(self):
|
||||
""" Returns True if discussion is enabled for this conversation.
|
||||
|
||||
This method checks five different settings in order to figure out if
|
||||
@@ -52,8 +64,9 @@ class ConversationView(object):
|
||||
return False
|
||||
|
||||
# Always return False if object is a folder
|
||||
if (IFolderish.providedBy(context) and
|
||||
not INonStructuralFolder.providedBy(context)):
|
||||
context_is_folderish = IFolderish.providedBy(context)
|
||||
context_is_structural = not INonStructuralFolder.providedBy(context)
|
||||
if (context_is_folderish and context_is_structural):
|
||||
return False
|
||||
|
||||
def traverse_parents(context):
|
||||
@@ -61,8 +74,9 @@ class ConversationView(object):
|
||||
# enabled in a parent folder.
|
||||
for obj in aq_chain(context):
|
||||
if not IPloneSiteRoot.providedBy(obj):
|
||||
if (IFolderish.providedBy(obj) and
|
||||
not INonStructuralFolder.providedBy(obj)):
|
||||
obj_is_folderish = IFolderish.providedBy(obj)
|
||||
obj_is_stuctural = not INonStructuralFolder.providedBy(obj)
|
||||
if (obj_is_folderish and obj_is_stuctural):
|
||||
flag = getattr(obj, 'allow_discussion', None)
|
||||
if flag is not None:
|
||||
return flag
|
||||
@@ -94,3 +108,38 @@ class ConversationView(object):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _enabled_for_dexterity_types(self):
|
||||
""" Returns True if discussion is enabled for this conversation.
|
||||
|
||||
This method checks five different settings in order to figure out if
|
||||
discussion is enable on a specific content object:
|
||||
|
||||
1) Check if discussion is enabled globally in the plone.app.discussion
|
||||
registry/control panel.
|
||||
|
||||
2) Check if the allow_discussion boolean flag on the content object is
|
||||
set. If it is set to True or False, return the value. If it set to
|
||||
None, try further.
|
||||
|
||||
3) Check if discussion is allowed for the content type.
|
||||
"""
|
||||
context = aq_inner(self.context)
|
||||
|
||||
# Fetch discussion registry
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
|
||||
# Check if discussion is allowed globally
|
||||
if not settings.globally_enabled:
|
||||
return False
|
||||
|
||||
# Check if discussion is allowed on the content object
|
||||
if hasattr(context, "allow_discussion"):
|
||||
if context.allow_discussion is not None:
|
||||
return context.allow_discussion
|
||||
|
||||
# Check if discussion is allowed on the content type
|
||||
portal_types = getToolByName(self, 'portal_types')
|
||||
document_fti = getattr(portal_types, context.portal_type)
|
||||
return document_fti.getProperty('allow_discussion')
|
||||
|
||||
@@ -35,8 +35,9 @@
|
||||
*/
|
||||
reply_div.appendTo(comment_div).css("display", "none");
|
||||
|
||||
/* Remove id="reply" attribute, since we use it to uniquely
|
||||
/* Remove id="commenting" attribute, since we use it to uniquely define
|
||||
the main reply form. */
|
||||
// Still belongs to class="reply"
|
||||
reply_div.removeAttr("id");
|
||||
|
||||
/* Hide the reply button (only hide, because we may want to show it
|
||||
@@ -47,6 +48,13 @@
|
||||
/* Fetch the reply form inside the reply div */
|
||||
var reply_form = reply_div.find("form");
|
||||
|
||||
/* Change the id of the textarea of the reply form
|
||||
* To avoid conflict later between textareas with same id 'form-widgets-comment-text' while implementing a seperate instance of TinyMCE
|
||||
* */
|
||||
reply_form.find('#formfield-form-widgets-comment-text').attr('id', 'formfield-form-widgets-new-textarea'+comment_id );
|
||||
reply_form.find('#form-widgets-comment-text').attr('id', 'form-widgets-new-textarea'+comment_id );
|
||||
|
||||
|
||||
/* Populate the hidden 'in_reply_to' field with the correct comment
|
||||
id */
|
||||
reply_form.find("input[name='form.widgets.in_reply_to']")
|
||||
@@ -127,23 +135,23 @@
|
||||
parents().
|
||||
filter(".comment").
|
||||
find(".reply-to-comment-button");
|
||||
|
||||
|
||||
/* Find the reply-to-comment form and hide and remove it again. */
|
||||
$.reply_to_comment_form = $(this).parents().filter(".reply");
|
||||
$.reply_to_comment_form.slideUp("slow", function () {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
|
||||
/* Show the reply-to-comment button again. */
|
||||
reply_to_comment_button.css("display", "inline");
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Publish a single comment.
|
||||
**********************************************************************/
|
||||
$("input[name='form.button.PublishComment']").live('click', function () {
|
||||
$("input[name='form.button.PublishComment']").on('click', function () {
|
||||
var trigger = this;
|
||||
var form = $(this).parents("form");
|
||||
var data = $(form).serialize();
|
||||
@@ -151,7 +159,7 @@
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: form_url,
|
||||
data: "workflow_action=publish",
|
||||
data: data,
|
||||
context: trigger,
|
||||
success: function (msg) {
|
||||
// remove button (trigger object can't be directly removed)
|
||||
@@ -165,11 +173,20 @@
|
||||
return false;
|
||||
});
|
||||
|
||||
/**********************************************************************
|
||||
* Edit a comment
|
||||
**********************************************************************/
|
||||
$("form[name='edit']").prepOverlay({
|
||||
cssclass: 'overlay-edit-comment',
|
||||
width: '60%',
|
||||
subtype: 'ajax',
|
||||
filter: '#content>*'
|
||||
})
|
||||
|
||||
/**********************************************************************
|
||||
* Delete a comment and its answers.
|
||||
**********************************************************************/
|
||||
$("input[name='form.button.DeleteComment']").live('click', function () {
|
||||
$("input[name='form.button.DeleteComment']").on('click', function () {
|
||||
var trigger = this;
|
||||
var form = $(this).parents("form");
|
||||
var data = $(form).serialize();
|
||||
@@ -177,6 +194,7 @@
|
||||
$.ajax({
|
||||
type: 'POST',
|
||||
url: form_url,
|
||||
data: data,
|
||||
context: $(trigger).parents(".comment"),
|
||||
success: function (data) {
|
||||
var comment = $(this);
|
||||
|
||||
@@ -7,39 +7,40 @@
|
||||
// This unnamed function allows us to use $ inside of a block of code
|
||||
// without permanently overwriting $.
|
||||
// http://docs.jquery.com/Using_jQuery_with_Other_Libraries
|
||||
|
||||
|
||||
/* Disable a control panel setting */
|
||||
$.disableSettings = function (settings) {
|
||||
$.each(settings, function (intIndex, setting) {
|
||||
setting.addClass('unclickable');
|
||||
var setting_field = $(setting).find("input,select");
|
||||
setting_field.attr('disabled', 'disabled');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/* Enable a control panel setting */
|
||||
$.enableSettings = function (settings) {
|
||||
$.each(settings, function (intIndex, setting) {
|
||||
setting.removeClass('unclickable');
|
||||
var setting_field = $(setting).find("input,select");
|
||||
setting_field.removeAttr('disabled');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/* Update settings */
|
||||
$.updateSettings = function () {
|
||||
|
||||
|
||||
var globally_enabled = $("#content").hasClass("globally_enabled");
|
||||
var anonymous_comments = $("#content").hasClass("anonymous_comments");
|
||||
var moderation_enabled = $("#content").hasClass("moderation_enabled");
|
||||
var moderation_custom = $("#content").hasClass("moderation_custom");
|
||||
var invalid_mail_setup = $("#content").hasClass("invalid_mail_setup");
|
||||
|
||||
|
||||
/* If commenting is globally disabled, disable all settings. */
|
||||
if (globally_enabled === true) {
|
||||
$.enableSettings([
|
||||
$('#formfield-form-widgets-anonymous_comments'),
|
||||
$('#formfield-form-widgets-moderation_enabled'),
|
||||
$('#formfield-form-widgets-edit_comment_enabled'),
|
||||
$('#formfield-form-widgets-text_transform'),
|
||||
$('#formfield-form-widgets-captcha'),
|
||||
$('#formfield-form-widgets-show_commenter_image'),
|
||||
@@ -52,6 +53,7 @@
|
||||
$.disableSettings([
|
||||
$('#formfield-form-widgets-anonymous_comments'),
|
||||
$('#formfield-form-widgets-moderation_enabled'),
|
||||
$('#formfield-form-widgets-edit_comment_enabled'),
|
||||
$('#formfield-form-widgets-text_transform'),
|
||||
$('#formfield-form-widgets-captcha'),
|
||||
$('#formfield-form-widgets-show_commenter_image'),
|
||||
@@ -60,7 +62,7 @@
|
||||
$('#formfield-form-widgets-user_notification_enabled')
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/* If the mail setup is invalid, disable the mail settings. */
|
||||
if (invalid_mail_setup === true) {
|
||||
$.disableSettings([
|
||||
@@ -73,14 +75,14 @@
|
||||
/* Enable mail setup only if discussion is enabled. */
|
||||
if (globally_enabled === true) {
|
||||
$.enableSettings([
|
||||
$('#formfield-form-widgets-moderator_notification_enabled'),
|
||||
$('#formfield-form-widgets-moderator_email'),
|
||||
$('#formfield-form-widgets-moderator_notification_enabled'),
|
||||
$('#formfield-form-widgets-moderator_email'),
|
||||
$('#formfield-form-widgets-user_notification_enabled')
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/* If a custom workflow for comments is enabled, disable the moderation
|
||||
|
||||
/* If a custom workflow for comments is enabled, disable the moderation
|
||||
switch. */
|
||||
if (moderation_custom === true) {
|
||||
$.disableSettings([
|
||||
@@ -89,21 +91,21 @@
|
||||
}
|
||||
};
|
||||
//#JSCOVERAGE_IF 0
|
||||
|
||||
|
||||
/**************************************************************************
|
||||
* Window Load Function: Executes when complete page is fully loaded,
|
||||
* including all frames,
|
||||
**************************************************************************/
|
||||
$(window).load(function () {
|
||||
|
||||
|
||||
// Update settings on page load
|
||||
$.updateSettings();
|
||||
|
||||
|
||||
// Set #content class and update settings afterwards
|
||||
$("input,select").live("change", function (e) {
|
||||
$("input,select").on("change", function (e) {
|
||||
var id = $(this).attr("id");
|
||||
if (id === "form-widgets-globally_enabled-0") {
|
||||
if ($(this).attr("checked") === true) {
|
||||
if (id === "form-widgets-globally_enabled-0") {
|
||||
if ($(this).attr("checked")) {
|
||||
$("#content").addClass("globally_enabled");
|
||||
}
|
||||
else {
|
||||
@@ -112,17 +114,16 @@
|
||||
$.updateSettings();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Remove the disabled attribute from all form elements before
|
||||
* Remove the disabled attribute from all form elements before
|
||||
* submitting the form. Otherwise the z3c.form will raise errors on
|
||||
* the required attributes.
|
||||
**********************************************************************/
|
||||
$("form#DiscussionSettingsEditForm").bind("submit", function (e) {
|
||||
$(this).find("input,select").removeAttr('disabled');
|
||||
$(this).submit();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
//#JSCOVERAGE_ENDIF
|
||||
|
||||
@@ -17,7 +17,7 @@ content object. Each comment div has a unique id::
|
||||
<div class="documentByLine"></div>
|
||||
<div class="commentBody"> </div>
|
||||
<div class="commentActions">
|
||||
<button class="reply-to-comment-button">Reply</button>
|
||||
<button class="reply-to-comment-button">Reply</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@ The comment form is rendered inside a "commenting" div::
|
||||
<div id="formfield-form-widgets-in_reply_to">
|
||||
<input id="form-widgets-in_reply_to"
|
||||
name="form.widgets.in_reply_to" value=
|
||||
type="hidden"
|
||||
type="hidden"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
/******************************************************************************
|
||||
*
|
||||
*
|
||||
* jQuery functions for the plone.app.discussion bulk moderation.
|
||||
*
|
||||
*
|
||||
******************************************************************************/
|
||||
|
||||
(function ($) {
|
||||
// This unnamed function allows us to use $ inside of a block of code
|
||||
// This unnamed function allows us to use $ inside of a block of code
|
||||
// without permanently overwriting $.
|
||||
// http://docs.jquery.com/Using_jQuery_with_Other_Libraries
|
||||
|
||||
|
||||
//#JSCOVERAGE_IF 0
|
||||
|
||||
|
||||
/**************************************************************************
|
||||
* Window Load Function: Executes when complete page is fully loaded,
|
||||
* Window Load Function: Executes when complete page is fully loaded,
|
||||
* including all frames,
|
||||
**************************************************************************/
|
||||
**************************************************************************/
|
||||
$(window).load(function () {
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Delete a single comment.
|
||||
**********************************************************************/
|
||||
@@ -47,8 +47,8 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Publish a single comment.
|
||||
**********************************************************************/
|
||||
@@ -78,8 +78,8 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Bulk actions for comments (delete, publish)
|
||||
**********************************************************************/
|
||||
@@ -116,8 +116,8 @@
|
||||
selectField.find("option[value='-1']").attr('selected', 'selected');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Check or uncheck all checkboxes from the batch moderation page.
|
||||
**********************************************************************/
|
||||
@@ -134,12 +134,12 @@
|
||||
$(this).val("0");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
/**********************************************************************
|
||||
* Show full text of a comment in the batch moderation page.
|
||||
**********************************************************************/
|
||||
$(".show-full-comment-text").click(function (e) {
|
||||
$(".show-full-comment-text").click(function (e) {
|
||||
e.preventDefault();
|
||||
var target = $(this).attr("href");
|
||||
var td = $(this).parent();
|
||||
@@ -156,9 +156,9 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
//#JSCOVERAGE_ENDIF
|
||||
|
||||
|
||||
}(jQuery));
|
||||
|
||||
@@ -14,6 +14,9 @@ from plone.app.discussion.comment import CommentFactory
|
||||
|
||||
from plone.app.discussion.interfaces import IConversation, IReplies, IComment
|
||||
|
||||
from types import TupleType
|
||||
from DateTime import DateTime
|
||||
|
||||
|
||||
def DT2dt(DT):
|
||||
"""Convert a Zope DateTime (with timezone) into a Python datetime (GMT)."""
|
||||
@@ -64,8 +67,15 @@ class View(BrowserView):
|
||||
if len(replies) == 0:
|
||||
return True
|
||||
|
||||
for reply in replies:
|
||||
workflow = context.portal_workflow
|
||||
oldchain = workflow.getChainForPortalType('Discussion Item')
|
||||
new_workflow = workflow.comment_review_workflow
|
||||
mt = getToolByName(self.context, 'portal_membership')
|
||||
|
||||
if type(oldchain) == TupleType and len(oldchain) > 0:
|
||||
oldchain = oldchain[0]
|
||||
|
||||
for reply in replies:
|
||||
# log
|
||||
indent = " "
|
||||
for i in range(depth):
|
||||
@@ -87,9 +97,30 @@ class View(BrowserView):
|
||||
comment.mime_type = 'text/html'
|
||||
comment.creator = reply.Creator()
|
||||
|
||||
email = reply.getProperty('email', None)
|
||||
if email:
|
||||
comment.author_email = email
|
||||
try:
|
||||
comment.author_username = reply.author_username
|
||||
except AttributeError:
|
||||
comment.author_username = reply.Creator()
|
||||
|
||||
member = mt.getMemberById(comment.author_username)
|
||||
if member:
|
||||
comment.author_name = member.fullname
|
||||
|
||||
if not comment.author_name:
|
||||
# In migrated site member.fullname = ''
|
||||
# while member.getProperty('fullname') has the
|
||||
# correct value
|
||||
if member:
|
||||
comment.author_name = member.getProperty(
|
||||
'fullname'
|
||||
)
|
||||
else:
|
||||
comment.author_name = comment.author_username
|
||||
|
||||
try:
|
||||
comment.author_email = reply.email
|
||||
except AttributeError:
|
||||
comment.author_email = None
|
||||
|
||||
comment.creation_date = DT2dt(reply.creation_date)
|
||||
comment.modification_date = DT2dt(reply.modification_date)
|
||||
@@ -105,6 +136,32 @@ class View(BrowserView):
|
||||
replies = IReplies(comment_to_reply_to)
|
||||
new_in_reply_to = replies.addComment(comment)
|
||||
|
||||
# migrate the review state
|
||||
old_status = workflow.getStatusOf(oldchain, reply)
|
||||
new_status = {
|
||||
'action': None,
|
||||
'actor': None,
|
||||
'comment': 'Migrated workflow state',
|
||||
'review_state': old_status and old_status.get(
|
||||
'review_state',
|
||||
new_workflow.initial_state
|
||||
) or 'published',
|
||||
'time': DateTime()
|
||||
}
|
||||
workflow.setStatusOf('comment_review_workflow',
|
||||
comment,
|
||||
new_status)
|
||||
|
||||
auto_transition = new_workflow._findAutomaticTransition(
|
||||
comment,
|
||||
new_workflow._getWorkflowStateOf(comment))
|
||||
if auto_transition is not None:
|
||||
new_workflow._changeStateOf(comment, auto_transition)
|
||||
else:
|
||||
new_workflow.updateRoleMappingsFor(comment)
|
||||
comment.reindexObject(idxs=['allowedRolesAndUsers',
|
||||
'review_state'])
|
||||
|
||||
self.total_comments_migrated += 1
|
||||
|
||||
# migrate all talkbacks of the reply
|
||||
@@ -133,13 +190,17 @@ class View(BrowserView):
|
||||
object_provides='Products.CMFCore.interfaces._content.IContentish')
|
||||
log("Found %s content objects." % len(brains))
|
||||
|
||||
count_discussion_items = len(catalog.searchResults(
|
||||
Type='Discussion Item'))
|
||||
count_comments_pad = len(catalog.searchResults(
|
||||
object_provides=IComment.__identifier__))
|
||||
count_comments_old = len(catalog.searchResults(
|
||||
object_provides=IDiscussionResponse.\
|
||||
__identifier__))
|
||||
count_discussion_items = len(
|
||||
catalog.searchResults(Type='Discussion Item')
|
||||
)
|
||||
count_comments_pad = len(
|
||||
catalog.searchResults(object_provides=IComment.__identifier__)
|
||||
)
|
||||
count_comments_old = len(
|
||||
catalog.searchResults(
|
||||
object_provides=IDiscussionResponse.__identifier__
|
||||
)
|
||||
)
|
||||
|
||||
log("Found %s Discussion Item objects." % count_discussion_items)
|
||||
log("Found %s old discussion items." % count_comments_old)
|
||||
@@ -172,10 +233,14 @@ class View(BrowserView):
|
||||
obj.talkback = None
|
||||
|
||||
if self.total_comments_deleted != self.total_comments_migrated:
|
||||
log("Something went wrong during migration. The number of \
|
||||
migrated comments (%s) differs from the number of deleted \
|
||||
comments (%s)." # pragma: no cover
|
||||
% (self.total_comments_migrated, self.total_comments_deleted))
|
||||
log(
|
||||
"Something went wrong during migration. The number of " +
|
||||
"migrated comments (%s) differs from the number of deleted " +
|
||||
"comments (%s)." % (
|
||||
self.total_comments_migrated,
|
||||
self.total_comments_deleted
|
||||
)
|
||||
)
|
||||
if not test: # pragma: no cover
|
||||
transaction.abort() # pragma: no cover
|
||||
log("Abort transaction") # pragma: no cover
|
||||
@@ -188,9 +253,11 @@ class View(BrowserView):
|
||||
% (self.total_comments_migrated, count_comments_old))
|
||||
|
||||
if self.total_comments_migrated != count_comments_old:
|
||||
log("%s comments could not be migrated." %
|
||||
(count_comments_old - \
|
||||
self.total_comments_migrated)) # pragma: no cover
|
||||
log(
|
||||
"%s comments could not be migrated." % (
|
||||
count_comments_old - self.total_comments_migrated
|
||||
)
|
||||
) # pragma: no cover
|
||||
log("Please make sure your " +
|
||||
"portal catalog is up-to-date.") # pragma: no cover
|
||||
|
||||
|
||||
@@ -25,18 +25,18 @@
|
||||
Moderate comments
|
||||
</h1>
|
||||
|
||||
<dl class="portalMessage warning"
|
||||
<div class="portalMessage warning"
|
||||
tal:condition="not: view/moderation_enabled">
|
||||
<dt i18n:domain="plone" i18n:translate="">Warning</dt>
|
||||
<dd i18n:translate="message_moderation_disabled">
|
||||
<strong i18n:domain="plone" i18n:translate="">Warning</strong>
|
||||
<span tal:omit-tag="" i18n:translate="message_moderation_disabled">
|
||||
Moderation workflow is disabled. You have to
|
||||
<a i18n:name="enable_comment_workflow"
|
||||
i18n:translate="message_enable_comment_workflow" href=""
|
||||
tal:attributes="href string:${context/portal_url}/@@types-controlpanel?type_id=Discussion Item">
|
||||
enable the 'Comment Review Workflow' for the Comment content
|
||||
type</a> before you can moderate comments here.
|
||||
</dd>
|
||||
</dl>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form tal:condition="not:items">
|
||||
<fieldset id="fieldset-moderate-comments" class="formPanel">
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<div metal:use-macro="here/batch_macros/macros/navigation" />
|
||||
|
||||
<table id="review-comments" class="listing" style="width: 100%">
|
||||
<table id="review-comments" class="listing">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="bulkactions" class="nosort" colspan="7">
|
||||
@@ -87,7 +87,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tal:block repeat="item batch">
|
||||
<tr style="vertical-align: top" class="commentrow"
|
||||
<tr class="commentrow"
|
||||
tal:define="even repeat/item/even"
|
||||
tal:attributes="class python: even and 'odd' or 'even'">
|
||||
<td class="notDraggable">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from Acquisition import aq_inner, aq_parent
|
||||
from AccessControl import getSecurityManager
|
||||
from zope.component import queryUtility
|
||||
|
||||
from AccessControl import Unauthorized, getSecurityManager
|
||||
|
||||
@@ -10,6 +12,8 @@ from Products.CMFCore.utils import getToolByName
|
||||
|
||||
from Products.statusmessages.interfaces import IStatusMessage
|
||||
|
||||
from plone.registry.interfaces import IRegistry
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.app.discussion.interfaces import _
|
||||
from plone.app.discussion.interfaces import IComment
|
||||
from plone.app.discussion.interfaces import IReplies
|
||||
@@ -97,15 +101,28 @@ class DeleteComment(BrowserView):
|
||||
comment = aq_inner(self.context)
|
||||
conversation = aq_parent(comment)
|
||||
content_object = aq_parent(conversation)
|
||||
del conversation[comment.id]
|
||||
IStatusMessage(self.context.REQUEST).addStatusMessage(
|
||||
_("Comment deleted."),
|
||||
type="info")
|
||||
# conditional security
|
||||
# base ZCML condition zope2.deleteObject allows 'delete own object'
|
||||
# modify this for 'delete_own_comment_allowed' controlpanel setting
|
||||
if self.can_delete(comment):
|
||||
del conversation[comment.id]
|
||||
content_object.reindexObject()
|
||||
IStatusMessage(self.context.REQUEST).addStatusMessage(
|
||||
_("Comment deleted."),
|
||||
type="info")
|
||||
came_from = self.context.REQUEST.HTTP_REFERER
|
||||
if len(came_from) == 0:
|
||||
# if the referrer already has a came_from in it, don't redirect back
|
||||
if len(came_from) == 0 or 'came_from=' in came_from:
|
||||
came_from = content_object.absolute_url()
|
||||
return self.context.REQUEST.RESPONSE.redirect(came_from)
|
||||
|
||||
def can_delete(self, reply):
|
||||
"""By default requires 'Review comments'.
|
||||
If 'delete own comments' is enabled, requires 'Edit comments'.
|
||||
"""
|
||||
return getSecurityManager().checkPermission('Delete comments',
|
||||
aq_inner(reply))
|
||||
|
||||
|
||||
class DeleteOwnComment(DeleteComment):
|
||||
"""Delete an own comment if it has no replies. Following conditions have to be true
|
||||
@@ -160,16 +177,16 @@ class PublishComment(BrowserView):
|
||||
comment = aq_inner(self.context)
|
||||
content_object = aq_parent(aq_parent(comment))
|
||||
workflowTool = getToolByName(comment, 'portal_workflow', None)
|
||||
current_state = workflowTool.getInfoFor(comment, 'review_state')
|
||||
if current_state != 'published':
|
||||
workflowTool.doActionFor(comment, 'publish')
|
||||
catalogTool = getToolByName(comment, 'portal_catalog')
|
||||
catalogTool.reindexObject(comment)
|
||||
workflow_action = self.request.form.get('workflow_action', 'publish')
|
||||
workflowTool.doActionFor(comment, workflow_action)
|
||||
comment.reindexObject()
|
||||
content_object.reindexObject(idxs=['total_comments'])
|
||||
IStatusMessage(self.context.REQUEST).addStatusMessage(
|
||||
_("Comment approved."),
|
||||
type="info")
|
||||
came_from = self.context.REQUEST.HTTP_REFERER
|
||||
if len(came_from) == 0:
|
||||
# if the referrer already has a came_from in it, don't redirect back
|
||||
if len(came_from) == 0 or 'came_from=' in came_from:
|
||||
came_from = content_object.absolute_url()
|
||||
return self.context.REQUEST.RESPONSE.redirect(came_from)
|
||||
|
||||
@@ -230,12 +247,13 @@ class BulkActionsView(BrowserView):
|
||||
context = aq_inner(self.context)
|
||||
for path in self.paths:
|
||||
comment = context.restrictedTraverse(path)
|
||||
content_object = aq_parent(aq_parent(comment))
|
||||
workflowTool = getToolByName(comment, 'portal_workflow')
|
||||
current_state = workflowTool.getInfoFor(comment, 'review_state')
|
||||
if current_state != 'published':
|
||||
workflowTool.doActionFor(comment, 'publish')
|
||||
catalog = getToolByName(comment, 'portal_catalog')
|
||||
catalog.reindexObject(comment)
|
||||
comment.reindexObject()
|
||||
content_object.reindexObject(idxs=['total_comments'])
|
||||
|
||||
def mark_as_spam(self):
|
||||
raise NotImplementedError
|
||||
@@ -252,4 +270,6 @@ class BulkActionsView(BrowserView):
|
||||
for path in self.paths:
|
||||
comment = context.restrictedTraverse(path)
|
||||
conversation = aq_parent(comment)
|
||||
content_object = aq_parent(conversation)
|
||||
del conversation[comment.id]
|
||||
content_object.reindexObject(idxs=['total_comments'])
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
.discussion,
|
||||
#commenting {
|
||||
/* Clear "float: left" from "manage portlets" div above the
|
||||
comment viewlet. This will get fixed in the next Plone 4
|
||||
comment viewlet. This will get fixed in the next Plone 4
|
||||
release. */
|
||||
clear: both;
|
||||
}
|
||||
@@ -46,6 +46,7 @@
|
||||
clear: both;
|
||||
margin: 1em 0;
|
||||
overflow: auto;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.commentBody p {
|
||||
@@ -55,7 +56,7 @@
|
||||
.discussion .documentByLine {
|
||||
float: left;
|
||||
margin-left: 0;
|
||||
margin-bottom: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.commentActions {
|
||||
@@ -65,8 +66,52 @@
|
||||
|
||||
.discussion .discreet {
|
||||
color: #666666;
|
||||
font-size: 85%;
|
||||
font-size: 85%;
|
||||
}
|
||||
.loginbutton {
|
||||
margin-bottom: 1.25em;
|
||||
}
|
||||
.commentactionsform {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.replyTreeLevel0 {
|
||||
margin-left: 0em;
|
||||
}
|
||||
.replyTreeLevel1 {
|
||||
margin-left: 1em;
|
||||
}
|
||||
.replyTreeLevel2 {
|
||||
margin-left: 2em;
|
||||
}
|
||||
.replyTreeLevel3 {
|
||||
margin-left: 3em;
|
||||
}
|
||||
.replyTreeLevel4 {
|
||||
margin-left: 4em;
|
||||
}
|
||||
.replyTreeLevel5 {
|
||||
margin-left: 5em;
|
||||
}
|
||||
.replyTreeLevel6 {
|
||||
margin-left: 6em;
|
||||
}
|
||||
.replyTreeLevel7 {
|
||||
margin-left: 7em;
|
||||
}
|
||||
.replyTreeLevel8 {
|
||||
margin-left: 8em;
|
||||
}
|
||||
.replyTreeLevel9 {
|
||||
margin-left: 9em;
|
||||
}
|
||||
.replyTreeLevel10 {
|
||||
margin-left: 10em;
|
||||
}
|
||||
.defaultuserimg {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Reply Form
|
||||
-------------------------------------------------------------------------- */
|
||||
@@ -78,7 +123,7 @@
|
||||
|
||||
.reply .text-widget {
|
||||
/* Make sure the input fields are always below the label. This sometimes
|
||||
* break when the reply form is copied from the main comment form.
|
||||
* break when the reply form is copied from the main comment form.
|
||||
*/
|
||||
display: block;
|
||||
}
|
||||
@@ -120,6 +165,10 @@
|
||||
|
||||
#review-comments {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
#review-comments tbody tr {
|
||||
vertical-align: top
|
||||
}
|
||||
|
||||
#fieldset-moderate-comments {
|
||||
@@ -137,7 +186,7 @@
|
||||
/* Plone 4 Styles
|
||||
-------------------------------------------------------------------------- */
|
||||
|
||||
/* These styles are only applied for Plone 4, since Plone 3.x does
|
||||
/* These styles are only applied for Plone 4, since Plone 3.x does
|
||||
not have a .row class for portal-column-content. */
|
||||
|
||||
.row #dobulkaction {
|
||||
@@ -174,3 +223,9 @@
|
||||
.row .discussion label {
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
/* editing comments */
|
||||
|
||||
.overlay-edit-comment textarea {
|
||||
height: 10em;
|
||||
}
|
||||
@@ -32,7 +32,7 @@ except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from plone.formwidget.recaptcha.validator import WrongCaptchaCode
|
||||
from plone.formwidget.recaptcha.validator import WrongCaptchaCode # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user