This commit is contained in:
Jens W. Klein
2022-05-01 23:14:09 +02:00
parent e72d86b985
commit 34b758f2bd
38 changed files with 2155 additions and 2179 deletions
+146 -149
View File
@@ -35,28 +35,28 @@ 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.',
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.',
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.',
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.",
)
COMMENT_DESCRIPTION_MODERATION_ENABLED = _(
u'comment_description_moderation_enabled',
default=u'Comments are moderated.',
u"comment_description_moderation_enabled",
default=u"Comments are moderated.",
)
@@ -64,30 +64,31 @@ 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')
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
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'')
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
@@ -97,19 +98,19 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
# 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
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
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'
self.widgets["text"].id = "form-widgets-comment-text"
# Anonymous / Logged-in
mtool = getToolByName(self.context, 'portal_membership')
mtool = getToolByName(self.context, "portal_membership")
anon = mtool.isAnonymousUser()
registry = queryUtility(IRegistry)
@@ -119,52 +120,51 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
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
self.widgets["author_email"].field.required = True
self.widgets["author_email"].required = True
else:
self.widgets['author_email'].mode = interfaces.HIDDEN_MODE
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
self.widgets["author_name"].mode = interfaces.HIDDEN_MODE
self.widgets["author_email"].mode = interfaces.HIDDEN_MODE
member = mtool.getAuthenticatedMember()
member_email = member.getProperty('email')
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 == ''
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
self.widgets["user_notification"].mode = interfaces.HIDDEN_MODE
def updateActions(self):
super(CommentForm, self).updateActions()
self.actions['cancel'].addClass('btn btn-secondary')
self.actions['cancel'].addClass('hide')
self.actions['comment'].addClass('btn btn-primary')
self.actions["cancel"].addClass("btn btn-secondary")
self.actions["cancel"].addClass("hide")
self.actions["comment"].addClass("btn btn-primary")
def get_author(self, data):
context = aq_inner(self.context)
# some attributes are not always set
author_name = u''
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'])
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')
portal_membership = getToolByName(context, "portal_membership")
anon = portal_membership.isAnonymousUser()
if not anon and getSecurityManager().checkPermission(
'Reply to item', context):
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 == '':
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
@@ -179,7 +179,7 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
def create_comment(self, data):
context = aq_inner(self.context)
comment = createObject('plone.Comment')
comment = createObject("plone.Comment")
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
@@ -200,42 +200,44 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
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')
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):
"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.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"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')
@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',
"@@conversation_view",
).enabled():
raise Unauthorized(
'Discussion is not enabled for this content object.',
"Discussion is not enabled for this content object.",
)
# Validation form
@@ -246,28 +248,26 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
# Validate Captcha
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
portal_membership = getToolByName(self.context, 'portal_membership')
captcha_enabled = settings.captcha != 'disabled'
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'])
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']:
if data["in_reply_to"]:
# Add a reply to an existing comment
conversation_to_reply_to = conversation.get(data['in_reply_to'])
conversation_to_reply_to = conversation.get(data["in_reply_to"])
replies = IReplies(conversation_to_reply_to)
comment_id = replies.addComment(comment)
else:
@@ -279,25 +279,24 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
# 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')
can_review = getSecurityManager().checkPermission("Review comments", context)
workflowTool = getToolByName(context, "portal_workflow")
comment_review_state = workflowTool.getInfoFor(
comment,
'review_state',
"review_state",
None,
)
if comment_review_state == 'pending' and not can_review:
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')
_("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))
self.request.response.redirect(self.action + "#" + str(comment_id))
@button.buttonAndHandler(_(u'Cancel'))
@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.
@@ -307,15 +306,15 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
class CommentsViewlet(ViewletBase):
form = CommentForm
index = ViewPageTemplateFile('comments.pt')
index = ViewPageTemplateFile("comments.pt")
def update(self):
super(CommentsViewlet, self).update()
discussion_allowed = self.is_discussion_allowed()
anonymous_allowed_or_can_reply = (
self.is_anonymous() and
self.anonymous_discussion_allowed() or
self.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)
@@ -326,30 +325,29 @@ class CommentsViewlet(ViewletBase):
# view methods
def can_reply(self):
"""Returns true if current user has the 'Reply to item' permission.
"""
return getSecurityManager().checkPermission('Reply to item',
aq_inner(self.context))
"""Returns true if current user has the 'Reply to item' permission."""
return getSecurityManager().checkPermission(
"Reply to item", aq_inner(self.context)
)
def can_manage(self):
"""We keep this method for <= 1.0b9 backward compatibility. Since we do
not want any API changes in beta releases.
not want any API changes in beta releases.
"""
return self.can_review()
def can_review(self):
"""Returns true if current user has the 'Review comments' permission.
"""
return getSecurityManager().checkPermission('Review comments',
aq_inner(self.context))
"""Returns true if current user has the 'Review comments' permission."""
return getSecurityManager().checkPermission(
"Review comments", aq_inner(self.context)
)
def can_delete_own(self, comment):
"""Returns true if the current user can delete the comment. Only
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
@@ -358,8 +356,7 @@ class CommentsViewlet(ViewletBase):
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
@@ -367,58 +364,63 @@ class CommentsViewlet(ViewletBase):
"""Returns true if current user has the 'Edit comments'
permission.
"""
return getSecurityManager().checkPermission('Edit comments',
aq_inner(reply))
return getSecurityManager().checkPermission("Edit comments", aq_inner(reply))
def can_delete(self, reply):
"""Returns true if current user has the 'Delete comments'
permission.
"""
return getSecurityManager().checkPermission('Delete comments',
aq_inner(reply))
return getSecurityManager().checkPermission("Delete comments", aq_inner(reply))
def is_discussion_allowed(self):
context = aq_inner(self.context)
return context.restrictedTraverse('@@conversation_view').enabled()
return context.restrictedTraverse("@@conversation_view").enabled()
def comment_transform_message(self):
"""Returns the description that shows up above the comment text,
dependent on the text_transform setting and the comment moderation
workflow in the discussion control panel.
dependent on the text_transform setting and the comment moderation
workflow in the discussion control panel.
"""
context = aq_inner(self.context)
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
# text transform setting
if settings.text_transform == 'text/x-web-intelligent':
message = translate(Message(COMMENT_DESCRIPTION_INTELLIGENT_TEXT),
context=self.request)
elif settings.text_transform == 'text/x-web-markdown':
message = translate(Message(COMMENT_DESCRIPTION_MARKDOWN),
context=self.request)
if settings.text_transform == "text/x-web-intelligent":
message = translate(
Message(COMMENT_DESCRIPTION_INTELLIGENT_TEXT), context=self.request
)
elif settings.text_transform == "text/x-web-markdown":
message = translate(
Message(COMMENT_DESCRIPTION_MARKDOWN), context=self.request
)
else:
message = translate(Message(COMMENT_DESCRIPTION_PLAIN_TEXT),
context=self.request)
message = translate(
Message(COMMENT_DESCRIPTION_PLAIN_TEXT), context=self.request
)
# comment workflow
wftool = getToolByName(context, 'portal_workflow', None)
workflow_chain = wftool.getChainForPortalType('Discussion Item')
wftool = getToolByName(context, "portal_workflow", None)
workflow_chain = wftool.getChainForPortalType("Discussion Item")
if workflow_chain:
comment_workflow = workflow_chain[0]
comment_workflow = wftool[comment_workflow]
# check if the current workflow implements a pending state. If this
# is true comments are moderated
if 'pending' in comment_workflow.states:
message = message + ' ' + \
translate(Message(COMMENT_DESCRIPTION_MODERATION_ENABLED),
context=self.request)
if "pending" in comment_workflow.states:
message = (
message
+ " "
+ translate(
Message(COMMENT_DESCRIPTION_MODERATION_ENABLED),
context=self.request,
)
)
return message
def has_replies(self, workflow_actions=False):
"""Returns true if there are replies.
"""
"""Returns true if there are replies."""
if self.get_replies(workflow_actions) is not None:
try:
next(self.get_replies(workflow_actions))
@@ -442,31 +444,32 @@ class CommentsViewlet(ViewletBase):
if conversation is None:
return iter([])
wf = getToolByName(context, 'portal_workflow')
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']
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']
a
for a in wf.listActionInfos(object=comment_obj)
if a["category"] == "workflow" and a["allowed"]
]
r = r.copy()
r['actions'] = actions
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':
comment_obj = r["comment"]
workflow_status = wf.getInfoFor(comment_obj, "review_state")
if workflow_status == "published":
r = r.copy()
r['workflow_status'] = workflow_status
r["workflow_status"] = workflow_status
yield r
# Return all direct replies
@@ -480,20 +483,16 @@ class CommentsViewlet(ViewletBase):
if username is None:
return None
else:
return '{0}/author/{1}'.format(self.context.portal_url(), username)
return "{0}/author/{1}".format(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.png'
return "defaultUser.png"
else:
portal_membership = getToolByName(self.context,
'portal_membership',
None)
return portal_membership\
.getPersonalPortrait(username)\
.absolute_url()
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
@@ -520,20 +519,18 @@ class CommentsViewlet(ViewletBase):
return settings.show_commenter_image
def is_anonymous(self):
portal_membership = getToolByName(self.context,
'portal_membership',
None)
portal_membership = getToolByName(self.context, "portal_membership", None)
return portal_membership.isAnonymousUser()
def login_action(self):
return '{0}/login_form?came_from={1}'.format(
return "{0}/login_form?came_from={1}".format(
self.navigation_root_url,
quote(self.request.get('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')
util = getToolByName(self.context, "translation_service")
zope_time = DateTime(time.isoformat())
return util.toLocalizedTime(zope_time, long_format=True)