Merge pull request #195 from plone/plone-base-overhaul

Plone base overhaul
This commit is contained in:
Jens W. Klein 2022-05-03 15:10:38 +02:00 committed by GitHub
commit fe10c7448f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2374 additions and 2395 deletions

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# plone.app.discussion documentation build configuration file, created by
# sphinx-quickstart on Thu Mar 18 10:17:15 2010.
@ -24,184 +23,190 @@ import sys
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.intersphinx',
'sphinx.ext.todo',
'sphinx.ext.coverage',
'repoze.sphinx.autointerface'
]
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
"sphinx.ext.todo",
"sphinx.ext.coverage",
"repoze.sphinx.autointerface",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.txt'
source_suffix = ".txt"
# The encoding of source files.
#source_encoding = 'utf-8'
# source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'plone.app.discussion'
copyright = u'2010, Timo Stollenwerk - Plone Foundation'
project = "plone.app.discussion"
copyright = "2010, Timo Stollenwerk - Plone Foundation"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '2.0'
version = "2.0"
# The full version, including alpha/beta/rc tags.
release = '2.0'
release = "2.0"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = []
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
#html_theme = 'plone'
# html_theme = 'plone'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_themes']
html_theme_path = ["_themes"]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'ploneappdiscussiondoc'
htmlhelp_basename = "ploneappdiscussiondoc"
# -- Options for LaTeX output --------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'ploneappdiscussion.tex', u'plone.app.discussion Documentation',
u'Timo Stollenwerk', 'manual'),
(
"index",
"ploneappdiscussion.tex",
"plone.app.discussion Documentation",
"Timo Stollenwerk",
"manual",
),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
# latex_use_modindex = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
intersphinx_mapping = {"http://docs.python.org/": None}

View File

@ -58,13 +58,13 @@ comment form with the "website" field::
# Interface to define the fields we want to add to the comment form.
class ICommentExtenderFields(Interface):
website = schema.TextLine(title=u"Website", required=False)
website = schema.TextLine(title="Website", required=False)
# Persistent class that implements the ICommentExtenderFields interface
@adapter(Comment)
class CommentExtenderFields(Persistent):
interface.implements(ICommentExtenderFields)
website = u""
website = ""
# CommentExtenderFields factory
CommentExtenderFactory = factory(CommentExtenderFields)

View File

@ -20,8 +20,8 @@ configure.zcml::
Define an interface IMyDexterityContentType groked schema, I added::
allowDiscussion = schema.Bool(
title=_(u"Allow Users to Comment"),
description=_(u"Allow users to commemt on you. Comments
title=_("Allow Users to Comment"),
description=_("Allow users to comment on you. Comments
are shown at the end of each page"),
required=True,
default=True,

2
news/195.breaking Normal file
View File

@ -0,0 +1,2 @@
Code style black & isort. Remove six usage. Use plone.base and move annotation key over to here.
[jensens]

View File

@ -1,2 +1 @@
# -*- coding: utf-8 -*-
__import__('pkg_resources').declare_namespace(__name__)
__import__("pkg_resources").declare_namespace(__name__)

View File

@ -1,2 +1 @@
# -*- coding: utf-8 -*-
__import__('pkg_resources').declare_namespace(__name__)
__import__("pkg_resources").declare_namespace(__name__)

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from zope.i18nmessageid import MessageFactory
_ = MessageFactory('plone')
_ = MessageFactory("plone")

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# Captcha validator, see captcha.txt for design notes.
from persistent import Persistent
from plone.app.discussion.browser.comments import CommentForm
@ -21,9 +20,9 @@ from zope.publisher.interfaces.browser import IDefaultBrowserLayer
@adapter(Comment)
@interface.implementer(ICaptcha)
class Captcha(Persistent):
"""Captcha input field.
"""
captcha = u''
"""Captcha input field."""
captcha = ""
Captcha = factory(Captcha)
@ -47,22 +46,24 @@ class CaptchaExtender(extensible.FormExtender):
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
self.captcha = settings.captcha
portal_membership = getToolByName(self.context, 'portal_membership')
portal_membership = getToolByName(self.context, "portal_membership")
self.isAnon = portal_membership.isAnonymousUser()
def update(self):
if self.captcha != 'disabled' and self.isAnon:
if self.captcha != "disabled" and self.isAnon:
# Add a captcha field if captcha is enabled in the registry
self.add(ICaptcha, prefix='')
if self.captcha == 'captcha':
self.add(ICaptcha, prefix="")
if self.captcha == "captcha":
from plone.formwidget.captcha import CaptchaFieldWidget
self.form.fields['captcha'].widgetFactory = CaptchaFieldWidget
elif self.captcha == 'recaptcha':
self.form.fields["captcha"].widgetFactory = CaptchaFieldWidget
elif self.captcha == "recaptcha":
from plone.formwidget.recaptcha import ReCaptchaFieldWidget
self.form.fields['captcha'].widgetFactory = \
ReCaptchaFieldWidget
elif self.captcha == 'norobots':
self.form.fields["captcha"].widgetFactory = ReCaptchaFieldWidget
elif self.captcha == "norobots":
from collective.z3cform.norobots import NorobotsFieldWidget
self.form.fields['captcha'].widgetFactory = NorobotsFieldWidget
self.form.fields["captcha"].widgetFactory = NorobotsFieldWidget
else:
self.form.fields['captcha'].mode = interfaces.HIDDEN_MODE
self.form.fields["captcha"].mode = interfaces.HIDDEN_MODE

View File

@ -1,4 +1,3 @@
# coding: utf-8
from .comments import CommentForm
from AccessControl import getSecurityManager
from Acquisition import aq_inner
@ -14,7 +13,6 @@ from zope.component import getMultiAdapter
from zope.component import getUtility
from zope.event import notify
from zope.lifecycleevent import ObjectModifiedEvent
from .comments import CommentForm
class View(BrowserView):
@ -38,8 +36,7 @@ class View(BrowserView):
context = aq_inner(self.context)
registry = getUtility(IRegistry)
view_action_types = registry.get(
'plone.types_use_view_action_in_listings', [])
view_action_types = registry.get("plone.types_use_view_action_in_listings", [])
obj = aq_parent(aq_parent(context))
url = obj.absolute_url()
@ -50,33 +47,34 @@ class View(BrowserView):
will redirect right to the binary object, bypassing comments.
"""
if obj.portal_type in view_action_types:
url = '{0}/view'.format(url)
url = f"{url}/view"
self.request.response.redirect('{0}#{1}'.format(url, context.id))
self.request.response.redirect(f"{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')
id = "edit-comment-form"
label = _("edit_comment_form_title", default="Edit comment")
def updateWidgets(self):
super(EditCommentForm, self).updateWidgets()
self.widgets['text'].value = self.context.text
super().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'
self.widgets["text"].id = "overlay-comment-text"
def _redirect(self, target=''):
def _redirect(self, target=""):
if not target:
portal_state = getMultiAdapter((self.context, self.request),
name=u'plone_portal_state')
portal_state = getMultiAdapter(
(self.context, self.request), name="plone_portal_state"
)
target = portal_state.portal_url()
self.request.response.redirect(target)
@button.buttonAndHandler(_(u'label_save',
default=u'Save'), name='comment')
@button.buttonAndHandler(_("label_save", default="Save"), name="comment")
def handleComment(self, action):
# Validate form
@ -85,32 +83,28 @@ class EditCommentForm(CommentForm):
return
# Check permissions
can_edit = getSecurityManager().checkPermission(
'Edit comments',
self.context)
mtool = getToolByName(self.context, 'portal_membership')
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']
self.context.text = data["text"]
# Notify that the object has been modified
notify(ObjectModifiedEvent(self.context))
# Redirect to comment
IStatusMessage(self.request).add(_(u'comment_edit_notification',
default='Comment was edited'),
type='info')
return self._redirect(
target=self.action.replace('@@edit-comment', '@@view'))
IStatusMessage(self.request).add(
_("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')
@button.buttonAndHandler(_("cancel_form_button", default="Cancel"), name="cancel")
def handle_cancel(self, action):
IStatusMessage(self.request).add(
_(u'comment_edit_cancel_notification',
default=u'Edit comment cancelled'),
type='info')
_("comment_edit_cancel_notification", default="Edit comment cancelled"),
type="info",
)
return self._redirect(target=self.context.absolute_url())

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from AccessControl import getSecurityManager
from AccessControl import Unauthorized
from Acquisition import aq_inner
@ -12,15 +11,15 @@ from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.interfaces import IReplies
from plone.app.layout.viewlets.common import ViewletBase
from plone.base.utils import safe_text
from plone.registry.interfaces import IRegistry
from plone.z3cform import z2
from plone.z3cform.fieldsets import extensible
from plone.z3cform.interfaces import IWrappedForm
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.utils import safe_unicode
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.statusmessages.interfaces import IStatusMessage
from six.moves.urllib.parse import quote
from urllib.parse import quote
from z3c.form import button
from z3c.form import field
from z3c.form import form
@ -35,28 +34,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.',
"comment_description_plain_text",
default="You can add a comment by filling out the form below. "
"Plain text formatting.",
)
COMMENT_DESCRIPTION_MARKDOWN = _(
u'comment_description_markdown',
default=u'You can add a comment by filling out the form below. '
u'Plain text formatting. You can use the Markdown syntax for '
u'links and images.',
"comment_description_markdown",
default="You can add a comment by filling out the form below. "
"Plain text formatting. You can use the Markdown syntax for "
"links and images.",
)
COMMENT_DESCRIPTION_INTELLIGENT_TEXT = _(
u'comment_description_intelligent_text',
default=u'You can add a comment by filling out the form below. '
u'Plain text formatting. Web and email addresses are '
u'transformed into clickable links.',
"comment_description_intelligent_text",
default="You can add a comment by filling out the form below. "
"Plain text formatting. Web and email addresses are "
"transformed into clickable links.",
)
COMMENT_DESCRIPTION_MODERATION_ENABLED = _(
u'comment_description_moderation_enabled',
default=u'Comments are moderated.',
"comment_description_moderation_enabled",
default="Comments are moderated.",
)
@ -64,30 +63,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 = _("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
super().updateFields()
self.fields["user_notification"].widgetFactory = SingleCheckBoxFieldWidget
def updateWidgets(self):
super(CommentForm, self).updateWidgets()
super().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 = _("")
# 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 +97,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,56 +119,55 @@ 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')
super().updateActions()
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 = ""
# 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_text(data["author_name"])
if "author_email" in data:
author_email = safe_text(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_text(member.getProperty("email"))
fullname = member.getProperty("fullname")
if not fullname or fullname == "":
fullname = member.getUserName()
fullname = safe_unicode(fullname)
fullname = safe_text(fullname)
author_name = fullname
email = safe_unicode(email)
email = safe_text(email)
# XXX: according to IComment interface author_email must not be # noqa T000
# set for logged in users, cite:
# 'for anonymous comments only, set to None for logged in comments'
@ -179,7 +178,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 +199,42 @@ 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"'reply to item' permission.",
"Anonymous user tries to post a comment, but anonymous "
"commenting is disabled. Or user does not have the "
"'reply to item' permission.",
)
return comment
@button.buttonAndHandler(_(u'add_comment_button', default=u'Comment'),
name='comment')
@button.buttonAndHandler(_("add_comment_button", default="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 +245,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"] = ""
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 +276,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(_("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 +303,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()
super().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 +322,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 +353,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 +361,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 +441,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 +480,16 @@ class CommentsViewlet(ViewletBase):
if username is None:
return None
else:
return '{0}/author/{1}'.format(self.context.portal_url(), username)
return f"{self.context.portal_url()}/author/{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 +516,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 "{}/login_form?came_from={}".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)

View File

@ -48,7 +48,7 @@
<!-- Moderate comments enabled view -->
<browser:page
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
for="plone.base.interfaces.IPloneSiteRoot"
name="moderate-comments-enabled"
layer="..interfaces.IDiscussionLayer"
class=".moderation.ModerateCommentsEnabled"
@ -141,14 +141,14 @@
<!-- Control panel -->
<browser:page
name="discussion-controlpanel"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
for="plone.base.interfaces.IPloneSiteRoot"
class=".controlpanel.DiscussionSettingsControlPanel"
permission="cmf.ManagePortal"
/>
<!-- Deprecated controlpanel url -->
<browser:page
name="discussion-settings"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
for="plone.base.interfaces.IPloneSiteRoot"
class=".controlpanel.DiscussionSettingsControlPanel"
permission="cmf.ManagePortal"
/>

View File

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.interfaces import _
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.upgrades import update_registry
from ..interfaces import _
from ..interfaces import IDiscussionSettings
from ..upgrades import update_registry
from plone.app.registry.browser import controlpanel
from plone.base.interfaces.controlpanel import IConfigurationChangedEvent
from plone.base.interfaces.controlpanel import IMailSchema
from plone.registry.interfaces import IRecordModifiedEvent
from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.interfaces.controlpanel import IMailSchema
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.statusmessages.interfaces import IStatusMessage
from z3c.form import button
@ -16,87 +16,76 @@ from zope.component import getUtility
from zope.component import queryUtility
from zope.component.hooks import getSite
# try/except was added because Configuration Changed Event was moved inside the
# controlpanel file in the PR #2495 on Products.CMFPlone
try:
from Products.CMFPlone.interfaces.controlpanel import IConfigurationChangedEvent # noqa: E501
except ImportError:
from Products.CMFPlone.interfaces import IConfigurationChangedEvent
class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
"""Discussion settings form.
"""
"""Discussion settings form."""
schema = IDiscussionSettings
id = 'DiscussionSettingsEditForm'
label = _(u'Discussion settings')
id = "DiscussionSettingsEditForm"
label = _("Discussion settings")
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".',
"help_discussion_settings_editform",
default="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".',
)
def updateFields(self):
super(DiscussionSettingsEditForm, self).updateFields()
self.fields['globally_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget
self.fields['moderation_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget
self.fields['edit_comment_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget
self.fields['delete_own_comment_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget
self.fields['anonymous_comments'].widgetFactory = \
SingleCheckBoxFieldWidget
self.fields['show_commenter_image'].widgetFactory = \
SingleCheckBoxFieldWidget
self.fields['moderator_notification_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget
self.fields['user_notification_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget
super().updateFields()
self.fields["globally_enabled"].widgetFactory = SingleCheckBoxFieldWidget
self.fields["moderation_enabled"].widgetFactory = SingleCheckBoxFieldWidget
self.fields["edit_comment_enabled"].widgetFactory = SingleCheckBoxFieldWidget
self.fields[
"delete_own_comment_enabled"
].widgetFactory = SingleCheckBoxFieldWidget
self.fields["anonymous_comments"].widgetFactory = SingleCheckBoxFieldWidget
self.fields["show_commenter_image"].widgetFactory = SingleCheckBoxFieldWidget
self.fields[
"moderator_notification_enabled"
].widgetFactory = SingleCheckBoxFieldWidget
self.fields[
"user_notification_enabled"
].widgetFactory = SingleCheckBoxFieldWidget
def updateWidgets(self):
try:
super(DiscussionSettingsEditForm, self).updateWidgets()
super().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')
self.widgets['moderator_notification_enabled'].label = _(
u'Moderator Email Notification',
super().updateWidgets()
self.widgets["globally_enabled"].label = _("Enable Comments")
self.widgets["anonymous_comments"].label = _("Anonymous Comments")
self.widgets["show_commenter_image"].label = _("Commenter Image")
self.widgets["moderator_notification_enabled"].label = _(
"Moderator Email Notification",
)
self.widgets['user_notification_enabled'].label = _(
u'User Email Notification',
self.widgets["user_notification_enabled"].label = _(
"User Email Notification",
)
@button.buttonAndHandler(_('Save'), name=None)
@button.buttonAndHandler(_("Save"), name=None)
def handleSave(self, action):
data, errors = self.extractData()
if errors:
self.status = self.formErrorsMessage
return
self.applyChanges(data)
IStatusMessage(self.request).addStatusMessage(_(u'Changes saved'),
'info')
self.context.REQUEST.RESPONSE.redirect('@@discussion-controlpanel')
IStatusMessage(self.request).addStatusMessage(_("Changes saved"), "info")
self.context.REQUEST.RESPONSE.redirect("@@discussion-controlpanel")
@button.buttonAndHandler(_('Cancel'), name='cancel')
@button.buttonAndHandler(_("Cancel"), name="cancel")
def handleCancel(self, action):
IStatusMessage(self.request).addStatusMessage(_(u'Edit cancelled'),
'info')
IStatusMessage(self.request).addStatusMessage(_("Edit cancelled"), "info")
self.request.response.redirect(
'{0}/{1}'.format(
"{}/{}".format(
self.context.absolute_url(),
self.control_panel_view,
),
@ -104,15 +93,15 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
"""Discussion settings control panel.
"""
"""Discussion settings control panel."""
form = DiscussionSettingsEditForm
index = ViewPageTemplateFile('controlpanel.pt')
index = ViewPageTemplateFile("controlpanel.pt")
def __call__(self):
self.mailhost_warning()
self.custom_comment_workflow_warning()
return super(DiscussionSettingsControlPanel, self).__call__()
return super().__call__()
@property
def site_url(self):
@ -123,43 +112,44 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
def settings(self):
"""Compose a string that contains all registry settings that are
needed for the discussion control panel.
needed for the discussion control panel.
"""
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
wftool = getToolByName(self.context, 'portal_workflow', None)
workflow_chain = wftool.getChainForPortalType('Discussion Item')
wftool = getToolByName(self.context, "portal_workflow", None)
workflow_chain = wftool.getChainForPortalType("Discussion Item")
output = []
# Globally enabled
if settings.globally_enabled:
output.append('globally_enabled')
output.append("globally_enabled")
# Comment moderation
one_state_worklow_disabled = \
'comment_one_state_workflow' not in workflow_chain
comment_review_workflow_disabled = \
'comment_review_workflow' not in workflow_chain
one_state_worklow_disabled = "comment_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')
output.append("moderation_custom")
elif settings.moderation_enabled:
output.append('moderation_enabled')
output.append("moderation_enabled")
if settings.edit_comment_enabled:
output.append('edit_comment_enabled')
output.append("edit_comment_enabled")
if settings.delete_own_comment_enabled:
output.append('delete_own_comment_enabled')
output.append("delete_own_comment_enabled")
# Anonymous comments
if settings.anonymous_comments:
output.append('anonymous_comments')
output.append("anonymous_comments")
# Invalid mail setting
ctrlOverview = getMultiAdapter((self.context, self.request),
name='overview-controlpanel')
ctrlOverview = getMultiAdapter(
(self.context, self.request), name="overview-controlpanel"
)
if ctrlOverview.mailhost_warning():
output.append('invalid_mail_setup')
output.append("invalid_mail_setup")
# Workflow
if workflow_chain:
@ -167,69 +157,71 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
output.append(discussion_workflow)
# Merge all settings into one string
return ' '.join(output)
return " ".join(output)
def mailhost_warning(self):
"""Returns true if mailhost is not configured properly.
"""
"""Returns true if mailhost is not configured properly."""
# Copied from Products.CMFPlone/controlpanel/browser/overview.py
registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mailhost = mail_settings.smtp_host
email = mail_settings.email_from_address
if mailhost and email:
pass
else:
message = _(u'discussion_text_no_mailhost_configured',
default=u'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 E-Mail Settings to fix this.') # noqa: E501
IStatusMessage(self.request).addStatusMessage(message, 'warning')
message = _(
"discussion_text_no_mailhost_configured",
default="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 E-Mail Settings to fix this.",
) # noqa: E501
IStatusMessage(self.request).addStatusMessage(message, "warning")
def custom_comment_workflow_warning(self):
"""Return True if a custom comment workflow is enabled."""
wftool = getToolByName(self.context, 'portal_workflow', None)
workflow_chain = wftool.getChainForPortalType('Discussion Item')
one_state_workflow_enabled = \
'comment_one_state_workflow' in workflow_chain
comment_review_workflow_enabled = \
'comment_review_workflow' in workflow_chain
wftool = getToolByName(self.context, "portal_workflow", None)
workflow_chain = wftool.getChainForPortalType("Discussion Item")
one_state_workflow_enabled = "comment_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:
pass
else:
message = _(u'discussion_text_custom_comment_workflow',
default=u'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 \'Discussion Item\' workflows. Go to the Types control panel to choose a workflow for the \'Discussion Item\' type.') # noqa: E501
IStatusMessage(self.request).addStatusMessage(message, 'warning')
message = _(
"discussion_text_custom_comment_workflow",
default="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 'Discussion Item' workflows. Go to the Types control panel to choose a workflow for the 'Discussion Item' type.",
) # noqa: E501
IStatusMessage(self.request).addStatusMessage(message, "warning")
def notify_configuration_changed(event):
"""Event subscriber that is called every time the configuration changed.
"""
"""Event subscriber that is called every time the configuration changed."""
portal = getSite()
wftool = getToolByName(portal, 'portal_workflow', None)
wftool = getToolByName(portal, "portal_workflow", None)
if IRecordModifiedEvent.providedBy(event):
# Discussion control panel setting changed
if event.record.fieldName == 'moderation_enabled':
if event.record.fieldName == "moderation_enabled":
# Moderation enabled has changed
if event.record.value is True:
# Enable moderation workflow
wftool.setChainForPortalTypes(('Discussion Item',),
'comment_review_workflow')
wftool.setChainForPortalTypes(
("Discussion Item",), "comment_review_workflow"
)
else:
# Disable moderation workflow
wftool.setChainForPortalTypes(('Discussion Item',),
'comment_one_state_workflow')
wftool.setChainForPortalTypes(
("Discussion Item",), "comment_one_state_workflow"
)
if IConfigurationChangedEvent.providedBy(event):
# Types control panel setting changed
if 'workflow' in event.data:
if "workflow" in event.data:
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
workflow_chain = wftool.getChainForPortalType('Discussion Item')
workflow_chain = wftool.getChainForPortalType("Discussion Item")
if workflow_chain:
workflow = workflow_chain[0]
if workflow == 'comment_one_state_workflow':
if workflow == "comment_one_state_workflow":
settings.moderation_enabled = False
elif workflow == 'comment_review_workflow':
elif workflow == "comment_review_workflow":
settings.moderation_enabled = True
else:
# Custom workflow

View File

@ -1,19 +1,19 @@
# -*- coding: utf-8 -*-
from ..interfaces import IDiscussionSettings
from Acquisition import aq_base
from Acquisition import aq_chain
from Acquisition import aq_inner
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.base.interfaces import INonStructuralFolder
from plone.base.interfaces import IPloneSiteRoot
from plone.base.utils import safe_hasattr
from plone.registry.interfaces import IRegistry
from Products.CMFCore.interfaces import IFolderish
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.interfaces import INonStructuralFolder
from Products.CMFPlone.interfaces import IPloneSiteRoot
from Products.CMFPlone.utils import safe_hasattr
from zope.component import queryUtility
try:
from plone.dexterity.interfaces import IDexterityContent
DEXTERITY_INSTALLED = True
except ImportError:
DEXTERITY_INSTALLED = False
@ -26,15 +26,14 @@ def traverse_parents(context):
if not IPloneSiteRoot.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 obj_is_folderish and obj_is_stuctural:
flag = getattr(obj, "allow_discussion", None)
if flag is not None:
return flag
return None
class ConversationView(object):
class ConversationView:
def enabled(self):
if DEXTERITY_INSTALLED and IDexterityContent.providedBy(self.context):
return self._enabled_for_dexterity_types()
@ -42,7 +41,7 @@ class ConversationView(object):
return self._enabled_for_archetypes()
def _enabled_for_archetypes(self):
""" Returns True if discussion is enabled for this conversation.
"""Returns True if discussion is enabled for this conversation.
This method checks five different settings in order to figure out if
discussion is enabled on a specific content object:
@ -82,7 +81,7 @@ class ConversationView(object):
return False
# If discussion is disabled for the object, bail out
obj_flag = getattr(aq_base(context), 'allow_discussion', None)
obj_flag = getattr(aq_base(context), "allow_discussion", None)
if obj_flag is False:
return False
@ -91,16 +90,16 @@ class ConversationView(object):
folder_allow_discussion = traverse_parents(context)
if folder_allow_discussion:
if not getattr(self, 'allow_discussion', None):
if not getattr(self, "allow_discussion", None):
return True
else:
if obj_flag:
return True
# Check if discussion is allowed on the content type
portal_types = getToolByName(self, 'portal_types')
portal_types = getToolByName(self, "portal_types")
document_fti = getattr(portal_types, context.portal_type)
if not document_fti.getProperty('allow_discussion'):
if not document_fti.getProperty("allow_discussion"):
# If discussion is not allowed on the content type,
# check if 'allow discussion' is overridden on the content object.
if not obj_flag:
@ -109,7 +108,7 @@ class ConversationView(object):
return True
def _enabled_for_dexterity_types(self):
""" Returns True if discussion is enabled for this conversation.
"""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:
@ -134,11 +133,11 @@ class ConversationView(object):
return False
# Check if discussion is allowed on the content object
if safe_hasattr(context, 'allow_discussion'):
if safe_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')
portal_types = getToolByName(self, "portal_types")
document_fti = getattr(portal_types, context.portal_type)
return document_fti.getProperty('allow_discussion')
return document_fti.getProperty("allow_discussion")

View File

@ -1,11 +1,10 @@
# coding: utf-8
from AccessControl import getSecurityManager
from AccessControl import Unauthorized
from Acquisition import aq_inner
from Acquisition import aq_parent
from plone.app.discussion.events import CommentDeletedEvent
from plone.app.discussion.events import CommentPublishedEvent
from plone.app.discussion.events import CommentTransitionEvent
from plone.app.discussion.events import CommentDeletedEvent
from plone.app.discussion.interfaces import _
from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IReplies
@ -18,21 +17,20 @@ from zope.event import notify
# Translations for generated values in buttons
# States
_('comment_pending', default='pending')
_("comment_pending", default="pending")
# _('comment_approved', default='published')
_('comment_published', default='published')
_('comment_rejected', default='rejected')
_('comment_spam', default='marked as spam')
_("comment_published", default="published")
_("comment_rejected", default="rejected")
_("comment_spam", default="marked as spam")
# Transitions
_('Recall')
_('Approve')
_('Reject')
_('Spam')
_("Recall")
_("Approve")
_("Reject")
_("Spam")
PMF = _
class TranslationHelper(BrowserView):
def translate(self, text=""):
return _(text)
@ -44,22 +42,21 @@ class TranslationHelper(BrowserView):
class View(BrowserView):
"""Show comment moderation view."""
template = ViewPageTemplateFile('moderation.pt')
template = ViewPageTemplateFile("moderation.pt")
try:
template.id = '@@moderate-comments'
template.id = "@@moderate-comments"
except AttributeError:
# id is not writeable in Zope 2.12
pass
def __init__(self, context, request):
super(View, self).__init__(context, request)
self.workflowTool = getToolByName(self.context, 'portal_workflow')
super().__init__(context, request)
self.workflowTool = getToolByName(self.context, "portal_workflow")
self.transitions = []
def __call__(self):
self.request.set('disable_border', True)
self.request.set('review_state',
self.request.get('review_state', 'pending'))
self.request.set("disable_border", True)
self.request.set("review_state", self.request.get("review_state", "pending"))
return self.template()
def comments(self):
@ -67,15 +64,19 @@ class View(BrowserView):
review_state is string or list of strings.
"""
catalog = getToolByName(self.context, 'portal_catalog')
if self.request.review_state == 'all':
return catalog(object_provides=IComment.__identifier__,
sort_on='created',
sort_order='reverse')
return catalog(object_provides=IComment.__identifier__,
review_state=self.request.review_state,
sort_on='created',
sort_order='reverse')
catalog = getToolByName(self.context, "portal_catalog")
if self.request.review_state == "all":
return catalog(
object_provides=IComment.__identifier__,
sort_on="created",
sort_order="reverse",
)
return catalog(
object_provides=IComment.__identifier__,
review_state=self.request.review_state,
sort_on="created",
sort_order="reverse",
)
def moderation_enabled(self):
"""Return true if a review workflow is enabled on 'Discussion Item'
@ -84,11 +85,10 @@ class View(BrowserView):
A 'review workflow' is characterized by implementing a 'pending'
workflow state.
"""
workflows = self.workflowTool.getChainForPortalType(
'Discussion Item')
workflows = self.workflowTool.getChainForPortalType("Discussion Item")
if workflows:
comment_workflow = self.workflowTool[workflows[0]]
if 'pending' in comment_workflow.states:
if "pending" in comment_workflow.states:
return True
return False
@ -100,11 +100,10 @@ class View(BrowserView):
A 'review multipe state workflow' is characterized by implementing
a 'rejected' workflow state and a 'spam' workflow state.
"""
workflows = self.workflowTool.getChainForPortalType(
'Discussion Item')
workflows = self.workflowTool.getChainForPortalType("Discussion Item")
if workflows:
comment_workflow = self.workflowTool[workflows[0]]
if 'spam' in comment_workflow.states:
if "spam" in comment_workflow.states:
return True
return False
@ -125,27 +124,26 @@ class View(BrowserView):
"""
if obj:
transitions = [
a for a in self.workflowTool.listActionInfos(object=obj)
if a['category'] == 'workflow' and a['allowed']
]
a
for a in self.workflowTool.listActionInfos(object=obj)
if a["category"] == "workflow" and a["allowed"]
]
return transitions
class ModerateCommentsEnabled(BrowserView):
def __call__(self):
"""Returns true if a 'review workflow' is enabled on 'Discussion Item'
content type. A 'review workflow' is characterized by implementing
a 'pending' workflow state.
content type. A 'review workflow' is characterized by implementing
a 'pending' workflow state.
"""
context = aq_inner(self.context)
workflowTool = getToolByName(context, 'portal_workflow', None)
comment_workflow = workflowTool.getChainForPortalType(
'Discussion Item')
workflowTool = getToolByName(context, "portal_workflow", None)
comment_workflow = workflowTool.getChainForPortalType("Discussion Item")
if comment_workflow:
comment_workflow = comment_workflow[0]
comment_workflow = workflowTool[comment_workflow]
if 'pending' in comment_workflow.states:
if "pending" in comment_workflow.states:
return True
return False
@ -184,13 +182,15 @@ class DeleteComment(BrowserView):
content_object.reindexObject()
notify(CommentDeletedEvent(self.context, comment))
IStatusMessage(self.context.REQUEST).addStatusMessage(
_('Comment deleted.'),
type='info')
_("Comment deleted."), type="info"
)
came_from = self.context.REQUEST.HTTP_REFERER
# 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 or
not getToolByName(
content_object, 'portal_url').isURLInPortal(came_from)):
if (
len(came_from) == 0
or "came_from=" in came_from
or not getToolByName(content_object, "portal_url").isURLInPortal(came_from)
):
came_from = content_object.absolute_url()
return self.context.REQUEST.RESPONSE.redirect(came_from)
@ -198,8 +198,7 @@ class DeleteComment(BrowserView):
"""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))
class DeleteOwnComment(DeleteComment):
@ -213,26 +212,23 @@ class DeleteOwnComment(DeleteComment):
"""
def could_delete(self, comment=None):
"""Returns true if the comment could be deleted if it had no replies.
"""
"""Returns true if the comment could be deleted if it had no replies."""
sm = getSecurityManager()
comment = comment or aq_inner(self.context)
userid = sm.getUser().getId()
return (
sm.checkPermission('Delete own comments', comment) and
'Owner' in comment.get_local_roles_for_userid(userid)
)
return sm.checkPermission(
"Delete own comments", comment
) and "Owner" in comment.get_local_roles_for_userid(userid)
def can_delete(self, comment=None):
comment = comment or self.context
return (
len(IReplies(aq_inner(comment))) == 0 and
self.could_delete(comment=comment)
return len(IReplies(aq_inner(comment))) == 0 and self.could_delete(
comment=comment
)
def __call__(self):
if self.can_delete():
super(DeleteOwnComment, self).__call__()
super().__call__()
else:
raise Unauthorized("You're not allowed to delete this comment.")
@ -262,33 +258,37 @@ class CommentTransition(BrowserView):
"""Call CommentTransition."""
comment = aq_inner(self.context)
content_object = aq_parent(aq_parent(comment))
workflow_action = self.request.form.get('workflow_action', 'publish')
workflowTool = getToolByName(self.context, 'portal_workflow')
workflow_action = self.request.form.get("workflow_action", "publish")
workflowTool = getToolByName(self.context, "portal_workflow")
workflowTool.doActionFor(comment, workflow_action)
comment.reindexObject()
content_object.reindexObject(idxs=['total_comments'])
content_object.reindexObject(idxs=["total_comments"])
notify(CommentPublishedEvent(self.context, comment))
# for complexer workflows:
notify(CommentTransitionEvent(self.context, comment))
comment_state_translated = ''
comment_state_translated = ""
if workflowTool.getWorkflowsFor(comment):
review_state_new = workflowTool.getInfoFor(ob=comment, name='review_state')
review_state_new = workflowTool.getInfoFor(ob=comment, name="review_state")
helper = self.context.restrictedTraverse("translationhelper")
comment_state_translated = helper.translate_comment_review_state(review_state_new)
comment_state_translated = helper.translate_comment_review_state(
review_state_new
)
msgid = _(
"comment_transmitted",
default='Comment ${comment_state_translated}.',
mapping={"comment_state_translated": comment_state_translated})
default="Comment ${comment_state_translated}.",
mapping={"comment_state_translated": comment_state_translated},
)
translated = self.context.translate(msgid)
IStatusMessage(self.request).add(translated, type='info')
IStatusMessage(self.request).add(translated, type="info")
came_from = self.context.REQUEST.HTTP_REFERER
# 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
or not getToolByName(
content_object, 'portal_url').isURLInPortal(came_from)):
if (
len(came_from) == 0
or "came_from=" in came_from
or not getToolByName(content_object, "portal_url").isURLInPortal(came_from)
):
came_from = content_object.absolute_url()
return self.context.REQUEST.RESPONSE.redirect(came_from)
@ -317,19 +317,19 @@ class BulkActionsView(BrowserView):
"""
def __init__(self, context, request):
super(BulkActionsView, self).__init__(context, request)
self.workflowTool = getToolByName(context, 'portal_workflow')
super().__init__(context, request)
self.workflowTool = getToolByName(context, "portal_workflow")
def __call__(self):
"""Call BulkActionsView."""
if 'form.select.BulkAction' in self.request:
bulkaction = self.request.get('form.select.BulkAction')
self.paths = self.request.get('paths')
if "form.select.BulkAction" in self.request:
bulkaction = self.request.get("form.select.BulkAction")
self.paths = self.request.get("paths")
if self.paths:
if bulkaction == '-1':
if bulkaction == "-1":
# no bulk action was selected
pass
elif bulkaction == 'delete':
elif bulkaction == "delete":
self.delete()
else:
self.transmit(bulkaction)
@ -346,13 +346,14 @@ class BulkActionsView(BrowserView):
comment = context.restrictedTraverse(path)
content_object = aq_parent(aq_parent(comment))
allowed_transitions = [
transition['id'] for transition in self.workflowTool.listActionInfos(object=comment)
if transition['category'] == 'workflow' and transition['allowed']
]
transition["id"]
for transition in self.workflowTool.listActionInfos(object=comment)
if transition["category"] == "workflow" and transition["allowed"]
]
if action in allowed_transitions:
self.workflowTool.doActionFor(comment, action)
comment.reindexObject()
content_object.reindexObject(idxs=['total_comments'])
content_object.reindexObject(idxs=["total_comments"])
notify(CommentPublishedEvent(content_object, comment))
# for complexer workflows:
notify(CommentTransitionEvent(self.context, comment))
@ -370,5 +371,5 @@ class BulkActionsView(BrowserView):
conversation = aq_parent(comment)
content_object = aq_parent(conversation)
del conversation[comment.id]
content_object.reindexObject(idxs=['total_comments'])
content_object.reindexObject(idxs=["total_comments"])
notify(CommentDeletedEvent(content_object, comment))

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Implement the ++comments++ traversal namespace. This should return the
IDiscussion container for the context, from which traversal will continue
into an actual comment object.
@ -15,7 +14,7 @@ from zope.traversing.interfaces import TraversalError
@implementer(ITraversable)
@adapter(Interface, IBrowserRequest)
class ConversationNamespace(object):
class ConversationNamespace:
"""Allow traversal into a conversation via a ++conversation++name
namespace. The name is the name of an adapter from context to
IConversation. The special name 'default' will be taken as the default
@ -29,8 +28,8 @@ class ConversationNamespace(object):
def traverse(self, name, ignore):
if name == 'default':
name = u''
if name == "default":
name = ""
conversation = queryAdapter(self.context, IConversation, name=name)
if conversation is None:

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Captcha validator, see captcha.txt for design notes.
"""
from Acquisition import aq_inner
@ -39,16 +38,17 @@ class CaptchaValidator(validator.SimpleFieldValidator):
# We adapt the CaptchaValidator class to all form fields (IField)
def validate(self, value):
super(CaptchaValidator, self).validate(value)
super().validate(value)
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
if settings.captcha in ('captcha', 'recaptcha', 'norobots'):
captcha = getMultiAdapter((aq_inner(self.context), self.request),
name=settings.captcha)
if settings.captcha in ("captcha", "recaptcha", "norobots"):
captcha = getMultiAdapter(
(aq_inner(self.context), self.request), name=settings.captcha
)
if not captcha.verify(input=value):
if settings.captcha == 'norobots':
if settings.captcha == "norobots":
raise WrongNorobotsAnswer
else:
raise WrongCaptchaCode
@ -57,5 +57,4 @@ class CaptchaValidator(validator.SimpleFieldValidator):
# Register Captcha validator for the Captcha field in the ICaptcha Form
validator.WidgetValidatorDiscriminators(CaptchaValidator,
field=ICaptcha['captcha'])
validator.WidgetValidatorDiscriminators(CaptchaValidator, field=ICaptcha["captcha"])

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Catalog indexers, using plone.indexer. These will populate standard catalog
indexes with values based on the IComment interface.
@ -7,13 +6,12 @@ Also provide event handlers to actually catalog the comments.
from DateTime import DateTime
from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IConversation
from plone.base.utils import safe_text
from plone.indexer import indexer
from plone.uuid.interfaces import IUUID
from Products.CMFCore.interfaces import IContentish
from Products.CMFPlone.utils import safe_unicode
from Products.ZCatalog.interfaces import IZCatalog
import six
MAX_DESCRIPTION = 25
@ -24,7 +22,7 @@ MAX_DESCRIPTION = 25
def total_comments(object):
# Total number of comments on a conversation
# Indexers won't work on old discussion items
if object.meta_type != 'Discussion Item':
if object.meta_type != "Discussion Item":
try:
conversation = IConversation(object)
return conversation.total_comments()
@ -38,7 +36,7 @@ def total_comments(object):
def last_comment_date(object):
# Date of the latest comment on a conversation
# Indexers won't work on old discussion items
if object.meta_type != 'Discussion Item':
if object.meta_type != "Discussion Item":
try:
conversation = IConversation(object)
return conversation.last_comment_date
@ -52,7 +50,7 @@ def last_comment_date(object):
def commentators(object):
# List of commentators on a conversation
# Indexers won't work on old discussion items
if object.meta_type != 'Discussion Item':
if object.meta_type != "Discussion Item":
try:
conversation = IConversation(object)
return conversation.public_commentators
@ -61,6 +59,7 @@ def commentators(object):
# implemented an adapter for it
pass
# Comment Indexers
@ -73,26 +72,24 @@ def title(object):
def creator(object):
if not object.creator:
return
value = safe_unicode(object.creator)
if six.PY2:
return value.encode('utf8')
value = safe_text(object.creator)
return value
@indexer(IComment)
def description(object):
# Return the first 25 words of the comment text and append ' [...]'
text = ' '.join(
object.getText(targetMimetype='text/plain').split()[:MAX_DESCRIPTION],
text = " ".join(
object.getText(targetMimetype="text/plain").split()[:MAX_DESCRIPTION],
)
if len(object.getText().split()) > 25:
text += ' [...]'
text += " [...]"
return text
@indexer(IComment)
def searchable_text(object):
return object.getText(targetMimetype='text/plain')
return object.getText(targetMimetype="text/plain")
@indexer(IComment)
@ -106,42 +103,42 @@ def in_response_to(object):
def effective(object):
# the catalog index needs Zope DateTime instead of Python datetime
return DateTime(
object.creation_date.year,
object.creation_date.month,
object.creation_date.day,
object.creation_date.hour,
object.creation_date.minute,
object.creation_date.second,
'GMT',
)
object.creation_date.year,
object.creation_date.month,
object.creation_date.day,
object.creation_date.hour,
object.creation_date.minute,
object.creation_date.second,
"GMT",
)
@indexer(IComment)
def created(object):
# the catalog index needs Zope DateTime instead of Python datetime
return DateTime(
object.creation_date.year,
object.creation_date.month,
object.creation_date.day,
object.creation_date.hour,
object.creation_date.minute,
object.creation_date.second,
'GMT',
)
object.creation_date.year,
object.creation_date.month,
object.creation_date.day,
object.creation_date.hour,
object.creation_date.minute,
object.creation_date.second,
"GMT",
)
@indexer(IComment)
def modified(object):
# the catalog index needs Zope DateTime instead of Python datetime
return DateTime(
object.modification_date.year,
object.modification_date.month,
object.modification_date.day,
object.modification_date.hour,
object.modification_date.minute,
object.modification_date.second,
'GMT',
)
object.modification_date.year,
object.modification_date.month,
object.modification_date.day,
object.modification_date.hour,
object.modification_date.minute,
object.modification_date.second,
"GMT",
)
# Override the conversation indexers for comments

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""The default comment class and factory.
"""
from AccessControl import ClassSecurityInfo
@ -21,14 +20,14 @@ from plone.app.discussion.events import ReplyRemovedEvent
from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.base.interfaces.controlpanel import IMailSchema
from plone.base.utils import safe_text
from plone.registry.interfaces import IRegistry
from Products.CMFCore import permissions
from Products.CMFCore.CMFCatalogAware import CatalogAware
from Products.CMFCore.CMFCatalogAware import WorkflowAware
from Products.CMFCore.DynamicType import DynamicType
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.interfaces.controlpanel import IMailSchema
from Products.CMFPlone.utils import safe_unicode
from smtplib import SMTPException
from zope.annotation.interfaces import IAnnotatable
from zope.component import getUtility
@ -40,61 +39,69 @@ from zope.i18nmessageid import Message
from zope.interface import implementer
import logging
import six
COMMENT_TITLE = _(
u'comment_title',
default=u'${author_name} on ${content}',
)
"comment_title",
default="${author_name} on ${content}",
)
MAIL_NOTIFICATION_MESSAGE = _(
u'mail_notification_message',
default=u'A comment on "${title}" '
u'has been posted here: ${link}\n\n'
u'---\n'
u'${text}\n'
u'---\n',
)
"mail_notification_message",
default='A comment on "${title}" '
"has been posted here: ${link}\n\n"
"---\n"
"${text}\n"
"---\n",
)
MAIL_NOTIFICATION_MESSAGE_MODERATOR = _(
u'mail_notification_message_moderator2',
default=u'A comment on "${title}" '
u'has been posted by ${commentator}\n'
u'here: ${link}\n\n'
u'---\n\n'
u'${text}\n\n'
u'---\n\n'
u'Log in to moderate.\n\n',
)
"mail_notification_message_moderator2",
default='A comment on "${title}" '
"has been posted by ${commentator}\n"
"here: ${link}\n\n"
"---\n\n"
"${text}\n\n"
"---\n\n"
"Log in to moderate.\n\n",
)
logger = logging.getLogger('plone.app.discussion')
logger = logging.getLogger("plone.app.discussion")
@implementer(IComment)
class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
RoleManager, Owned, Implicit, Persistent):
class Comment(
CatalogAware,
WorkflowAware,
DynamicType,
Traversable,
RoleManager,
Owned,
Implicit,
Persistent,
):
"""A comment.
This object attempts to be as lightweight as possible. We implement a
number of standard methods instead of subclassing, to have total control
over what goes into the object.
"""
security = ClassSecurityInfo()
meta_type = portal_type = 'Discussion Item'
meta_type = portal_type = "Discussion Item"
# This needs to be kept in sync with types/Discussion_Item.xml title
fti_title = 'Comment'
fti_title = "Comment"
__parent__ = None
comment_id = None # long
in_reply_to = None # long
title = u''
title = ""
mime_type = None
text = u''
text = ""
creator = None
creation_date = None
@ -113,19 +120,22 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
def __init__(self):
self.creation_date = self.modification_date = datetime.utcnow()
self.mime_type = 'text/plain'
self.mime_type = "text/plain"
user = getSecurityManager().getUser()
if user and user.getId():
aclpath = [x for x in user.getPhysicalPath() if x]
self._owner = (aclpath, user.getId(),)
self._owner = (
aclpath,
user.getId(),
)
self.__ac_local_roles__ = {
user.getId(): ['Owner'],
user.getId(): ["Owner"],
}
@property
def __name__(self):
return self.comment_id and six.text_type(self.comment_id) or None
return self.comment_id and str(self.comment_id) or None
@property
def id(self):
@ -137,32 +147,30 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
def getText(self, targetMimetype=None):
"""The body text of a comment."""
transforms = getToolByName(self, 'portal_transforms')
transforms = getToolByName(self, "portal_transforms")
if targetMimetype is None:
targetMimetype = 'text/x-html-safe'
targetMimetype = "text/x-html-safe"
sourceMimetype = getattr(self, 'mime_type', None)
sourceMimetype = getattr(self, "mime_type", None)
if sourceMimetype is None:
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
sourceMimetype = settings.text_transform
text = self.text
if text is None:
return ''
if six.PY2 and isinstance(text, six.text_type):
text = text.encode('utf8')
return ""
transform = transforms.convertTo(
targetMimetype,
text,
context=self,
mimetype=sourceMimetype)
targetMimetype, text, context=self, mimetype=sourceMimetype
)
if transform:
return transform.getData()
else:
logger = logging.getLogger('plone.app.discussion')
msg = u'Transform "{0}" => "{1}" not available. Failed to ' \
u'transform comment "{2}".'
logger = logging.getLogger("plone.app.discussion")
msg = (
'Transform "{0}" => "{1}" not available. Failed to '
'transform comment "{2}".'
)
logger.error(
msg.format(
sourceMimetype,
@ -182,8 +190,8 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
author_name = translate(
Message(
_(
u'label_anonymous',
default=u'Anonymous',
"label_anonymous",
default="Anonymous",
),
),
)
@ -194,9 +202,14 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
# conversation, the parent of the conversation is the content object).
content = aq_base(self.__parent__.__parent__)
title = translate(
Message(COMMENT_TITLE,
mapping={'author_name': safe_unicode(author_name),
'content': safe_unicode(content.Title())}))
Message(
COMMENT_TITLE,
mapping={
"author_name": safe_text(author_name),
"content": safe_text(content.Title()),
},
)
)
return title
def Creator(self):
@ -224,25 +237,23 @@ CommentFactory = Factory(Comment)
def notify_workflow(obj, event):
"""Tell the workflow tool when a comment is added
"""
tool = getToolByName(obj, 'portal_workflow', None)
"""Tell the workflow tool when a comment is added"""
tool = getToolByName(obj, "portal_workflow", None)
if tool is not None:
tool.notifyCreated(obj)
def notify_content_object(obj, event):
"""Tell the content object when a comment is added
"""
"""Tell the content object when a comment is added"""
content_obj = aq_parent(aq_parent(obj))
content_obj.reindexObject(idxs=('total_comments',
'last_comment_date',
'commentators'))
content_obj.reindexObject(
idxs=("total_comments", "last_comment_date", "commentators")
)
def notify_content_object_deleted(obj, event):
"""Remove all comments of a content object when the content object has been
deleted.
deleted.
"""
if IAnnotatable.providedBy(obj):
conversation = IConversation(obj)
@ -251,40 +262,40 @@ def notify_content_object_deleted(obj, event):
def notify_comment_added(obj, event):
""" Notify custom discussion events when a comment is added or replied
"""
"""Notify custom discussion events when a comment is added or replied"""
conversation = aq_parent(obj)
context = aq_parent(conversation)
if getattr(obj, 'in_reply_to', None):
if getattr(obj, "in_reply_to", None):
return notify(ReplyAddedEvent(context, obj))
return notify(CommentAddedEvent(context, obj))
def notify_comment_modified(obj, event):
""" Notify custom discussion events when a comment, or a reply, is modified
"""
"""Notify custom discussion events when a comment, or a reply, is modified"""
conversation = aq_parent(obj)
context = aq_parent(conversation)
if getattr(obj, 'in_reply_to', None):
if getattr(obj, "in_reply_to", None):
return notify(ReplyModifiedEvent(context, obj))
return notify(CommentModifiedEvent(context, obj))
def notify_comment_removed(obj, event):
""" Notify custom discussion events when a comment or reply is removed
"""
"""Notify custom discussion events when a comment or reply is removed"""
conversation = aq_parent(obj)
context = aq_parent(conversation)
if getattr(obj, 'in_reply_to', None):
if getattr(obj, "in_reply_to", None):
return notify(ReplyRemovedEvent(context, obj))
return notify(CommentRemovedEvent(context, obj))
def notify_content_object_moved(obj, event):
"""Update all comments of a content object that has been moved.
"""
if event.oldParent is None or event.newParent is None \
or event.oldName is None or event.newName is None:
"""Update all comments of a content object that has been moved."""
if (
event.oldParent is None
or event.newParent is None
or event.oldName is None
or event.newName is None
):
return
# This method is also called for sublocations of moved objects. We
@ -293,21 +304,19 @@ def notify_content_object_moved(obj, event):
# in the object hierarchy. The object is already moved at this point. so
# obj.getPhysicalPath retruns the new path get the part of the path that
# was moved.
moved_path = obj.getPhysicalPath()[
len(event.newParent.getPhysicalPath()) + 1:
]
moved_path = obj.getPhysicalPath()[len(event.newParent.getPhysicalPath()) + 1 :]
# Remove comments at the old location from catalog
catalog = getToolByName(obj, 'portal_catalog')
old_path = '/'.join(
event.oldParent.getPhysicalPath() +
(event.oldName,) +
moved_path,
catalog = getToolByName(obj, "portal_catalog")
old_path = "/".join(
event.oldParent.getPhysicalPath() + (event.oldName,) + moved_path,
)
brains = catalog.searchResults(
dict(
path={"query": old_path},
portal_type="Discussion Item",
)
)
brains = catalog.searchResults(dict(
path={'query': old_path},
portal_type='Discussion Item',
))
for brain in brains:
catalog.uncatalog_object(brain.getPath())
# Reindex comment at the new location
@ -320,11 +329,11 @@ def notify_content_object_moved(obj, event):
def notify_user(obj, event):
"""Tell users when a comment has been added.
This method composes and sends emails to all users that have added a
comment to this conversation and enabled user notification.
This method composes and sends emails to all users that have added a
comment to this conversation and enabled user notification.
This requires the user_notification setting to be enabled in the
discussion control panel.
This requires the user_notification setting to be enabled in the
discussion control panel.
"""
# Check if user notification is enabled
@ -334,9 +343,9 @@ def notify_user(obj, event):
return
# Get informations that are necessary to send an email
mail_host = getToolByName(obj, 'MailHost')
mail_host = getToolByName(obj, "MailHost")
registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
sender = mail_settings.email_from_address
# Check if a sender address is available
@ -360,15 +369,14 @@ def notify_user(obj, event):
if not emails:
return
subject = translate(_(u'A comment has been posted.'),
context=obj.REQUEST)
subject = translate(_("A comment has been posted."), context=obj.REQUEST)
message = translate(
Message(
MAIL_NOTIFICATION_MESSAGE,
mapping={
'title': safe_unicode(content_object.title),
'link': content_object.absolute_url() + '/view#' + obj.id,
'text': obj.text,
"title": safe_text(content_object.title),
"link": content_object.absolute_url() + "/view#" + obj.id,
"text": obj.text,
},
),
context=obj.REQUEST,
@ -381,12 +389,11 @@ def notify_user(obj, event):
email,
sender,
subject,
charset='utf-8',
charset="utf-8",
)
except SMTPException:
logger.error(
'SMTP exception while trying to send an ' +
'email from %s to %s',
"SMTP exception while trying to send an " + "email from %s to %s",
sender,
email,
)
@ -395,15 +402,15 @@ def notify_user(obj, event):
def notify_moderator(obj, event):
"""Tell the moderator when a comment needs attention.
This method sends an email to the moderator if comment moderation a new
comment has been added that needs to be approved.
This method sends an email to the moderator if comment moderation a new
comment has been added that needs to be approved.
The moderator_notification setting has to be enabled in the discussion
control panel.
The moderator_notification setting has to be enabled in the discussion
control panel.
Configure the moderator e-mail address in the discussion control panel.
If no moderator is configured but moderator notifications are turned on,
the site admin email (from the mail control panel) will be used.
Configure the moderator e-mail address in the discussion control panel.
If no moderator is configured but moderator notifications are turned on,
the site admin email (from the mail control panel) will be used.
"""
# Check if moderator notification is enabled
registry = queryUtility(IRegistry)
@ -412,9 +419,9 @@ def notify_moderator(obj, event):
return
# Get informations that are necessary to send an email
mail_host = getToolByName(obj, 'MailHost')
mail_host = getToolByName(obj, "MailHost")
registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
sender = mail_settings.email_from_address
if settings.moderator_email:
@ -430,22 +437,23 @@ def notify_moderator(obj, event):
content_object = aq_parent(conversation)
# Compose email
subject = translate(_(u'A comment has been posted.'), context=obj.REQUEST)
subject = translate(_("A comment has been posted."), context=obj.REQUEST)
message = translate(
Message(
MAIL_NOTIFICATION_MESSAGE_MODERATOR,
mapping={
'title': safe_unicode(content_object.title),
'link': content_object.absolute_url() + '/view#' + obj.id,
'text': obj.text,
'commentator': obj.author_email or translate(
Message(
_(
u'label_anonymous',
default=u'Anonymous',
),
"title": safe_text(content_object.title),
"link": content_object.absolute_url() + "/view#" + obj.id,
"text": obj.text,
"commentator": obj.author_email
or translate(
Message(
_(
"label_anonymous",
default="Anonymous",
),
)
),
),
},
),
context=obj.REQUEST,
@ -453,12 +461,12 @@ def notify_moderator(obj, event):
# Send email
try:
mail_host.send(message, mto, sender, subject, charset='utf-8')
mail_host.send(message, mto, sender, subject, charset="utf-8")
except SMTPException as e:
logger.error(
'SMTP exception (%s) while trying to send an ' +
'email notification to the comment moderator ' +
'(from %s to %s, message: %s)',
"SMTP exception (%s) while trying to send an "
+ "email notification to the comment moderator "
+ "(from %s to %s, message: %s)",
e,
sender,
mto,

View File

@ -39,7 +39,7 @@
description="Commenting infrastructure for Plone"
directory="profiles/default"
provides="Products.GenericSetup.interfaces.EXTENSION"
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
for="plone.base.interfaces.IPloneSiteRoot"
/>
<!-- For upgrade steps see upgrades.zcml. -->

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
""" Content rules handlers
"""
from plone.app.discussion import _
@ -7,101 +6,94 @@ from plone.app.discussion import _
try:
from plone.stringinterp.adapters import BaseSubstitution
except ImportError:
class BaseSubstitution(object):
""" Fallback class if plone.stringinterp is not available
"""
class BaseSubstitution:
"""Fallback class if plone.stringinterp is not available"""
def __init__(self, context, **kwargs):
self.context = context
try:
from plone.app.contentrules.handlers import execute
except ImportError:
def execute(context, event):
return False
def execute_comment(event):
""" Execute comment content rules
"""
"""Execute comment content rules"""
execute(event.object, event)
class CommentSubstitution(BaseSubstitution):
""" Comment string substitution
"""
"""Comment string substitution"""
def __init__(self, context, **kwargs):
super(CommentSubstitution, self).__init__(context, **kwargs)
super().__init__(context, **kwargs)
@property
def event(self):
""" event that triggered the content rule
"""
return self.context.REQUEST.get('event')
"""event that triggered the content rule"""
return self.context.REQUEST.get("event")
@property
def comment(self):
""" Get changed inline comment
"""
"""Get changed inline comment"""
return self.event.comment
class Id(CommentSubstitution):
""" Comment id string substitution
"""
category = _(u'Comments')
description = _(u'Comment id')
"""Comment id string substitution"""
category = _("Comments")
description = _("Comment id")
def safe_call(self):
""" Safe call
"""
return getattr(self.comment, 'comment_id', u'')
"""Safe call"""
return getattr(self.comment, "comment_id", "")
class Text(CommentSubstitution):
""" Comment text
"""
category = _(u'Comments')
description = _(u'Comment text')
"""Comment text"""
category = _("Comments")
description = _("Comment text")
def safe_call(self):
""" Safe call
"""
return getattr(self.comment, 'text', u'')
"""Safe call"""
return getattr(self.comment, "text", "")
class AuthorUserName(CommentSubstitution):
""" Comment author user name string substitution
"""
category = _(u'Comments')
description = _(u'Comment author user name')
"""Comment author user name string substitution"""
category = _("Comments")
description = _("Comment author user name")
def safe_call(self):
""" Safe call
"""
return getattr(self.comment, 'author_username', u'')
"""Safe call"""
return getattr(self.comment, "author_username", "")
class AuthorFullName(CommentSubstitution):
""" Comment author full name string substitution
"""
category = _(u'Comments')
description = _(u'Comment author full name')
"""Comment author full name string substitution"""
category = _("Comments")
description = _("Comment author full name")
def safe_call(self):
""" Safe call
"""
return getattr(self.comment, 'author_name', u'')
"""Safe call"""
return getattr(self.comment, "author_name", "")
class AuthorEmail(CommentSubstitution):
""" Comment author email string substitution
"""
category = _(u'Comments')
description = _(u'Comment author email')
"""Comment author email string substitution"""
category = _("Comments")
description = _("Comment author email")
def safe_call(self):
""" Safe call
"""
return getattr(self.comment, 'author_email', u'')
"""Safe call"""
return getattr(self.comment, "author_email", "")

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""The conversation and replies adapters
The conversation is responsible for storing all comments. It provides a
@ -10,6 +9,10 @@ manipulate the same data structures, but provide an API for finding and
manipulating the comments directly in reply to a particular comment or at the
top level of the conversation.
"""
from .comment import Comment
from .interfaces import DISCUSSION_ANNOTATION_KEY as ANNOTATION_KEY
from .interfaces import IConversation
from .interfaces import IReplies
from AccessControl.SpecialUsers import nobody as user_nobody
from Acquisition import aq_base
from Acquisition import aq_inner
@ -22,11 +25,7 @@ from OFS.event import ObjectWillBeAddedEvent
from OFS.event import ObjectWillBeRemovedEvent
from OFS.Traversable import Traversable
from persistent import Persistent
from plone.app.discussion.comment import Comment
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IReplies
from Products.CMFPlone import DISCUSSION_ANNOTATION_KEY as ANNOTATION_KEY
from Products.CMFPlone.interfaces import IHideFromBreadcrumbs
from plone.base.interfaces import IHideFromBreadcrumbs
from zope.annotation.interfaces import IAnnotatable
from zope.annotation.interfaces import IAnnotations
from zope.component import adapter
@ -37,7 +36,6 @@ from zope.lifecycleevent import ObjectAddedEvent
from zope.lifecycleevent import ObjectCreatedEvent
from zope.lifecycleevent import ObjectRemovedEvent
import six
import time
@ -51,7 +49,7 @@ class Conversation(Traversable, Persistent, Explicit):
__allow_access_to_unprotected_subobjects__ = True
def __init__(self, id='++conversation++default'):
def __init__(self, id="++conversation++default"):
self.id = id
# username -> count of comments; key is removed when count reaches 0
@ -72,12 +70,11 @@ class Conversation(Traversable, Persistent, Explicit):
def enabled(self):
parent = aq_inner(self.__parent__)
return parent.restrictedTraverse('@@conversation_view').enabled()
return parent.restrictedTraverse("@@conversation_view").enabled()
def total_comments(self):
public_comments = [
x for x in self.values()
if user_nobody.has_permission('View', x)
x for x in self.values() if user_nobody.has_permission("View", x)
]
return len(public_comments)
@ -88,7 +85,7 @@ class Conversation(Traversable, Persistent, Explicit):
comment_keys = self._comments.keys()
for comment_key in reversed(comment_keys):
comment = self._comments[comment_key]
if user_nobody.has_permission('View', comment):
if user_nobody.has_permission("View", comment):
return comment.creation_date
return None
@ -100,7 +97,7 @@ class Conversation(Traversable, Persistent, Explicit):
def public_commentators(self):
retval = set()
for comment in self._comments.values():
if not user_nobody.has_permission('View', comment):
if not user_nobody.has_permission("View", comment):
continue
retval.add(comment.author_username)
return tuple(retval)
@ -109,8 +106,7 @@ class Conversation(Traversable, Persistent, Explicit):
return self._comments.keys()
def getComments(self, start=0, size=None):
"""Get unthreaded comments
"""
"""Get unthreaded comments"""
count = 0
for comment in self._comments.values(min=start):
# Yield the acquisition wrapped comment
@ -121,20 +117,18 @@ class Conversation(Traversable, Persistent, Explicit):
return
def getThreads(self, start=0, size=None, root=0, depth=None):
"""Get threaded comments
"""
"""Get threaded comments"""
def recurse(comment_id, d=0):
# Yield the current comment before we look for its children
yield {'id': comment_id, 'comment': self[comment_id], 'depth': d}
yield {"id": comment_id, "comment": self[comment_id], "depth": d}
# Recurse if there are children and we are not out of our depth
if depth is None or d + 1 < depth:
children = self._children.get(comment_id, None)
if children is not None:
for child_id in children:
for value in recurse(child_id, d + 1):
yield value
yield from recurse(child_id, d + 1)
# Find top level threads
comments = self._children.get(root, None)
@ -148,8 +142,7 @@ class Conversation(Traversable, Persistent, Explicit):
return
# Let the closure recurse
for value in recurse(comment_id):
yield value
yield from recurse(comment_id)
def addComment(self, comment):
"""Add a new comment. The parent id should have been set already. The
@ -209,8 +202,7 @@ class Conversation(Traversable, Persistent, Explicit):
return int(key) in self._comments
def __getitem__(self, key):
"""Get an item by its int key
"""
"""Get an item by its int key"""
try:
comment_id = int(key)
except ValueError:
@ -218,8 +210,7 @@ class Conversation(Traversable, Persistent, Explicit):
return self._comments[comment_id].__of__(self)
def __delitem__(self, key, suppress_container_modified=False):
"""Delete an item by its int key
"""
"""Delete an item by its int key"""
key = int(key)
@ -269,21 +260,30 @@ class Conversation(Traversable, Persistent, Explicit):
return self._comments.keys()
def items(self):
return [(i[0], i[1].__of__(self),) for i in self._comments.items()]
return [
(
i[0],
i[1].__of__(self),
)
for i in self._comments.items()
]
def values(self):
return [v.__of__(self) for v in self._comments.values()]
def iterkeys(self):
return six.iterkeys(self._comments)
return self._comments.keys()
def itervalues(self):
for v in six.itervalues(self._comments):
for v in self._comments.values():
yield v.__of__(self)
def iteritems(self):
for k, v in six.iteritems(self._comments):
yield (k, v.__of__(self),)
for k, v in self._comments.items():
yield (
k,
v.__of__(self),
)
def allowedContentTypes(self):
return []
@ -309,6 +309,7 @@ try:
except ImportError:
pass
else:
@implementer(IConversation) # pragma: no cover
@adapter(IAnnotatable) # pragma: no cover
def conversationCanonicalAdapterFactory(content): # pragma: no cover
@ -327,7 +328,7 @@ else:
@implementer(IReplies)
@adapter(Conversation) # relies on implementation details
class ConversationReplies(object):
class ConversationReplies:
"""An IReplies adapter for conversations.
This makes it easy to work with top-level comments.
@ -350,16 +351,14 @@ class ConversationReplies(object):
return int(key) in self.children
def __getitem__(self, key):
"""Get an item by its int key
"""
"""Get an item by its int key"""
key = int(key)
if key not in self.children:
raise KeyError(key)
return self.conversation[key]
def __delitem__(self, key):
"""Delete an item by its int key
"""
"""Delete an item by its int key"""
key = int(key)
if key not in self.children:
raise KeyError(key)
@ -392,7 +391,10 @@ class ConversationReplies(object):
def iteritems(self):
for key in self.children:
yield (key, self.conversation[key],)
yield (
key,
self.conversation[key],
)
@property
def children(self):
@ -418,11 +420,12 @@ class CommentReplies(ConversationReplies):
self.conversation = aq_parent(self.comment)
conversation_has_no_children = not hasattr(
self.conversation,
'_children',
"_children",
)
if self.conversation is None or conversation_has_no_children:
raise TypeError("This adapter doesn't know what to do with the "
'parent conversation')
raise TypeError(
"This adapter doesn't know what to do with the " "parent conversation"
)
self.comment_id = self.comment.comment_id

View File

@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
""" Custom discussion events
"""
from plone.app.discussion.interfaces import ICommentAddedEvent
from plone.app.discussion.interfaces import ICommentModifiedEvent
from plone.app.discussion.interfaces import ICommentRemovedEvent
from plone.app.discussion.interfaces import IDiscussionEvent
from plone.app.discussion.interfaces import ICommentDeletedEvent
from plone.app.discussion.interfaces import ICommentModifiedEvent
from plone.app.discussion.interfaces import ICommentPublishedEvent
from plone.app.discussion.interfaces import ICommentRemovedEvent
from plone.app.discussion.interfaces import ICommentTransitionEvent
from plone.app.discussion.interfaces import IDiscussionEvent
from plone.app.discussion.interfaces import IReplyAddedEvent
from plone.app.discussion.interfaces import IReplyModifiedEvent
from plone.app.discussion.interfaces import IReplyRemovedEvent
@ -15,9 +14,8 @@ from zope.interface import implementer
@implementer(IDiscussionEvent)
class DiscussionEvent(object):
""" Custom event
"""
class DiscussionEvent:
"""Custom event"""
def __init__(self, context, comment, **kwargs):
self.object = context
@ -28,55 +26,47 @@ class DiscussionEvent(object):
# Add event to the request to be able to access comment attributes
# in content-rules dynamic strings
request = context.REQUEST
request.set('event', self)
request.set("event", self)
@implementer(ICommentAddedEvent)
class CommentAddedEvent(DiscussionEvent):
""" Event to be triggered when a Comment is added
"""
"""Event to be triggered when a Comment is added"""
@implementer(ICommentModifiedEvent)
class CommentModifiedEvent(DiscussionEvent):
""" Event to be triggered when a Comment is modified
"""
"""Event to be triggered when a Comment is modified"""
@implementer(ICommentRemovedEvent)
class CommentRemovedEvent(DiscussionEvent):
""" Event to be triggered when a Comment is removed
"""
"""Event to be triggered when a Comment is removed"""
@implementer(IReplyAddedEvent)
class ReplyAddedEvent(DiscussionEvent):
""" Event to be triggered when a Comment reply is added
"""
"""Event to be triggered when a Comment reply is added"""
@implementer(IReplyModifiedEvent)
class ReplyModifiedEvent(DiscussionEvent):
""" Event to be triggered when a Comment reply is modified
"""
"""Event to be triggered when a Comment reply is modified"""
@implementer(IReplyRemovedEvent)
class ReplyRemovedEvent(DiscussionEvent):
""" Event to be triggered when a Comment reply is removed
"""
"""Event to be triggered when a Comment reply is removed"""
@implementer(ICommentDeletedEvent)
class CommentDeletedEvent(DiscussionEvent):
""" Event to be triggered when a Comment is deleted
"""
"""Event to be triggered when a Comment is deleted"""
@implementer(ICommentPublishedEvent)
class CommentPublishedEvent(DiscussionEvent):
""" Event to be triggered when a Comment is publicated
"""
"""Event to be triggered when a Comment is publicated"""
@implementer(ICommentTransitionEvent)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Interfaces for plone.app.discussion
"""
from plone.app.discussion import _
@ -12,11 +11,14 @@ from zope.interface.common.mapping import IIterableMapping
from zope.interface.interfaces import IObjectEvent
DISCUSSION_ANNOTATION_KEY = "plone.app.discussion:conversation"
def isEmail(value):
portal = getUtility(ISiteRoot)
reg_tool = getToolByName(portal, 'portal_registration')
reg_tool = getToolByName(portal, "portal_registration")
if not (value and reg_tool.isValidEmail(value)):
raise Invalid(_('Invalid email address.'))
raise Invalid(_("Invalid email address."))
return True
@ -42,25 +44,24 @@ class IConversation(IIterableMapping):
"""
total_comments = schema.Int(
title=_(u'Total number of public comments on this item'),
title=_("Total number of public comments on this item"),
min=0,
readonly=True,
)
last_comment_date = schema.Date(
title=_(u'Date of the most recent public comment'),
title=_("Date of the most recent public comment"),
readonly=True,
)
commentators = schema.Set(
title=_(u'The set of unique commentators (usernames)'),
title=_("The set of unique commentators (usernames)"),
readonly=True,
)
public_commentators = schema.Set(
title=_(
u'The set of unique commentators (usernames) '
u'of published_comments',
"The set of unique commentators (usernames) " "of published_comments",
),
readonly=True,
)
@ -72,8 +73,7 @@ class IConversation(IIterableMapping):
"""
def __delitem__(key):
"""Delete the comment with the given key. The key is a long id.
"""
"""Delete the comment with the given key. The key is a long id."""
def getComments(start=0, size=None):
"""Return an iterator of comment objects for rendering.
@ -130,8 +130,7 @@ class IReplies(IIterableMapping):
"""
def __delitem__(key):
"""Delete the comment with the given key. The key is a long id.
"""
"""Delete the comment with the given key. The key is a long id."""
class IComment(Interface):
@ -141,61 +140,58 @@ class IComment(Interface):
"""
portal_type = schema.ASCIILine(
title=_(u'Portal type'),
default='Discussion Item',
title=_("Portal type"),
default="Discussion Item",
)
__parent__ = schema.Object(
title=_(u'Conversation'), schema=Interface)
__parent__ = schema.Object(title=_("Conversation"), schema=Interface)
__name__ = schema.TextLine(title=_(u'Name'))
__name__ = schema.TextLine(title=_("Name"))
comment_id = schema.Int(
title=_(u'A comment id unique to this conversation'))
comment_id = schema.Int(title=_("A comment id unique to this conversation"))
in_reply_to = schema.Int(
title=_(u'Id of comment this comment is in reply to'),
title=_("Id of comment this comment is in reply to"),
required=False,
)
# for logged in comments - set to None for anonymous
author_username = schema.TextLine(title=_(u'Name'), required=False)
author_username = schema.TextLine(title=_("Name"), required=False)
# for anonymous comments only, set to None for logged in comments
author_name = schema.TextLine(title=_(u'Name'), required=False)
author_email = schema.TextLine(title=_(u'Email'),
required=False,
constraint=isEmail,
)
author_name = schema.TextLine(title=_("Name"), required=False)
author_email = schema.TextLine(
title=_("Email"),
required=False,
constraint=isEmail,
)
title = schema.TextLine(title=_(u'label_subject',
default=u'Subject'))
title = schema.TextLine(title=_("label_subject", default="Subject"))
mime_type = schema.ASCIILine(title=_(u'MIME type'), default='text/plain')
mime_type = schema.ASCIILine(title=_("MIME type"), default="text/plain")
text = schema.Text(
title=_(
u'label_comment',
default=u'Comment',
"label_comment",
default="Comment",
),
)
user_notification = schema.Bool(
title=_(
u'Notify me of new comments via email.',
"Notify me of new comments via email.",
),
required=False,
)
creator = schema.TextLine(title=_(u'Username of the commenter'))
creation_date = schema.Date(title=_(u'Creation date'))
modification_date = schema.Date(title=_(u'Modification date'))
creator = schema.TextLine(title=_("Username of the commenter"))
creation_date = schema.Date(title=_("Creation date"))
modification_date = schema.Date(title=_("Modification date"))
class ICaptcha(Interface):
"""Captcha/ReCaptcha text field to extend the existing comment form.
"""
captcha = schema.TextLine(title=_(u'Captcha'),
required=False)
"""Captcha/ReCaptcha text field to extend the existing comment form."""
captcha = schema.TextLine(title=_("Captcha"), required=False)
class IDiscussionSettings(Interface):
@ -210,40 +206,38 @@ class IDiscussionSettings(Interface):
# - Search control panel: Show comments in search results
globally_enabled = schema.Bool(
title=_(u'label_globally_enabled',
default=u'Globally enable comments'),
title=_("label_globally_enabled", default="Globally enable comments"),
description=_(
u'help_globally_enabled',
default=u'If selected, users are able to post comments on the '
u'site. However, you will still need to enable comments '
u'for specific content types, folders or content '
u'objects before users will be able to post comments.',
"help_globally_enabled",
default="If selected, users are able to post comments on the "
"site. However, you will still need to enable comments "
"for specific content types, folders or content "
"objects before users will be able to post comments.",
),
required=False,
default=False,
)
anonymous_comments = schema.Bool(
title=_(u'label_anonymous_comments',
default='Enable anonymous comments'),
title=_("label_anonymous_comments", default="Enable anonymous comments"),
description=_(
u'help_anonymous_comments',
default=u'If selected, anonymous users are able to post '
u'comments without logging in. It is highly '
u'recommended to use a captcha solution to prevent '
u'spam if this setting is enabled.',
"help_anonymous_comments",
default="If selected, anonymous users are able to post "
"comments without logging in. It is highly "
"recommended to use a captcha solution to prevent "
"spam if this setting is enabled.",
),
required=False,
default=False,
)
anonymous_email_enabled = schema.Bool(
title=_(u'label_anonymous_email_enabled',
default=u'Enable anonymous email field'),
title=_(
"label_anonymous_email_enabled", default="Enable anonymous email field"
),
description=_(
u'help_anonymous_email_enabled',
default=u'If selected, anonymous user will have to '
u'give their email.',
"help_anonymous_email_enabled",
default="If selected, anonymous user will have to " "give their email.",
),
required=False,
default=False,
@ -251,130 +245,137 @@ class IDiscussionSettings(Interface):
moderation_enabled = schema.Bool(
title=_(
u'label_moderation_enabled',
default='Enable comment moderation',
"label_moderation_enabled",
default="Enable comment moderation",
),
description=_(
u'help_moderation_enabled',
default=u'If selected, comments will enter a "Pending" state '
u'in which they are invisible to the public. A user '
u'with the "Review comments" permission ("Reviewer" '
u'or "Manager") can approve comments to make them '
u'visible to the public. If you want to enable a '
u'custom comment workflow, you have to go to the '
u'types control panel.',
"help_moderation_enabled",
default='If selected, comments will enter a "Pending" state '
"in which they are invisible to the public. A user "
'with the "Review comments" permission ("Reviewer" '
'or "Manager") can approve comments to make them '
"visible to the public. If you want to enable a "
"custom comment workflow, you have to go to the "
"types control panel.",
),
required=False,
default=False,
)
edit_comment_enabled = schema.Bool(
title=_(u'label_edit_comment_enabled',
default='Enable editing of comments'),
description=_(u'help_edit_comment_enabled',
default=u'If selected, supports editing '
'of comments for users with the "Edit comments" '
'permission.'),
title=_("label_edit_comment_enabled", default="Enable editing of comments"),
description=_(
"help_edit_comment_enabled",
default="If selected, supports editing "
'of comments for users with the "Edit comments" '
"permission.",
),
required=False,
default=False,
)
delete_own_comment_enabled = schema.Bool(
title=_(u'label_delete_own_comment_enabled',
default='Enable deleting own comments'),
description=_(u'help_delete_own_comment_enabled',
default=u'If selected, supports deleting '
'of own comments for users with the '
'"Delete own comments" permission.'),
title=_(
"label_delete_own_comment_enabled", default="Enable deleting own comments"
),
description=_(
"help_delete_own_comment_enabled",
default="If selected, supports deleting "
"of own comments for users with the "
'"Delete own comments" permission.',
),
required=False,
default=False,
)
text_transform = schema.Choice(
title=_(u'label_text_transform',
default='Comment text transform'),
title=_("label_text_transform", default="Comment text transform"),
description=_(
u'help_text_transform',
default=u'Use this setting to choose if the comment text '
u'should be transformed in any way. You can choose '
u'between "Plain text" and "Intelligent text". '
u'"Intelligent text" converts plain text into HTML '
u'where line breaks and indentation is preserved, '
u'and web and email addresses are made into '
u'clickable links.'),
"help_text_transform",
default="Use this setting to choose if the comment text "
"should be transformed in any way. You can choose "
'between "Plain text" and "Intelligent text". '
'"Intelligent text" converts plain text into HTML '
"where line breaks and indentation is preserved, "
"and web and email addresses are made into "
"clickable links.",
),
required=True,
default='text/plain',
vocabulary='plone.app.discussion.vocabularies.TextTransformVocabulary',
default="text/plain",
vocabulary="plone.app.discussion.vocabularies.TextTransformVocabulary",
)
captcha = schema.Choice(
title=_(u'label_captcha',
default='Captcha'),
title=_("label_captcha", default="Captcha"),
description=_(
u'help_captcha',
default=u'Use this setting to enable or disable Captcha '
u'validation for comments. Install '
u'plone.formwidget.captcha, '
u'plone.formwidget.recaptcha, collective.akismet, or '
u'collective.z3cform.norobots if there are no options '
u'available.'),
"help_captcha",
default="Use this setting to enable or disable Captcha "
"validation for comments. Install "
"plone.formwidget.captcha, "
"plone.formwidget.recaptcha, collective.akismet, or "
"collective.z3cform.norobots if there are no options "
"available.",
),
required=True,
default='disabled',
vocabulary='plone.app.discussion.vocabularies.CaptchaVocabulary',
default="disabled",
vocabulary="plone.app.discussion.vocabularies.CaptchaVocabulary",
)
show_commenter_image = schema.Bool(
title=_(u'label_show_commenter_image',
default=u'Show commenter image'),
title=_("label_show_commenter_image", default="Show commenter image"),
description=_(
u'help_show_commenter_image',
default=u'If selected, an image of the user is shown next to '
u'the comment.'),
"help_show_commenter_image",
default="If selected, an image of the user is shown next to "
"the comment.",
),
required=False,
default=True,
)
moderator_notification_enabled = schema.Bool(
title=_(u'label_moderator_notification_enabled',
default=u'Enable moderator email notification'),
title=_(
"label_moderator_notification_enabled",
default="Enable moderator email notification",
),
description=_(
u'help_moderator_notification_enabled',
default=u'If selected, the moderator is notified if a comment '
u'needs attention. The moderator email address can '
u'be set below.'),
"help_moderator_notification_enabled",
default="If selected, the moderator is notified if a comment "
"needs attention. The moderator email address can "
"be set below.",
),
required=False,
default=False,
)
moderator_email = schema.ASCIILine(
title=_(
u'label_moderator_email',
default=u'Moderator Email Address',
"label_moderator_email",
default="Moderator Email Address",
),
description=_(
u'help_moderator_email',
default=u'Address to which moderator notifications '
u'will be sent.'),
"help_moderator_email",
default="Address to which moderator notifications " "will be sent.",
),
required=False,
)
user_notification_enabled = schema.Bool(
title=_(
u'label_user_notification_enabled',
default=u'Enable user email notification',
"label_user_notification_enabled",
default="Enable user email notification",
),
description=_(
u'help_user_notification_enabled',
default=u'If selected, users can choose to be notified '
u'of new comments by email.'),
"help_user_notification_enabled",
default="If selected, users can choose to be notified "
"of new comments by email.",
),
required=False,
default=False,
)
class IDiscussionLayer(Interface):
"""Request marker installed via browserlayer.xml.
"""
"""Request marker installed via browserlayer.xml."""
class ICommentingTool(Interface):
@ -384,54 +385,46 @@ class ICommentingTool(Interface):
of Plone that had a portal_discussion tool.
"""
#
# Custom events
#
class IDiscussionEvent(IObjectEvent):
""" Discussion custom event
"""
"""Discussion custom event"""
class ICommentAddedEvent(IDiscussionEvent):
""" Comment added
"""
"""Comment added"""
class ICommentModifiedEvent(IDiscussionEvent):
""" Comment modified
"""
"""Comment modified"""
class ICommentRemovedEvent(IDiscussionEvent):
""" Comment removed
"""
"""Comment removed"""
class IReplyAddedEvent(IDiscussionEvent):
""" Comment reply added
"""
"""Comment reply added"""
class IReplyModifiedEvent(IDiscussionEvent):
""" Comment reply modified
"""
"""Comment reply modified"""
class IReplyRemovedEvent(IDiscussionEvent):
""" Comment reply removed
"""
"""Comment reply removed"""
class ICommentPublishedEvent(IDiscussionEvent):
""" Notify user on comment publication
"""
"""Notify user on comment publication"""
class ICommentDeletedEvent(IDiscussionEvent):
""" Notify user on comment delete
"""
"""Notify user on comment delete"""
class ICommentTransitionEvent(IDiscussionEvent):

View File

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*-
from Products.CMFCore.utils import getToolByName
def index_object(obj, event):
"""Index the object when it is added/modified to the conversation.
"""
"""Index the object when it is added/modified to the conversation."""
obj.indexObject()
def unindex_object(obj, event):
"""Unindex the object when it is removed from the conversation.
"""
"""Unindex the object when it is removed from the conversation."""
obj.unindexObject()

View File

@ -72,7 +72,7 @@
<!-- Control panel event subscribers -->
<subscriber
for="Products.CMFPlone.interfaces.events.IConfigurationChangedEvent"
for="plone.base.interfaces.events.IConfigurationChangedEvent"
handler=".browser.controlpanel.notify_configuration_changed"
/>

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.robotframework.testing import REMOTE_LIBRARY_ROBOT_TESTING
@ -15,40 +14,43 @@ from zope.component import queryUtility
try:
import plone.app.collection # noqa
COLLECTION_TYPE = 'Collection'
COLLECTION_TYPE = "Collection"
except ImportError:
COLLECTION_TYPE = 'Topic'
COLLECTION_TYPE = "Topic"
class PloneAppDiscussion(PloneSandboxLayer):
defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)
USER_NAME = 'johndoe'
USER_PASSWORD = 'secret'
MEMBER_NAME = 'janedoe'
MEMBER_PASSWORD = 'secret'
USER_WITH_FULLNAME_NAME = 'jim'
USER_WITH_FULLNAME_FULLNAME = 'Jim Fulton'
USER_WITH_FULLNAME_PASSWORD = 'secret'
MANAGER_USER_NAME = 'manager'
MANAGER_USER_PASSWORD = 'secret'
REVIEWER_NAME = 'reviewer'
REVIEWER_PASSWORD = 'secret'
USER_NAME = "johndoe"
USER_PASSWORD = "secret"
MEMBER_NAME = "janedoe"
MEMBER_PASSWORD = "secret"
USER_WITH_FULLNAME_NAME = "jim"
USER_WITH_FULLNAME_FULLNAME = "Jim Fulton"
USER_WITH_FULLNAME_PASSWORD = "secret"
MANAGER_USER_NAME = "manager"
MANAGER_USER_PASSWORD = "secret"
REVIEWER_NAME = "reviewer"
REVIEWER_PASSWORD = "secret"
def setUpZope(self, app, configurationContext):
# Load ZCML
import plone.app.discussion
self.loadZCML(package=plone.app.discussion,
context=configurationContext,
)
self.loadZCML(
package=plone.app.discussion,
context=configurationContext,
)
def setUpPloneSite(self, portal):
# Install into Plone site using portal_setup
applyProfile(portal, 'plone.app.discussion:default')
applyProfile(portal, "plone.app.discussion:default")
# Creates some users
acl_users = getToolByName(portal, 'acl_users')
acl_users = getToolByName(portal, "acl_users")
acl_users.userFolderAddUser(
self.USER_NAME,
self.USER_PASSWORD,
@ -58,41 +60,42 @@ class PloneAppDiscussion(PloneSandboxLayer):
acl_users.userFolderAddUser(
self.MEMBER_NAME,
self.MEMBER_PASSWORD,
['Member'],
["Member"],
[],
)
acl_users.userFolderAddUser(
self.USER_WITH_FULLNAME_NAME,
self.USER_WITH_FULLNAME_PASSWORD,
['Member'],
["Member"],
[],
)
acl_users.userFolderAddUser(
self.REVIEWER_NAME,
self.REVIEWER_PASSWORD,
['Member'],
["Member"],
[],
)
mtool = getToolByName(portal, 'portal_membership', None)
gtool = getToolByName(portal, 'portal_groups', None)
gtool.addPrincipalToGroup(self.REVIEWER_NAME, 'Reviewers')
mtool.addMember('jim', 'Jim', ['Member'], [])
mtool.getMemberById('jim').setMemberProperties(
{'fullname': 'Jim Fult\xc3\xb8rn'})
mtool = getToolByName(portal, "portal_membership", None)
gtool = getToolByName(portal, "portal_groups", None)
gtool.addPrincipalToGroup(self.REVIEWER_NAME, "Reviewers")
mtool.addMember("jim", "Jim", ["Member"], [])
mtool.getMemberById("jim").setMemberProperties(
{"fullname": "Jim Fult\xc3\xb8rn"}
)
acl_users.userFolderAddUser(
self.MANAGER_USER_NAME,
self.MANAGER_USER_PASSWORD,
['Manager'],
["Manager"],
[],
)
# Add a document
setRoles(portal, TEST_USER_ID, ['Manager'])
setRoles(portal, TEST_USER_ID, ["Manager"])
portal.invokeFactory(
id='doc1',
title='Document 1',
type_name='Document',
id="doc1",
title="Document 1",
type_name="Document",
)
@ -112,12 +115,12 @@ class PloneAppDiscussionRobot(PloneAppDiscussion):
PLONE_APP_DISCUSSION_ROBOT_FIXTURE = PloneAppDiscussionRobot()
PLONE_APP_DISCUSSION_FIXTURE = PloneAppDiscussion()
PLONE_APP_DISCUSSION_INTEGRATION_TESTING = IntegrationTesting(
bases=(PLONE_APP_DISCUSSION_FIXTURE,),
name='PloneAppDiscussion:Integration')
bases=(PLONE_APP_DISCUSSION_FIXTURE,), name="PloneAppDiscussion:Integration"
)
PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING = FunctionalTesting(
bases=(PLONE_APP_DISCUSSION_FIXTURE,),
name='PloneAppDiscussion:Functional')
bases=(PLONE_APP_DISCUSSION_FIXTURE,), name="PloneAppDiscussion:Functional"
)
PLONE_APP_DISCUSSION_ROBOT_TESTING = FunctionalTesting(
bases=(PLONE_APP_DISCUSSION_ROBOT_FIXTURE,),
name='PloneAppDiscussion:Robot',
name="PloneAppDiscussion:Robot",
)

View File

@ -252,7 +252,7 @@ Now we can post an anonymous comment.
>>> unprivileged_browser.open(urldoc)
>>> unprivileged_browser.getControl(name='form.widgets.text').value = "This is an anonymous comment"
>>> unprivileged_browser.getControl(name='form.widgets.author_name').value = u'John'
>>> unprivileged_browser.getControl(name='form.widgets.author_name').value = 'John'
>>> unprivileged_browser.getControl(name='form.widgets.author_email').value = 'john@acme.com'
>>> unprivileged_browser.getControl(name='form.buttons.comment').click()

View File

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
"""Test the plone.app.discussion catalog indexes
"""
from datetime import datetime
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.discussion.testing import ( # noqa
PLONE_APP_DISCUSSION_INTEGRATION_TESTING,
)
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from Products.CMFCore.utils import getToolByName
@ -21,33 +22,29 @@ class CatalogSetupTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
self.portal = self.layer["portal"]
def test_catalog_installed(self):
self.assertTrue(
'total_comments' in
self.portal.portal_catalog.indexes(),
"total_comments" in self.portal.portal_catalog.indexes(),
)
self.assertTrue(
'commentators' in
self.portal.portal_catalog.indexes(),
"commentators" in self.portal.portal_catalog.indexes(),
)
self.assertTrue(
'total_comments' in
self.portal.portal_catalog.schema(),
"total_comments" in self.portal.portal_catalog.schema(),
)
self.assertTrue(
'in_response_to' in
self.portal.portal_catalog.schema(),
"in_response_to" in self.portal.portal_catalog.schema(),
)
def test_collection_criteria_installed(self):
if 'portal_atct' not in self.portal:
if "portal_atct" not in self.portal:
return
try:
self.portal.portal_atct.getIndex('commentators')
self.portal.portal_atct.getIndex('total_comments')
self.portal.portal_atct.getMetadata('total_comments')
self.portal.portal_atct.getIndex("commentators")
self.portal.portal_atct.getIndex("total_comments")
self.portal.portal_atct.getMetadata("total_comments")
except AttributeError:
self.fail()
@ -57,19 +54,19 @@ class ConversationCatalogTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish')
workflow.doActionFor(self.portal.doc1, "publish")
self.catalog = getToolByName(self.portal, 'portal_catalog')
self.catalog = getToolByName(self.portal, "portal_catalog")
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1.title = 'Comment 1'
comment1.text = 'Comment text'
comment1.creator = 'jim'
comment1.author_username = 'Jim'
comment1 = createObject("plone.Comment")
comment1.title = "Comment 1"
comment1.text = "Comment text"
comment1.creator = "jim"
comment1.author_username = "Jim"
comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12)
comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12)
@ -79,9 +76,9 @@ class ConversationCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
self.conversation = conversation
@ -91,54 +88,54 @@ class ConversationCatalogTest(unittest.TestCase):
self.new_comment1_id = new_comment1_id
def test_total_comments(self):
self.assertTrue('total_comments' in self.doc1_brain)
self.assertTrue("total_comments" in self.doc1_brain)
self.assertEqual(self.doc1_brain.total_comments, 1)
comment2 = createObject('plone.Comment')
comment2.title = 'Comment 2'
comment2.text = 'Comment text'
comment2 = createObject("plone.Comment")
comment2.title = "Comment 2"
comment2.text = "Comment text"
new_comment2_id = self.conversation.addComment(comment2)
comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment2_id),
f"++conversation++default/{new_comment2_id}",
)
comment2.reindexObject()
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
doc1_brain = brains[0]
self.assertEqual(doc1_brain.total_comments, 2)
def test_last_comment_date(self):
self.assertTrue('last_comment_date' in self.doc1_brain)
self.assertTrue("last_comment_date" in self.doc1_brain)
self.assertEqual(
self.doc1_brain.last_comment_date,
datetime(2006, 9, 17, 14, 18, 12),
)
# Add another comment and check if last comment date is updated.
comment2 = createObject('plone.Comment')
comment2.title = 'Comment 2'
comment2.text = 'Comment text'
comment2 = createObject("plone.Comment")
comment2.title = "Comment 2"
comment2.text = "Comment text"
comment2.creation_date = datetime(2009, 9, 17, 14, 18, 12)
comment2.modification_date = datetime(2009, 9, 17, 14, 18, 12)
new_comment2_id = self.conversation.addComment(comment2)
comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment2_id),
f"++conversation++default/{new_comment2_id}",
)
comment2.reindexObject()
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
doc1_brain = brains[0]
@ -153,9 +150,9 @@ class ConversationCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
doc1_brain = brains[0]
@ -169,44 +166,44 @@ class ConversationCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
doc1_brain = brains[0]
self.assertEqual(doc1_brain.last_comment_date, None)
def test_commentators(self):
self.assertTrue('commentators' in self.doc1_brain)
self.assertEqual(self.doc1_brain.commentators, ('Jim',))
self.assertTrue("commentators" in self.doc1_brain)
self.assertEqual(self.doc1_brain.commentators, ("Jim",))
# add another comment with another author
comment2 = createObject('plone.Comment')
comment2.title = 'Comment 2'
comment2.text = 'Comment text'
comment2.creator = 'emma'
comment2.author_username = 'Emma'
comment2 = createObject("plone.Comment")
comment2.title = "Comment 2"
comment2.text = "Comment text"
comment2.creator = "emma"
comment2.author_username = "Emma"
new_comment2_id = self.conversation.addComment(comment2)
comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment2_id),
f"++conversation++default/{new_comment2_id}",
)
comment2.reindexObject()
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
doc1_brain = brains[0]
self.assertEqual(
sorted(doc1_brain.commentators),
sorted(('Jim', 'Emma')),
sorted(("Jim", "Emma")),
)
# remove one comments
@ -214,22 +211,22 @@ class ConversationCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
doc1_brain = brains[0]
self.assertEqual(doc1_brain.commentators, ('Jim',))
self.assertEqual(doc1_brain.commentators, ("Jim",))
# remove all comments
del self.conversation[self.new_comment1_id]
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
doc1_brain = brains[0]
@ -239,9 +236,9 @@ class ConversationCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Discussion Item',
portal_type="Discussion Item",
),
)
comment1_brain = brains[0]
@ -250,14 +247,14 @@ class ConversationCatalogTest(unittest.TestCase):
self.assertEqual(comment1_brain.total_comments, None)
def test_dont_index_private_commentators(self):
self.comment1.manage_permission('View', roles=tuple())
self.comment1.manage_permission("View", roles=tuple())
self.portal.doc1.reindexObject()
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
doc1_brain = brains[0]
@ -269,70 +266,70 @@ class CommentCatalogTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.catalog = getToolByName(self.portal, 'portal_catalog')
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.catalog = getToolByName(self.portal, "portal_catalog")
conversation = IConversation(self.portal.doc1)
self.conversation = conversation
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1.creator = 'jim'
comment1.author_name = 'Jim'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
comment1.creator = "jim"
comment1.author_name = "Jim"
new_comment1_id = conversation.addComment(comment1)
self.comment_id = new_comment1_id
# Comment brain
self.comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment1_id),
f"++conversation++default/{new_comment1_id}",
)
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.comment.getPhysicalPath()),
"query": "/".join(self.comment.getPhysicalPath()),
},
),
)
self.comment_brain = brains[0]
def test_title(self):
self.assertEqual(self.comment_brain.Title, 'Jim on Document 1')
self.assertEqual(self.comment_brain.Title, "Jim on Document 1")
def test_no_name_title(self):
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
cid = self.conversation.addComment(comment)
# Comment brain
comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(cid),
f"++conversation++default/{cid}",
)
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(comment.getPhysicalPath()),
"query": "/".join(comment.getPhysicalPath()),
},
),
)
comment_brain = brains[0]
self.assertEqual(comment_brain.Title, 'Anonymous on Document 1')
self.assertEqual(comment_brain.Title, "Anonymous on Document 1")
def test_type(self):
self.assertEqual(self.comment_brain.portal_type, 'Discussion Item')
self.assertEqual(self.comment_brain.Type, 'Comment')
self.assertEqual(self.comment_brain.portal_type, "Discussion Item")
self.assertEqual(self.comment_brain.Type, "Comment")
def test_review_state(self):
self.assertEqual(self.comment_brain.review_state, 'published')
self.assertEqual(self.comment_brain.review_state, "published")
def test_creator(self):
self.assertEqual(self.comment_brain.Creator, 'jim')
self.assertEqual(self.comment_brain.Creator, "jim")
def test_in_response_to(self):
"""Make sure in_response_to returns the title or id of the content
object the comment was added to.
object the comment was added to.
"""
self.assertEqual(self.comment_brain.in_response_to, 'Document 1')
self.assertEqual(self.comment_brain.in_response_to, "Document 1")
def test_add_comment(self):
self.assertTrue(self.comment_brain)
@ -344,7 +341,7 @@ class CommentCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.comment.getPhysicalPath()),
"query": "/".join(self.comment.getPhysicalPath()),
},
),
)
@ -352,54 +349,54 @@ class CommentCatalogTest(unittest.TestCase):
def test_reindex_comment(self):
# Make sure a comment is reindexed on the catalog when is modified
self.comment.text = 'Another text'
self.comment.text = "Another text"
notify(ObjectModifiedEvent(self.comment))
brains = self.catalog.searchResults(SearchableText='Another text')
brains = self.catalog.searchResults(SearchableText="Another text")
self.assertEqual(len(brains), 1)
def test_remove_comments_when_content_object_is_removed(self):
"""Make sure all comments are removed from the catalog, if the content
object is removed.
object is removed.
"""
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
self.assertEqual(len(brains), 1)
self.portal.manage_delObjects(['doc1'])
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
self.portal.manage_delObjects(["doc1"])
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
self.assertEqual(len(brains), 0)
def test_move_comments_when_content_object_is_moved(self):
# Create two folders and a content object with a comment
self.portal.invokeFactory(
id='folder1',
title='Folder 1',
type_name='Folder',
id="folder1",
title="Folder 1",
type_name="Folder",
)
self.portal.invokeFactory(
id='folder2',
title='Folder 2',
type_name='Folder',
id="folder2",
title="Folder 2",
type_name="Folder",
)
self.portal.folder1.invokeFactory(
id='moveme',
title='Move Me',
type_name='Document',
id="moveme",
title="Move Me",
type_name="Document",
)
conversation = IConversation(self.portal.folder1.moveme)
comment = createObject('plone.Comment')
comment = createObject("plone.Comment")
comment_id = conversation.addComment(comment)
# We need to commit here so that _p_jar isn't None and move will work
transaction.savepoint(optimistic=True)
# Move moveme from folder1 to folder2
cp = self.portal.folder1.manage_cutObjects(ids=('moveme',))
cp = self.portal.folder1.manage_cutObjects(ids=("moveme",))
self.portal.folder2.manage_pasteObjects(cp)
# Make sure no old comment brains are
brains = self.catalog.searchResults(
dict(
portal_type='Discussion Item',
portal_type="Discussion Item",
path={
'query': '/'.join(self.portal.folder1.getPhysicalPath()),
"query": "/".join(self.portal.folder1.getPhysicalPath()),
},
),
)
@ -407,61 +404,60 @@ class CommentCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults(
dict(
portal_type='Discussion Item',
portal_type="Discussion Item",
path={
'query': '/'.join(self.portal.folder2.getPhysicalPath()),
"query": "/".join(self.portal.folder2.getPhysicalPath()),
},
),
)
self.assertEqual(len(brains), 1)
self.assertEqual(
brains[0].getPath(),
'/plone/folder2/moveme/++conversation++default/' +
str(comment_id),
"/plone/folder2/moveme/++conversation++default/" + str(comment_id),
)
def test_move_upper_level_folder(self):
# create a folder with a nested structure
self.portal.invokeFactory(
id='sourcefolder',
title='Source Folder',
type_name='Folder',
id="sourcefolder",
title="Source Folder",
type_name="Folder",
)
self.portal.sourcefolder.invokeFactory(
id='moveme',
title='Move Me',
type_name='Folder',
id="moveme",
title="Move Me",
type_name="Folder",
)
self.portal.sourcefolder.moveme.invokeFactory(
id='mydocument',
title='My Document',
type_name='Folder',
id="mydocument",
title="My Document",
type_name="Folder",
)
self.portal.invokeFactory(
id='targetfolder',
title='Target Folder',
type_name='Folder',
id="targetfolder",
title="Target Folder",
type_name="Folder",
)
# create comment on my-document
conversation = IConversation(
self.portal.sourcefolder.moveme.mydocument,
)
comment = createObject('plone.Comment')
comment = createObject("plone.Comment")
comment_id = conversation.addComment(comment)
# We need to commit here so that _p_jar isn't None and move will work
transaction.savepoint(optimistic=True)
# Move moveme from folder1 to folder2
cp = self.portal.sourcefolder.manage_cutObjects(ids=('moveme',))
cp = self.portal.sourcefolder.manage_cutObjects(ids=("moveme",))
self.portal.targetfolder.manage_pasteObjects(cp)
# Make sure no old comment brains are left
brains = self.catalog.searchResults(
dict(
portal_type='Discussion Item',
path={'query': '/plone/sourcefolder/moveme'},
portal_type="Discussion Item",
path={"query": "/plone/sourcefolder/moveme"},
),
)
self.assertEqual(len(brains), 0)
@ -469,49 +465,47 @@ class CommentCatalogTest(unittest.TestCase):
# make sure comments are correctly index on the target
brains = self.catalog.searchResults(
dict(
portal_type='Discussion Item',
path={'query': '/plone/targetfolder/moveme'},
portal_type="Discussion Item",
path={"query": "/plone/targetfolder/moveme"},
),
)
self.assertEqual(len(brains), 1)
self.assertEqual(
brains[0].getPath(),
'/plone/targetfolder/moveme/mydocument/++conversation++default/' +
str(comment_id),
"/plone/targetfolder/moveme/mydocument/++conversation++default/"
+ str(comment_id),
)
def test_update_comments_when_content_object_is_renamed(self):
# We need to commit here so that _p_jar isn't None and move will work
transaction.savepoint(optimistic=True)
self.portal.manage_renameObject('doc1', 'doc2')
self.portal.manage_renameObject("doc1", "doc2")
brains = self.catalog.searchResults(
portal_type='Discussion Item',
portal_type="Discussion Item",
)
self.assertEqual(len(brains), 1)
self.assertEqual(
brains[0].getPath(),
'/plone/doc2/++conversation++default/' +
str(self.comment_id),
"/plone/doc2/++conversation++default/" + str(self.comment_id),
)
def test_clear_and_rebuild_catalog(self):
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
self.assertTrue(brains)
# Clear and rebuild catalog
self.catalog.clearFindAndRebuild()
# Check if comment is still there
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
self.assertTrue(brains)
comment_brain = brains[0]
self.assertEqual(comment_brain.Title, u'Jim on Document 1')
self.assertEqual(comment_brain.Title, "Jim on Document 1")
self.assertEqual(
comment_brain.getPath(),
'/plone/doc1/++conversation++default/' +
str(self.comment_id),
"/plone/doc1/++conversation++default/" + str(self.comment_id),
)
def test_clear_and_rebuild_catalog_for_nested_comments(self):
@ -526,25 +520,25 @@ class CommentCatalogTest(unittest.TestCase):
# +- Comment 2
# +- Comment 2_1
comment1_1 = createObject('plone.Comment')
comment1_1.title = 'Re: Comment 1'
comment1_1.text = 'Comment text'
comment1_1 = createObject("plone.Comment")
comment1_1.title = "Re: Comment 1"
comment1_1.text = "Comment text"
comment1_1_1 = createObject('plone.Comment')
comment1_1_1.title = 'Re: Re: Comment 1'
comment1_1_1.text = 'Comment text'
comment1_1_1 = createObject("plone.Comment")
comment1_1_1.title = "Re: Re: Comment 1"
comment1_1_1.text = "Comment text"
comment1_2 = createObject('plone.Comment')
comment1_2.title = 'Re: Comment 1 (2)'
comment1_2.text = 'Comment text'
comment1_2 = createObject("plone.Comment")
comment1_2.title = "Re: Comment 1 (2)"
comment1_2.text = "Comment text"
comment2 = createObject('plone.Comment')
comment2.title = 'Comment 2'
comment2.text = 'Comment text'
comment2 = createObject("plone.Comment")
comment2.title = "Comment 2"
comment2.text = "Comment text"
comment2_1 = createObject('plone.Comment')
comment2_1.title = 'Re: Comment 2'
comment2_1.text = 'Comment text'
comment2_1 = createObject("plone.Comment")
comment2_1.title = "Re: Comment 2"
comment2_1.text = "Comment text"
# Create the nested comment structure
new_id_1 = self.conversation.addComment(self.comment)
@ -566,7 +560,7 @@ class CommentCatalogTest(unittest.TestCase):
self.catalog.clearFindAndRebuild()
# Check if comments are still there
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
self.assertTrue(brains)
self.assertEqual(len(brains), 6)
@ -576,19 +570,19 @@ class NoConversationCatalogTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.catalog = getToolByName(self.portal, 'portal_catalog')
self.catalog = getToolByName(self.portal, "portal_catalog")
conversation = IConversation(self.portal.doc1)
brains = self.catalog.searchResults(
dict(
path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
"query": "/".join(self.portal.doc1.getPhysicalPath()),
},
portal_type='Document',
portal_type="Document",
),
)
self.conversation = conversation
@ -596,11 +590,10 @@ class NoConversationCatalogTest(unittest.TestCase):
self.doc1_brain = brains[0]
def test_total_comments(self):
self.assertTrue('total_comments' in self.doc1_brain)
self.assertTrue("total_comments" in self.doc1_brain)
self.assertEqual(self.doc1_brain.total_comments, 0)
# Make sure no conversation has been created
self.assertTrue(
'plone.app.discussion:conversation' not in
IAnnotations(self.portal.doc1),
"plone.app.discussion:conversation" not in IAnnotations(self.portal.doc1),
)

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.browser.comment import View
from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IReplies
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from Products.CMFCore.utils import getToolByName
@ -12,11 +11,10 @@ from zope.component import getMultiAdapter
import datetime
import logging
import six
import unittest
logger = logging.getLogger('plone.app.discussion.tests')
logger = logging.getLogger("plone.app.discussion.tests")
logger.addHandler(logging.StreamHandler())
@ -25,30 +23,30 @@ class CommentTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
self.request = self.layer['request']
self.portal = self.layer["portal"]
self.request = self.layer["request"]
workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish')
workflow.doActionFor(self.portal.doc1, "publish")
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.catalog = getToolByName(self.portal, 'portal_catalog')
self.document_brain = self.catalog.searchResults(
portal_type='Document')[0]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.catalog = getToolByName(self.portal, "portal_catalog")
self.document_brain = self.catalog.searchResults(portal_type="Document")[0]
def test_factory(self):
comment1 = createObject('plone.Comment')
comment1 = createObject("plone.Comment")
self.assertTrue(IComment.providedBy(comment1))
def test_UTCDates(self):
utc_to_local_diff = \
datetime.datetime.now() - datetime.datetime.utcnow()
utc_to_local_diff = datetime.datetime.now() - datetime.datetime.utcnow()
utc_to_local_diff = abs(utc_to_local_diff.seconds)
if utc_to_local_diff < 60:
logger.warning('Your computer is living in a timezone where local '
'time equals utc time. Some potential errors can '
'get hidden by that')
comment1 = createObject('plone.Comment')
logger.warning(
"Your computer is living in a timezone where local "
"time equals utc time. Some potential errors can "
"get hidden by that"
)
comment1 = createObject("plone.Comment")
local_utc = datetime.datetime.utcnow()
for date in (comment1.creation_date, comment1.modification_date):
difference = abs(date - local_utc)
@ -58,171 +56,163 @@ class CommentTest(unittest.TestCase):
self.assertFalse(difference // 10)
def test_id(self):
comment1 = createObject('plone.Comment')
comment1 = createObject("plone.Comment")
comment1.comment_id = 123
self.assertEqual('123', comment1.id)
self.assertEqual('123', comment1.getId())
self.assertEqual(u'123', comment1.__name__)
self.assertEqual("123", comment1.id)
self.assertEqual("123", comment1.getId())
self.assertEqual("123", comment1.__name__)
def test_uid(self):
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1 = createObject("plone.Comment")
conversation.addComment(comment1)
comment_brain = self.catalog.searchResults(
portal_type='Discussion Item',
portal_type="Discussion Item",
)[0]
self.assertTrue(comment_brain.UID)
def test_uid_is_unique(self):
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1 = createObject("plone.Comment")
conversation.addComment(comment1)
comment2 = createObject('plone.Comment')
comment2 = createObject("plone.Comment")
conversation.addComment(comment2)
brains = self.catalog.searchResults(
portal_type='Discussion Item',
portal_type="Discussion Item",
)
self.assertNotEqual(brains[0].UID, brains[1].UID)
def test_comment_uid_differs_from_content_uid(self):
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1 = createObject("plone.Comment")
conversation.addComment(comment1)
comment_brain = self.catalog.searchResults(
portal_type='Discussion Item',
portal_type="Discussion Item",
)[0]
self.assertNotEqual(self.document_brain.UID, comment_brain.UID)
def test_title(self):
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1.author_name = 'Jim Fulton'
comment1 = createObject("plone.Comment")
comment1.author_name = "Jim Fulton"
conversation.addComment(comment1)
self.assertEqual('Jim Fulton on Document 1', comment1.Title())
self.assertEqual("Jim Fulton on Document 1", comment1.Title())
def test_no_name_title(self):
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1 = createObject("plone.Comment")
conversation.addComment(comment1)
self.assertEqual('Anonymous on Document 1', comment1.Title())
self.assertEqual("Anonymous on Document 1", comment1.Title())
def test_title_special_characters(self):
self.portal.invokeFactory(
id='doc_sp_chars',
title=u'Document äüö',
type_name='Document',
id="doc_sp_chars",
title="Document äüö",
type_name="Document",
)
conversation = IConversation(self.portal.doc_sp_chars)
comment1 = createObject('plone.Comment')
comment1.author_name = u'Tarek Ziadé'
comment1 = createObject("plone.Comment")
comment1.author_name = "Tarek Ziadé"
conversation.addComment(comment1)
self.assertEqual(u'Tarek Ziadé on Document äüö', comment1.Title())
self.assertEqual("Tarek Ziadé on Document äüö", comment1.Title())
def test_title_special_characters_utf8(self):
self.portal.invokeFactory(
id='doc_sp_chars_utf8',
title='Document ëïû',
type_name='Document',
id="doc_sp_chars_utf8",
title="Document ëïû",
type_name="Document",
)
conversation = IConversation(self.portal.doc_sp_chars_utf8)
comment1 = createObject('plone.Comment')
comment1.author_name = 'Hüüb Bôûmä'
comment1 = createObject("plone.Comment")
comment1.author_name = "Hüüb Bôûmä"
conversation.addComment(comment1)
self.assertEqual(u'Hüüb Bôûmä on Document ëïû', comment1.Title())
self.assertEqual("Hüüb Bôûmä on Document ëïû", comment1.Title())
def test_creator(self):
comment1 = createObject('plone.Comment')
comment1.creator = 'jim'
self.assertEqual('jim', comment1.Creator())
comment1 = createObject("plone.Comment")
comment1.creator = "jim"
self.assertEqual("jim", comment1.Creator())
def test_creator_author_name(self):
comment1 = createObject('plone.Comment')
comment1.author_name = 'joey'
self.assertEqual('joey', comment1.Creator())
comment1 = createObject("plone.Comment")
comment1.author_name = "joey"
self.assertEqual("joey", comment1.Creator())
def test_owner(self):
comment1 = createObject('plone.Comment')
self.assertEqual((['plone', 'acl_users'], TEST_USER_ID),
comment1.getOwnerTuple())
comment1 = createObject("plone.Comment")
self.assertEqual(
(["plone", "acl_users"], TEST_USER_ID), comment1.getOwnerTuple()
)
def test_type(self):
comment1 = createObject('plone.Comment')
self.assertEqual(comment1.Type(), 'Comment')
comment1 = createObject("plone.Comment")
self.assertEqual(comment1.Type(), "Comment")
def test_mime_type(self):
comment1 = createObject('plone.Comment')
self.assertEqual(comment1.mime_type, 'text/plain')
comment1 = createObject("plone.Comment")
self.assertEqual(comment1.mime_type, "text/plain")
def test_getText(self):
comment1 = createObject('plone.Comment')
comment1.text = 'First paragraph\n\nSecond_paragraph'
comment1 = createObject("plone.Comment")
comment1.text = "First paragraph\n\nSecond_paragraph"
self.assertEqual(
''.join(comment1.getText().split()),
'<p>Firstparagraph<br><br>Second_paragraph</p>',
"".join(comment1.getText().split()),
"<p>Firstparagraph<br><br>Second_paragraph</p>",
)
def test_getText_escapes_HTML(self):
comment1 = createObject('plone.Comment')
comment1.text = '<b>Got HTML?</b>'
comment1 = createObject("plone.Comment")
comment1.text = "<b>Got HTML?</b>"
self.assertEqual(
comment1.getText(),
'<p>&lt;b&gt;Got HTML?&lt;/b&gt;</p>',
"<p>&lt;b&gt;Got HTML?&lt;/b&gt;</p>",
)
def test_getText_with_non_ascii_characters(self):
comment1 = createObject('plone.Comment')
comment1.text = u'Umlaute sind ä, ö und ü.'
out = b'<p>Umlaute sind \xc3\xa4, \xc3\xb6 und \xc3\xbc.</p>'
if six.PY2:
self.assertEqual(
comment1.getText(),
out)
else:
self.assertEqual(
comment1.getText(),
out.decode('utf8'))
comment1 = createObject("plone.Comment")
comment1.text = "Umlaute sind ä, ö und ü."
out = b"<p>Umlaute sind \xc3\xa4, \xc3\xb6 und \xc3\xbc.</p>"
self.assertEqual(comment1.getText(), out.decode("utf8"))
def test_getText_doesnt_link(self):
comment1 = createObject('plone.Comment')
comment1.text = 'Go to http://www.plone.org'
comment1 = createObject("plone.Comment")
comment1.text = "Go to http://www.plone.org"
self.assertEqual(
comment1.getText(),
'<p>Go to http://www.plone.org</p>',
"<p>Go to http://www.plone.org</p>",
)
def test_getText_uses_comment_mime_type(self):
comment1 = createObject('plone.Comment')
comment1.text = 'Go to http://www.plone.org'
comment1.mime_type = 'text/x-web-intelligent'
comment1 = createObject("plone.Comment")
comment1.text = "Go to http://www.plone.org"
comment1.mime_type = "text/x-web-intelligent"
self.assertEqual(
comment1.getText(),
'Go to <a href="http://www.plone.org" ' +
'rel="nofollow">http://www.plone.org</a>',
'Go to <a href="http://www.plone.org" '
+ 'rel="nofollow">http://www.plone.org</a>',
)
def test_getText_uses_comment_mime_type_html(self):
comment1 = createObject('plone.Comment')
comment1 = createObject("plone.Comment")
comment1.text = 'Go to <a href="http://www.plone.org">plone.org</a>'
comment1.mime_type = 'text/html'
comment1.mime_type = "text/html"
self.assertEqual(
comment1.getText(),
'Go to <a href="http://www.plone.org">plone.org</a>',
)
def test_getText_w_custom_targetMimetype(self):
comment1 = createObject('plone.Comment')
comment1.text = 'para'
self.assertEqual(comment1.getText(targetMimetype='text/plain'), 'para')
comment1 = createObject("plone.Comment")
comment1.text = "para"
self.assertEqual(comment1.getText(targetMimetype="text/plain"), "para")
def test_getText_invalid_transformation_raises_error(self):
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1.mime_type = 'text/x-html-safe'
comment1.text = 'para'
comment1 = createObject("plone.Comment")
comment1.mime_type = "text/x-html-safe"
comment1.text = "para"
conversation.addComment(comment1)
self.assertEqual(
comment1.getText(targetMimetype='text/html'),
'para')
self.assertEqual(comment1.getText(targetMimetype="text/html"), "para")
def test_traversal(self):
# make sure comments are traversable, have an id, absolute_url and
@ -230,26 +220,29 @@ class CommentTest(unittest.TestCase):
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
new_comment1_id = conversation.addComment(comment1)
comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment1_id),
f"++conversation++default/{new_comment1_id}",
)
self.assertTrue(IComment.providedBy(comment))
self.assertEqual(
(
'', 'plone', 'doc1', '++conversation++default',
"",
"plone",
"doc1",
"++conversation++default",
str(new_comment1_id),
),
comment.getPhysicalPath(),
)
self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' +
str(new_comment1_id), comment.absolute_url(),
"http://nohost/plone/doc1/++conversation++default/" + str(new_comment1_id),
comment.absolute_url(),
)
def test_view_blob_types(self):
@ -258,68 +251,67 @@ class CommentTest(unittest.TestCase):
version of the url with a /view in it.
"""
self.portal.invokeFactory(
id='image1',
title='Image',
type_name='Image',
id="image1",
title="Image",
type_name="Image",
)
conversation = IConversation(self.portal.image1)
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
new_comment1_id = conversation.addComment(comment1)
comment = self.portal.image1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment1_id),
f"++conversation++default/{new_comment1_id}",
)
view = View(comment, self.request)
View.__call__(view)
response = self.request.response
self.assertIn('/view', response.headers['location'])
self.assertIn("/view", response.headers["location"])
def test_workflow(self):
"""Basic test for the 'comment_review_workflow'
"""
"""Basic test for the 'comment_review_workflow'"""
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',),
('comment_review_workflow,'),
("Discussion Item",),
("comment_review_workflow,"),
)
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1 = createObject("plone.Comment")
new_comment1_id = conversation.addComment(comment1)
comment = conversation[new_comment1_id]
# Make sure comments use the 'comment_review_workflow'
chain = self.portal.portal_workflow.getChainFor(comment)
self.assertEqual(('comment_review_workflow',), chain)
self.assertEqual(("comment_review_workflow",), chain)
# Ensure the initial state was entered and recorded
self.assertEqual(
1,
len(comment.workflow_history['comment_review_workflow']),
len(comment.workflow_history["comment_review_workflow"]),
)
self.assertEqual(
None,
comment.workflow_history['comment_review_workflow'][0]['action'],
comment.workflow_history["comment_review_workflow"][0]["action"],
)
self.assertEqual(
'pending',
self.portal.portal_workflow.getInfoFor(comment, 'review_state'),
"pending",
self.portal.portal_workflow.getInfoFor(comment, "review_state"),
)
def test_fti(self):
# test that we can look up an FTI for Discussion Item
self.assertIn(
'Discussion Item',
"Discussion Item",
self.portal.portal_types.objectIds(),
)
comment1 = createObject('plone.Comment')
comment1 = createObject("plone.Comment")
fti = self.portal.portal_types.getTypeInfo(comment1)
self.assertEqual('Discussion Item', fti.getTypeInfo(comment1).getId())
self.assertEqual("Discussion Item", fti.getTypeInfo(comment1).getId())
def test_view(self):
# make sure that the comment view is there and redirects to the right
@ -330,21 +322,21 @@ class CommentTest(unittest.TestCase):
conversation = IConversation(self.portal.doc1)
# Create a comment
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
# Add comment to the conversation
new_comment1_id = conversation.addComment(comment1)
comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment1_id),
f"++conversation++default/{new_comment1_id}",
)
# make sure the view is there
self.assertTrue(
getMultiAdapter(
(comment, self.request),
name='view',
name="view",
),
)
@ -362,11 +354,11 @@ class RepliesTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish')
workflow.doActionFor(self.portal.doc1, "publish")
def test_add_comment(self):
# Add comments to a CommentReplies adapter
@ -378,16 +370,16 @@ class RepliesTest(unittest.TestCase):
# Add a comment to the conversation
replies = IReplies(conversation)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
new_id = replies.addComment(comment)
comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id),
f"++conversation++default/{new_id}",
)
# Add a reply to the CommentReplies adapter of the first comment
re_comment = createObject('plone.Comment')
re_comment.text = 'Comment text'
re_comment = createObject("plone.Comment")
re_comment.text = "Comment text"
replies = IReplies(comment)
@ -415,16 +407,16 @@ class RepliesTest(unittest.TestCase):
# Add a comment to the conversation
replies = IReplies(conversation)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
new_id = replies.addComment(comment)
comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id),
f"++conversation++default/{new_id}",
)
# Add a reply to the CommentReplies adapter of the first comment
re_comment = createObject('plone.Comment')
re_comment.text = 'Comment text'
re_comment = createObject("plone.Comment")
re_comment.text = "Comment text"
replies = IReplies(comment)
@ -446,83 +438,86 @@ class RepliesTest(unittest.TestCase):
# physical path
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
conversation.addComment(comment1)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
new_id = conversation.addComment(comment)
comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id),
f"++conversation++default/{new_id}",
)
# Add a reply to the CommentReplies adapter of the first comment
re_comment = createObject('plone.Comment')
re_comment.text = 'Comment text'
re_comment = createObject("plone.Comment")
re_comment.text = "Comment text"
replies = IReplies(comment)
new_re_id = replies.addComment(re_comment)
re_comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_re_id),
f"++conversation++default/{new_re_id}",
)
# Add a reply to the reply
re_re_comment = createObject('plone.Comment')
re_re_comment.text = 'Comment text'
re_re_comment = createObject("plone.Comment")
re_re_comment.text = "Comment text"
replies = IReplies(re_comment)
new_re_re_id = replies.addComment(re_re_comment)
re_re_comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_re_re_id),
f"++conversation++default/{new_re_re_id}",
)
# Add a reply to the replies reply
re_re_re_comment = createObject('plone.Comment')
re_re_re_comment.text = 'Comment text'
re_re_re_comment = createObject("plone.Comment")
re_re_re_comment.text = "Comment text"
replies = IReplies(re_re_comment)
new_re_re_re_id = replies.addComment(re_re_re_comment)
re_re_re_comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_re_re_re_id),
f"++conversation++default/{new_re_re_re_id}",
)
self.assertEqual(
('', 'plone', 'doc1', '++conversation++default', str(new_id)),
("", "plone", "doc1", "++conversation++default", str(new_id)),
comment.getPhysicalPath(),
)
self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' +
str(new_id), comment.absolute_url(),
"http://nohost/plone/doc1/++conversation++default/" + str(new_id),
comment.absolute_url(),
)
self.assertEqual(
('', 'plone', 'doc1', '++conversation++default', str(new_re_id)),
("", "plone", "doc1", "++conversation++default", str(new_re_id)),
re_comment.getPhysicalPath(),
)
self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' +
str(new_re_id),
"http://nohost/plone/doc1/++conversation++default/" + str(new_re_id),
re_comment.absolute_url(),
)
self.assertEqual(
(
'', 'plone', 'doc1', '++conversation++default',
"",
"plone",
"doc1",
"++conversation++default",
str(new_re_re_id),
),
re_re_comment.getPhysicalPath(),
)
self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' +
str(new_re_re_id),
"http://nohost/plone/doc1/++conversation++default/" + str(new_re_re_id),
re_re_comment.absolute_url(),
)
self.assertEqual(
(
'', 'plone', 'doc1', '++conversation++default',
"",
"plone",
"doc1",
"++conversation++default",
str(new_re_re_re_id),
),
re_re_re_comment.getPhysicalPath(),
)
self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' +
str(new_re_re_re_id),
"http://nohost/plone/doc1/++conversation++default/" + str(new_re_re_re_id),
re_re_re_comment.absolute_url(),
)

View File

@ -1,14 +1,13 @@
# -*- coding: utf-8 -*-
from .. import interfaces
from ..browser.comment import EditCommentForm
from ..browser.comments import CommentForm
from ..browser.comments import CommentsViewlet
from ..interfaces import IConversation
from ..interfaces import IDiscussionSettings
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from AccessControl import Unauthorized
from datetime import datetime
from OFS.Image import Image
from plone.app.discussion import interfaces
from plone.app.discussion.browser.comment import EditCommentForm
from plone.app.discussion.browser.comments import CommentForm
from plone.app.discussion.browser.comments import CommentsViewlet
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.testing import login
from plone.app.testing import logout
from plone.app.testing import setRoles
@ -16,7 +15,6 @@ from plone.app.testing import TEST_USER_ID
from plone.app.testing import TEST_USER_NAME
from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.tests import dummy
from z3c.form.interfaces import IFormLayer
from zope import interface
from zope.annotation.interfaces import IAttributeAnnotatable
@ -28,33 +26,67 @@ from zope.interface import alsoProvides
from zope.interface import Interface
from zope.publisher.browser import TestRequest
from zope.publisher.interfaces.browser import IBrowserRequest
from ZPublisher.HTTPRequest import FileUpload
import io
import time
import unittest
TEXT = b"file data"
class DummyFile(FileUpload):
"""Dummy upload object
Used to fake uploaded files.
"""
__allow_access_to_unprotected_subobjects__ = 1
filename = "dummy.txt"
data = TEXT
headers = {}
def __init__(self, filename=None, data=None, headers=None):
if filename is not None:
self.filename = filename
if data is not None:
self.data = data
if headers is not None:
self.headers = headers
self.file = io.BytesIO(self.data)
def seek(self, *args):
pass
def tell(self, *args):
return 1
def read(self, *args):
return self.data
class TestCommentForm(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
self.request = self.layer['request']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal.invokeFactory('Folder', 'test-folder')
self.folder = self.portal['test-folder']
self.portal = self.layer["portal"]
self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.portal.invokeFactory("Folder", "test-folder")
self.folder = self.portal["test-folder"]
interface.alsoProvides(
self.portal.REQUEST,
interfaces.IDiscussionLayer,
)
wftool = getToolByName(self.portal, 'portal_workflow')
wftool.doActionFor(self.portal.doc1, action='publish')
wftool = getToolByName(self.portal, "portal_workflow")
wftool.doActionFor(self.portal.doc1, action="publish")
self.portal.doc1.allow_discussion = True
self.membershipTool = getToolByName(self.folder, 'portal_membership')
self.membershipTool = getToolByName(self.folder, "portal_membership")
self.memberdata = self.portal.portal_memberdata
self.context = getattr(self.portal, 'doc1')
self.context = getattr(self.portal, "doc1")
# Allow discussion
registry = queryUtility(IRegistry)
@ -62,8 +94,7 @@ class TestCommentForm(unittest.TestCase):
settings.globally_enabled = True
def test_add_comment(self):
"""Post a comment as logged-in user.
"""
"""Post a comment as logged-in user."""
# Allow discussion
self.portal.doc1.allow_discussion = True
@ -80,7 +111,7 @@ class TestCommentForm(unittest.TestCase):
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name=u'comment-form',
name="comment-form",
)
# The form should return an error if the comment text field is empty
@ -88,46 +119,45 @@ class TestCommentForm(unittest.TestCase):
commentForm = getMultiAdapter(
(self.context, request),
name=u'comment-form',
name="comment-form",
)
commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 1)
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
# The form is submitted successfully, if the required text field is
# filled out
request = make_request(form={'form.widgets.text': u'bar'})
request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter(
(self.context, request),
name=u'comment-form',
name="comment-form",
)
commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
comments = IConversation(commentForm.context).getComments()
comments = [comment for comment in comments] # consume iterator
self.assertEqual(len(comments), 1)
for comment in comments:
self.assertEqual(comment.text, u'bar')
self.assertEqual(comment.creator, 'test_user_1_')
self.assertEqual(comment.getOwner().getUserName(), 'test-user')
self.assertEqual(comment.text, "bar")
self.assertEqual(comment.creator, "test_user_1_")
self.assertEqual(comment.getOwner().getUserName(), "test-user")
local_roles = comment.get_local_roles()
self.assertEqual(len(local_roles), 1)
userid, roles = local_roles[0]
self.assertEqual(userid, 'test_user_1_')
self.assertEqual(userid, "test_user_1_")
self.assertEqual(len(roles), 1)
self.assertEqual(roles[0], 'Owner')
self.assertEqual(roles[0], "Owner")
def test_edit_comment(self):
"""Edit a comment as logged-in user.
"""
"""Edit a comment as logged-in user."""
# Allow discussion
self.portal.doc1.allow_discussion = True
@ -144,65 +174,64 @@ class TestCommentForm(unittest.TestCase):
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name=u'comment-form',
name="comment-form",
)
provideAdapter(
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=EditCommentForm,
name=u'edit-comment-form',
name="edit-comment-form",
)
# The form is submitted successfully, if the required text field is
# filled out
request = make_request(form={'form.widgets.text': u'bar'})
request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter(
(self.context, request),
name=u'comment-form',
name="comment-form",
)
commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
# Edit the last comment
conversation = IConversation(self.context)
comment = [x for x in conversation.getComments()][-1]
request = make_request(form={'form.widgets.text': u'foobar'})
request = make_request(form={"form.widgets.text": "foobar"})
editForm = getMultiAdapter(
(comment, request),
name=u'edit-comment-form',
name="edit-comment-form",
)
editForm.update()
data, errors = editForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0)
self.assertFalse(editForm.handleComment(editForm, 'foo'))
self.assertFalse(editForm.handleComment(editForm, "foo"))
comment = [x for x in conversation.getComments()][-1]
self.assertEqual(comment.text, u'foobar')
self.assertEqual(comment.text, "foobar")
comments = IConversation(commentForm.context).getComments()
comments = [c for c in comments] # consume iterator
self.assertEqual(len(comments), 1)
for comment in comments:
self.assertEqual(comment.text, u'foobar')
self.assertEqual(comment.creator, 'test_user_1_')
self.assertEqual(comment.text, "foobar")
self.assertEqual(comment.creator, "test_user_1_")
self.assertEqual(comment.getOwner().getUserName(), 'test-user')
self.assertEqual(comment.getOwner().getUserName(), "test-user")
local_roles = comment.get_local_roles()
self.assertEqual(len(local_roles), 1)
userid, roles = local_roles[0]
self.assertEqual(userid, 'test_user_1_')
self.assertEqual(userid, "test_user_1_")
self.assertEqual(len(roles), 1)
self.assertEqual(roles[0], 'Owner')
self.assertEqual(roles[0], "Owner")
def test_delete_comment(self):
"""Delete a comment as logged-in user.
"""
"""Delete a comment as logged-in user."""
# Allow discussion
self.portal.doc1.allow_discussion = True
@ -219,48 +248,47 @@ class TestCommentForm(unittest.TestCase):
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name=u'comment-form',
name="comment-form",
)
# The form is submitted successfully, if the required text field is
# filled out
form_request = make_request(form={'form.widgets.text': u'bar'})
form_request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter(
(self.context, form_request),
name=u'comment-form',
name="comment-form",
)
commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
# Delete the last comment
conversation = IConversation(self.context)
comment = [x for x in conversation.getComments()][-1]
deleteView = getMultiAdapter(
(comment, self.request),
name=u'moderate-delete-comment',
name="moderate-delete-comment",
)
# try to delete last comment without 'Delete comments' permission
setRoles(self.portal, TEST_USER_ID, ['Member'])
setRoles(self.portal, TEST_USER_ID, ["Member"])
self.assertRaises(
Unauthorized,
comment.restrictedTraverse,
'@@moderate-delete-comment',
"@@moderate-delete-comment",
)
deleteView()
self.assertEqual(1, len([x for x in conversation.getComments()]))
# try to delete last comment with 'Delete comments' permission
setRoles(self.portal, TEST_USER_ID, ['Reviewer'])
setRoles(self.portal, TEST_USER_ID, ["Reviewer"])
deleteView()
self.assertEqual(0, len([x for x in conversation.getComments()]))
setRoles(self.portal, TEST_USER_ID, ['Manager'])
setRoles(self.portal, TEST_USER_ID, ["Manager"])
def test_delete_own_comment(self):
"""Delete own comment as logged-in user.
"""
"""Delete own comment as logged-in user."""
# Allow discussion
self.portal.doc1.allow_discussion = True
@ -277,42 +305,42 @@ class TestCommentForm(unittest.TestCase):
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name=u'comment-form',
name="comment-form",
)
# The form is submitted successfully, if the required text field is
# filled out
form_request = make_request(form={'form.widgets.text': u'bar'})
form_request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter(
(self.context, form_request),
name=u'comment-form',
name="comment-form",
)
commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
# Delete the last comment
conversation = IConversation(self.context)
comment = [x for x in conversation.getComments()][-1]
deleteView = getMultiAdapter(
(comment, self.request),
name=u'delete-own-comment',
name="delete-own-comment",
)
# try to delete last comment with johndoe
setRoles(self.portal, 'johndoe', ['Member'])
login(self.portal, 'johndoe')
setRoles(self.portal, "johndoe", ["Member"])
login(self.portal, "johndoe")
self.assertRaises(
Unauthorized,
comment.restrictedTraverse,
'@@delete-own-comment',
"@@delete-own-comment",
)
self.assertEqual(1, len([x for x in conversation.getComments()]))
# try to delete last comment with the same user that created it
login(self.portal, TEST_USER_NAME)
setRoles(self.portal, TEST_USER_ID, ['Member'])
setRoles(self.portal, TEST_USER_ID, ["Member"])
deleteView()
self.assertEqual(0, len([x for x in conversation.getComments()]))
@ -335,40 +363,43 @@ class TestCommentForm(unittest.TestCase):
alsoProvides(request, IAttributeAnnotatable)
return request
provideAdapter(adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name=u'comment-form')
provideAdapter(
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name="comment-form",
)
# Post an anonymous comment and provide a name
request = make_request(form={
'form.widgets.name': u'john doe',
'form.widgets.text': u'bar',
})
request = make_request(
form={
"form.widgets.name": "john doe",
"form.widgets.text": "bar",
}
)
commentForm = getMultiAdapter(
(self.context, request),
name=u'comment-form',
name="comment-form",
)
commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'action'))
self.assertFalse(commentForm.handleComment(commentForm, "action"))
comments = IConversation(commentForm.context).getComments()
comments = [comment for comment in comments] # consume itertor
self.assertEqual(len(comments), 1)
for comment in IConversation(commentForm.context).getComments():
self.assertEqual(comment.text, u'bar')
self.assertEqual(comment.text, "bar")
self.assertIsNone(comment.creator)
roles = comment.get_local_roles()
self.assertEqual(len(roles), 0)
def test_can_not_add_comments_if_discussion_is_not_allowed(self):
"""Make sure that comments can't be posted if discussion is disabled.
"""
"""Make sure that comments can't be posted if discussion is disabled."""
# Disable discussion
registry = queryUtility(IRegistry)
@ -382,16 +413,18 @@ class TestCommentForm(unittest.TestCase):
alsoProvides(request, IAttributeAnnotatable)
return request
provideAdapter(adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name=u'comment-form')
provideAdapter(
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name="comment-form",
)
request = make_request(form={'form.widgets.text': u'bar'})
request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter(
(self.context, request),
name=u'comment-form',
name="comment-form",
)
commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
@ -400,14 +433,11 @@ class TestCommentForm(unittest.TestCase):
# allowed
self.assertEqual(len(errors), 0)
self.assertRaises(Unauthorized,
commentForm.handleComment,
commentForm,
'foo')
self.assertRaises(Unauthorized, commentForm.handleComment, commentForm, "foo")
def test_anonymous_can_not_add_comments_if_discussion_is_not_allowed(self):
"""Make sure that anonymous users can't post comments if anonymous
comments are disabled.
comments are disabled.
"""
# Anonymous comments are disabled by default
@ -421,15 +451,16 @@ class TestCommentForm(unittest.TestCase):
alsoProvides(request, IAttributeAnnotatable)
return request
provideAdapter(adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name=u'comment-form')
provideAdapter(
adapts=(Interface, IBrowserRequest),
provides=Interface,
factory=CommentForm,
name="comment-form",
)
request = make_request(form={'form.widgets.text': u'bar'})
request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter((self.context, request),
name=u'comment-form')
commentForm = getMultiAdapter((self.context, request), name="comment-form")
commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
@ -438,7 +469,7 @@ class TestCommentForm(unittest.TestCase):
Unauthorized,
commentForm.handleComment,
commentForm,
'foo',
"foo",
)
@ -447,22 +478,22 @@ class TestCommentsViewlet(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
self.request = self.layer['request']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal.invokeFactory('Folder', 'test-folder')
self.folder = self.portal['test-folder']
self.portal = self.layer["portal"]
self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.portal.invokeFactory("Folder", "test-folder")
self.folder = self.portal["test-folder"]
interface.alsoProvides(
self.request,
interfaces.IDiscussionLayer,
)
self.workflowTool = getToolByName(self.portal, 'portal_workflow')
self.workflowTool.setDefaultChain('comment_one_state_workflow')
self.workflowTool = getToolByName(self.portal, "portal_workflow")
self.workflowTool.setDefaultChain("comment_one_state_workflow")
self.membershipTool = getToolByName(self.folder, 'portal_membership')
self.membershipTool = getToolByName(self.folder, "portal_membership")
self.memberdata = self.portal.portal_memberdata
context = getattr(self.portal, 'doc1')
context = getattr(self.portal, "doc1")
self.viewlet = CommentsViewlet(context, self.request, None, None)
# Allow discussion
@ -484,15 +515,14 @@ class TestCommentsViewlet(unittest.TestCase):
# Anonymous has no 'can review' permission
self.assertFalse(self.viewlet.can_review())
# The reviewer role has the 'Review comments' permission
self.portal.acl_users._doAddUser(
'reviewer', 'secret', ['Reviewer'], [])
login(self.portal, 'reviewer')
self.portal.acl_users._doAddUser("reviewer", "secret", ["Reviewer"], [])
login(self.portal, "reviewer")
self.assertTrue(self.viewlet.can_review())
def test_can_manage(self):
"""We keep this method for backward compatibility. This method has been
removed in version 1.0b9 and added again in 1.0b11 because we don't
do API changes in beta releases.
removed in version 1.0b9 and added again in 1.0b11 because we don't
do API changes in beta releases.
"""
# Portal owner has 'can review' permission
self.assertTrue(self.viewlet.can_manage())
@ -500,9 +530,8 @@ class TestCommentsViewlet(unittest.TestCase):
# Anonymous has no 'can review' permission
self.assertFalse(self.viewlet.can_manage())
# The reviewer role has the 'Review comments' permission
self.portal.acl_users._doAddUser(
'reviewer', 'secret', ['Reviewer'], [])
login(self.portal, 'reviewer')
self.portal.acl_users._doAddUser("reviewer", "secret", ["Reviewer"], [])
login(self.portal, "reviewer")
self.assertTrue(self.viewlet.can_manage())
def test_is_discussion_allowed(self):
@ -519,46 +548,48 @@ class TestCommentsViewlet(unittest.TestCase):
self.assertTrue(self.viewlet.comment_transform_message())
self.assertEqual(
self.viewlet.comment_transform_message(),
'You can add a comment by filling out the form below. Plain ' +
'text formatting.')
"You can add a comment by filling out the form below. Plain "
+ "text formatting.",
)
# Set text transform to intelligent text
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
settings.text_transform = 'text/x-web-intelligent'
settings.text_transform = "text/x-web-intelligent"
# Make sure the comment description changes accordingly
self.assertEqual(
self.viewlet.comment_transform_message(),
'You can add a comment by filling out the form below. ' +
'Plain text formatting. Web and email addresses are transformed ' +
'into clickable links.',
"You can add a comment by filling out the form below. "
+ "Plain text formatting. Web and email addresses are transformed "
+ "into clickable links.",
)
# Enable moderation workflow
self.workflowTool.setChainForPortalTypes(
('Discussion Item',),
('comment_review_workflow,'))
("Discussion Item",), ("comment_review_workflow,")
)
# Make sure the comment description shows that comments are moderated
self.assertEqual(
self.viewlet.comment_transform_message(),
'You can add a comment by filling out the form below. ' +
'Plain text formatting. Web and email addresses are transformed ' +
'into clickable links. Comments are moderated.')
"You can add a comment by filling out the form below. "
+ "Plain text formatting. Web and email addresses are transformed "
+ "into clickable links. Comments are moderated.",
)
def test_has_replies(self):
self.assertEqual(self.viewlet.has_replies(), False)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
conversation = IConversation(self.portal.doc1)
conversation.addComment(comment)
self.assertEqual(self.viewlet.has_replies(), True)
def test_get_replies(self):
self.assertFalse(self.viewlet.get_replies())
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
conversation = IConversation(self.portal.doc1)
conversation.addComment(comment)
conversation.addComment(comment)
@ -571,7 +602,7 @@ class TestCommentsViewlet(unittest.TestCase):
next(replies)
def test_get_replies_on_non_annotatable_object(self):
context = self.portal.MailHost # the mail host is not annotatable
context = self.portal.MailHost # the mail host is not annotatable
viewlet = CommentsViewlet(context, self.request, None, None)
replies = viewlet.get_replies()
self.assertEqual(len(tuple(replies)), 0)
@ -581,8 +612,8 @@ class TestCommentsViewlet(unittest.TestCase):
def test_get_replies_with_workflow_actions(self):
self.assertFalse(self.viewlet.get_replies(workflow_actions=True))
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
conversation = IConversation(self.portal.doc1)
c1 = conversation.addComment(comment)
self.assertEqual(
@ -591,32 +622,34 @@ class TestCommentsViewlet(unittest.TestCase):
)
# Enable moderation workflow
self.workflowTool.setChainForPortalTypes(
('Discussion Item',),
('comment_review_workflow,'),
("Discussion Item",),
("comment_review_workflow,"),
)
# Check if workflow actions are available
reply = next(self.viewlet.get_replies(workflow_actions=True))
self.assertTrue('actions' in reply)
self.assertTrue("actions" in reply)
self.assertEqual(
reply['actions'][0]['id'],
'mark_as_spam',
reply["actions"][0]["id"],
"mark_as_spam",
)
expected_url = (
"http://nohost/plone/doc1/++conversation++default/{0}"
"/content_status_modify?workflow_action=mark_as_spam"
)
expected_url = 'http://nohost/plone/doc1/++conversation++default/{0}' \
'/content_status_modify?workflow_action=mark_as_spam'
self.assertEqual(
reply['actions'][0]['url'],
reply["actions"][0]["url"],
expected_url.format(int(c1)),
)
def test_get_commenter_home_url(self):
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
IConversation(self.portal.doc1)
portal_membership = getToolByName(self.portal, 'portal_membership')
portal_membership = getToolByName(self.portal, "portal_membership")
m = portal_membership.getAuthenticatedMember()
self.assertEqual(
self.viewlet.get_commenter_home_url(m.getUserName()),
'http://nohost/plone/author/test-user',
"http://nohost/plone/author/test-user",
)
def test_get_commenter_home_url_is_none(self):
@ -625,72 +658,77 @@ class TestCommentsViewlet(unittest.TestCase):
def test_get_commenter_portrait(self):
# Add a user with a member image
self.membershipTool.addMember('jim', 'Jim', ['Member'], [])
self.memberdata._setPortrait(Image(
id='jim',
file=dummy.File(),
title='',
), 'jim')
self.assertEqual(
self.memberdata._getPortrait('jim').getId(),
'jim',
self.membershipTool.addMember("jim", "Jim", ["Member"], [])
self.memberdata._setPortrait(
Image(
id="jim",
file=DummyFile(),
title="",
),
"jim",
)
self.assertEqual(
self.memberdata._getPortrait('jim').meta_type,
'Image',
self.memberdata._getPortrait("jim").getId(),
"jim",
)
self.assertEqual(
self.memberdata._getPortrait("jim").meta_type,
"Image",
)
# Add a conversation with a comment
conversation = IConversation(self.portal.doc1)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment.Creator = 'Jim'
comment.author_username = 'jim'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment.Creator = "Jim"
comment.author_username = "jim"
conversation.addComment(comment)
# Call get_commenter_portrait method of the viewlet
self.viewlet.update()
portrait_url = self.viewlet.get_commenter_portrait('jim')
portrait_url = self.viewlet.get_commenter_portrait("jim")
# Check if the correct member image URL is returned
self.assertEqual(
portrait_url,
'http://nohost/plone/portal_memberdata/portraits/jim',
"http://nohost/plone/portal_memberdata/portraits/jim",
)
def test_get_commenter_portrait_is_none(self):
self.assertTrue(
self.viewlet.get_commenter_portrait() in (
'defaultUser.png',
'defaultUser.gif',
self.viewlet.get_commenter_portrait()
in (
"defaultUser.png",
"defaultUser.gif",
),
)
def test_get_commenter_portrait_without_userimage(self):
# Create a user without a user image
self.membershipTool.addMember('jim', 'Jim', ['Member'], [])
self.membershipTool.addMember("jim", "Jim", ["Member"], [])
# Add a conversation with a comment
conversation = IConversation(self.portal.doc1)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment.Creator = 'Jim'
comment.author_username = 'jim'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment.Creator = "Jim"
comment.author_username = "jim"
conversation.addComment(comment)
# Call get_commenter_portrait method of the viewlet
self.viewlet.update()
portrait_url = self.viewlet.get_commenter_portrait('jim')
portrait_url = self.viewlet.get_commenter_portrait("jim")
# Check if the correct default member image URL is returned.
# Note that Products.PlonePAS 4.0.5 and later have .png and
# earlier versions have .gif.
self.assertTrue(
portrait_url in (
'http://nohost/plone/defaultUser.png',
'http://nohost/plone/defaultUser.gif',
portrait_url
in (
"http://nohost/plone/defaultUser.png",
"http://nohost/plone/defaultUser.gif",
),
)
@ -700,8 +738,8 @@ class TestCommentsViewlet(unittest.TestCase):
# Allow anonymous discussion
registry = queryUtility(IRegistry)
registry[
'plone.app.discussion.interfaces.IDiscussionSettings.' +
'anonymous_comments'
"plone.app.discussion.interfaces.IDiscussionSettings."
+ "anonymous_comments"
] = True
# Test if anonymous discussion is allowed for the viewlet
self.assertTrue(self.viewlet.anonymous_discussion_allowed())
@ -710,8 +748,8 @@ class TestCommentsViewlet(unittest.TestCase):
self.assertTrue(self.viewlet.show_commenter_image())
registry = queryUtility(IRegistry)
registry[
'plone.app.discussion.interfaces.IDiscussionSettings.' +
'show_commenter_image'
"plone.app.discussion.interfaces.IDiscussionSettings."
+ "show_commenter_image"
] = False
self.assertFalse(self.viewlet.show_commenter_image())
@ -724,7 +762,7 @@ class TestCommentsViewlet(unittest.TestCase):
self.viewlet.update()
self.assertEqual(
self.viewlet.login_action(),
'http://nohost/plone/login_form?came_from=http%3A//nohost',
"http://nohost/plone/login_form?came_from=http%3A//nohost",
)
def test_format_time(self):
@ -737,9 +775,8 @@ class TestCommentsViewlet(unittest.TestCase):
# a correct utc time that can be used to make datetime set the utc
# time of the local time given above. That way, the time for the
# example below is correct within each time zone, independent of DST
python_time = datetime(
*time.gmtime(time.mktime(python_time.timetuple()))[:7])
python_time = datetime(*time.gmtime(time.mktime(python_time.timetuple()))[:7])
localized_time = self.viewlet.format_time(python_time)
self.assertTrue(
localized_time in ['Feb 01, 2009 11:32 PM', '2009-02-01 23:32'],
localized_time in ["Feb 01, 2009 11:32 PM", "2009-02-01 23:32"],
)

View File

@ -1,11 +1,12 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.interfaces import ICommentAddedEvent
from plone.app.discussion.interfaces import ICommentRemovedEvent
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IReplies
from plone.app.discussion.interfaces import IReplyAddedEvent
from plone.app.discussion.interfaces import IReplyRemovedEvent
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.discussion.testing import ( # noqa
PLONE_APP_DISCUSSION_INTEGRATION_TESTING,
)
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.contentrules.rule.interfaces import IRuleEventType
@ -17,31 +18,33 @@ import unittest
class CommentContentRulesTest(unittest.TestCase):
""" Test custom comments events
"""
"""Test custom comments events"""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
# Setup sandbox
self.portal = self.layer['portal']
self.request = self.layer['request']
self.portal = self.layer["portal"]
self.request = self.layer["request"]
# Setup current user properties
member = self.portal.portal_membership.getMemberById(TEST_USER_ID)
member.setMemberProperties({
'fullname': 'X Manager',
'email': 'xmanager@example.com',
})
member.setMemberProperties(
{
"fullname": "X Manager",
"email": "xmanager@example.com",
}
)
setRoles(self.portal, TEST_USER_ID, ['Manager'])
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.document = self.portal['doc1']
self.document = self.portal["doc1"]
comment = createObject('plone.Comment')
comment.text = 'This is a comment'
comment.author_username = 'jim'
comment.author_name = 'Jim'
comment.author_email = 'jim@example.com'
comment = createObject("plone.Comment")
comment.text = "This is a comment"
comment.author_username = "jim"
comment.author_name = "Jim"
comment.author_email = "jim@example.com"
conversation = IConversation(self.document)
conversation.addComment(comment)
@ -52,58 +55,61 @@ class CommentContentRulesTest(unittest.TestCase):
self.assertTrue(IRuleEventType.providedBy(IReplyRemovedEvent))
def testCommentIdStringSubstitution(self):
comment_id = getAdapter(self.document, IStringSubstitution,
name=u'comment_id')
comment_id = getAdapter(self.document, IStringSubstitution, name="comment_id")
self.assertIsInstance(comment_id(), int)
def testCommentTextStringSubstitution(self):
comment_text = getAdapter(self.document, IStringSubstitution,
name=u'comment_text')
self.assertEqual(comment_text(), u'This is a comment')
comment_text = getAdapter(
self.document, IStringSubstitution, name="comment_text"
)
self.assertEqual(comment_text(), "This is a comment")
def testCommentUserIdStringSubstitution(self):
comment_user_id = getAdapter(self.document, IStringSubstitution,
name=u'comment_user_id')
self.assertEqual(comment_user_id(), u'jim')
comment_user_id = getAdapter(
self.document, IStringSubstitution, name="comment_user_id"
)
self.assertEqual(comment_user_id(), "jim")
def testCommentUserFullNameStringSubstitution(self):
comment_user_fullname = getAdapter(self.document, IStringSubstitution,
name=u'comment_user_fullname')
self.assertEqual(comment_user_fullname(), u'Jim')
comment_user_fullname = getAdapter(
self.document, IStringSubstitution, name="comment_user_fullname"
)
self.assertEqual(comment_user_fullname(), "Jim")
def testCommentUserEmailStringSubstitution(self):
comment_user_email = getAdapter(self.document, IStringSubstitution,
name=u'comment_user_email')
self.assertEqual(comment_user_email(), u'jim@example.com')
comment_user_email = getAdapter(
self.document, IStringSubstitution, name="comment_user_email"
)
self.assertEqual(comment_user_email(), "jim@example.com")
class ReplyContentRulesTest(unittest.TestCase):
""" Test custom comments events
"""
"""Test custom comments events"""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
# Setup sandbox
self.portal = self.layer['portal']
self.request = self.layer['request']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.document = self.portal['doc1']
self.document = self.portal["doc1"]
conversation = IConversation(self.document)
replies = IReplies(conversation)
comment = createObject('plone.Comment')
comment.text = 'This is a comment'
comment = createObject("plone.Comment")
comment.text = "This is a comment"
new_id = replies.addComment(comment)
comment = self.document.restrictedTraverse(
'++conversation++default/{0}'.format(new_id),
f"++conversation++default/{new_id}",
)
re_comment = createObject('plone.Comment')
re_comment.text = 'This is a reply'
re_comment.author_username = 'julia'
re_comment.author_name = 'Juliana'
re_comment.author_email = 'julia@example.com'
re_comment = createObject("plone.Comment")
re_comment.text = "This is a reply"
re_comment.author_username = "julia"
re_comment.author_name = "Juliana"
re_comment.author_email = "julia@example.com"
replies = IReplies(comment)
replies.addComment(re_comment)
@ -112,7 +118,7 @@ class ReplyContentRulesTest(unittest.TestCase):
reply_id = getAdapter(
self.document,
IStringSubstitution,
name=u'comment_id',
name="comment_id",
)
self.assertIsInstance(reply_id(), int)
@ -120,30 +126,30 @@ class ReplyContentRulesTest(unittest.TestCase):
reply_text = getAdapter(
self.document,
IStringSubstitution,
name=u'comment_text',
name="comment_text",
)
self.assertEqual(reply_text(), u'This is a reply')
self.assertEqual(reply_text(), "This is a reply")
def testReplyUserIdStringSubstitution(self):
reply_user_id = getAdapter(
self.document,
IStringSubstitution,
name=u'comment_user_id',
name="comment_user_id",
)
self.assertEqual(reply_user_id(), u'julia')
self.assertEqual(reply_user_id(), "julia")
def testReplyUserFullNameStringSubstitution(self):
reply_user_fullname = getAdapter(
self.document,
IStringSubstitution,
name=u'comment_user_fullname',
name="comment_user_fullname",
)
self.assertEqual(reply_user_fullname(), u'Juliana')
self.assertEqual(reply_user_fullname(), "Juliana")
def testReplyUserEmailStringSubstitution(self):
reply_user_email = getAdapter(
self.document,
IStringSubstitution,
name=u'comment_user_email',
name="comment_user_email",
)
self.assertEqual(reply_user_email(), u'julia@example.com')
self.assertEqual(reply_user_email(), "julia@example.com")

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.discussion.testing import ( # noqa
PLONE_APP_DISCUSSION_INTEGRATION_TESTING,
)
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.registry import Registry
@ -17,8 +18,8 @@ class RegistryTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.registry = Registry()
self.registry.registerInterface(IDiscussionSettings)
@ -29,99 +30,100 @@ class RegistryTest(unittest.TestCase):
def test_discussion_controlpanel_view(self):
view = getMultiAdapter(
(self.portal, self.portal.REQUEST),
name='discussion-controlpanel',
name="discussion-controlpanel",
)
self.assertTrue(view())
def test_discussion_in_controlpanel(self):
# Check if discussion is in the control panel
self.controlpanel = getToolByName(self.portal, 'portal_controlpanel')
self.controlpanel = getToolByName(self.portal, "portal_controlpanel")
self.assertTrue(
'discussion' in [
a.getAction(self)['id']
for a in self.controlpanel.listActions()
],
"discussion"
in [a.getAction(self)["id"] for a in self.controlpanel.listActions()],
)
def test_globally_enabled(self):
# Check globally_enabled record
self.assertTrue('globally_enabled' in IDiscussionSettings)
self.assertTrue("globally_enabled" in IDiscussionSettings)
self.assertEqual(
self.registry[
'plone.app.discussion.interfaces.' +
'IDiscussionSettings.globally_enabled'
"plone.app.discussion.interfaces."
+ "IDiscussionSettings.globally_enabled"
],
False,
)
def test_anonymous_comments(self):
# Check anonymous_comments record
self.assertTrue('anonymous_comments' in IDiscussionSettings)
self.assertTrue("anonymous_comments" in IDiscussionSettings)
self.assertEqual(
self.registry[
'plone.app.discussion.interfaces.' +
'IDiscussionSettings.anonymous_comments'
"plone.app.discussion.interfaces."
+ "IDiscussionSettings.anonymous_comments"
],
False,
)
def test_moderation_enabled(self):
# Check globally_enabled record
self.assertTrue('moderation_enabled' in IDiscussionSettings)
self.assertTrue("moderation_enabled" in IDiscussionSettings)
self.assertEqual(
self.registry[
'plone.app.discussion.interfaces.' +
'IDiscussionSettings.moderation_enabled'
"plone.app.discussion.interfaces."
+ "IDiscussionSettings.moderation_enabled"
],
False,
)
def test_edit_comment_enabled(self):
# Check edit_comment_enabled record
self.assertTrue('edit_comment_enabled' in IDiscussionSettings)
self.assertTrue("edit_comment_enabled" in IDiscussionSettings)
self.assertEqual(
self.registry['plone.app.discussion.interfaces.' +
'IDiscussionSettings.edit_comment_enabled'],
self.registry[
"plone.app.discussion.interfaces."
+ "IDiscussionSettings.edit_comment_enabled"
],
False,
)
def test_delete_own_comment_enabled(self):
# Check delete_own_comment_enabled record
self.assertTrue('delete_own_comment_enabled' in IDiscussionSettings)
self.assertTrue("delete_own_comment_enabled" in IDiscussionSettings)
self.assertEqual(
self.registry['plone.app.discussion.interfaces.' +
'IDiscussionSettings.delete_own_comment_enabled'],
self.registry[
"plone.app.discussion.interfaces."
+ "IDiscussionSettings.delete_own_comment_enabled"
],
False,
)
def test_text_transform(self):
self.assertTrue('text_transform' in IDiscussionSettings)
self.assertTrue("text_transform" in IDiscussionSettings)
self.assertEqual(
self.registry[
'plone.app.discussion.interfaces.' +
'IDiscussionSettings.text_transform'
"plone.app.discussion.interfaces."
+ "IDiscussionSettings.text_transform"
],
'text/plain',
"text/plain",
)
def test_captcha(self):
# Check globally_enabled record
self.assertTrue('captcha' in IDiscussionSettings)
self.assertTrue("captcha" in IDiscussionSettings)
self.assertEqual(
self.registry[
'plone.app.discussion.interfaces.' +
'IDiscussionSettings.captcha'
"plone.app.discussion.interfaces." + "IDiscussionSettings.captcha"
],
'disabled',
"disabled",
)
def test_show_commenter_image(self):
# Check show_commenter_image record
self.assertTrue('show_commenter_image' in IDiscussionSettings)
self.assertTrue("show_commenter_image" in IDiscussionSettings)
self.assertEqual(
self.registry[
'plone.app.discussion.interfaces.' +
'IDiscussionSettings.show_commenter_image'
"plone.app.discussion.interfaces."
+ "IDiscussionSettings.show_commenter_image"
],
True,
)
@ -129,12 +131,12 @@ class RegistryTest(unittest.TestCase):
def test_moderator_notification_enabled(self):
# Check show_commenter_image record
self.assertTrue(
'moderator_notification_enabled' in IDiscussionSettings,
"moderator_notification_enabled" in IDiscussionSettings,
)
self.assertEqual(
self.registry[
'plone.app.discussion.interfaces.' +
'IDiscussionSettings.moderator_notification_enabled'
"plone.app.discussion.interfaces."
+ "IDiscussionSettings.moderator_notification_enabled"
],
False,
)
@ -154,22 +156,22 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
registry = queryUtility(IRegistry)
self.settings = registry.forInterface(IDiscussionSettings, check=False)
def test_moderation_enabled_in_discussion_control_panel_changed(self):
"""Make sure the 'Discussion Item' workflow is changed properly, when
the 'comment_moderation' setting in the discussion control panel
changes.
the 'comment_moderation' setting in the discussion control panel
changes.
"""
# By default the comment_one_state_workflow without moderation is
# enabled
self.assertEqual(
('comment_one_state_workflow',),
("comment_one_state_workflow",),
self.portal.portal_workflow.getChainForPortalType(
'Discussion Item',
"Discussion Item",
),
)
@ -179,32 +181,32 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
# Make sure the comment_review_workflow with moderation enabled is
# enabled
self.assertEqual(
('comment_review_workflow',),
("comment_review_workflow",),
self.portal.portal_workflow.getChainForPortalType(
'Discussion Item',
"Discussion Item",
),
)
# And back
self.settings.moderation_enabled = False
self.assertEqual(
('comment_one_state_workflow',),
("comment_one_state_workflow",),
self.portal.portal_workflow.getChainForPortalType(
'Discussion Item',
"Discussion Item",
),
)
def test_change_workflow_in_types_control_panel(self):
"""Make sure the setting in the discussion control panel is changed
accordingly, when the workflow for the 'Discussion Item' changed in
the types control panel.
accordingly, when the workflow for the 'Discussion Item' changed in
the types control panel.
"""
# By default, moderation is disabled
self.settings.moderation_enabled = False
# Enable the 'comment_review_workflow' with moderation enabled
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',),
('comment_review_workflow',),
("Discussion Item",),
("comment_review_workflow",),
)
# Make sure the moderation_enabled settings has changed
@ -212,15 +214,15 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
# Enable the 'comment_review_workflow' with moderation enabled
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',),
('comment_one_state_workflow',),
("Discussion Item",),
("comment_one_state_workflow",),
)
self.settings.moderation_enabled = True
# Enable a 'custom' discussion workflow
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',),
('intranet_workflow',),
("Discussion Item",),
("intranet_workflow",),
)
# Setting has not changed. A Custom workflow disables the

View File

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
from ..interfaces import IComment
from ..interfaces import IConversation
from ..interfaces import IDiscussionLayer
from ..interfaces import IDiscussionSettings
from ..interfaces import IReplies
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from Acquisition import aq_base
from Acquisition import aq_parent
from datetime import datetime
from datetime import timedelta
from plone.app.discussion import interfaces
from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.interfaces import IReplies
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.app.vocabularies.types import BAD_TYPES
from plone.dexterity.interfaces import IDexterityContent
from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import getToolByName
from zope import interface
@ -19,31 +19,22 @@ from zope.annotation.interfaces import IAnnotations
from zope.component import createObject
from zope.component import queryUtility
import six
import unittest
try:
from plone.dexterity.interfaces import IDexterityContent
DEXTERITY = True
except ImportError:
DEXTERITY = False
class ConversationTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
interface.alsoProvides(
self.portal.REQUEST, interfaces.IDiscussionLayer)
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
interface.alsoProvides(self.portal.REQUEST, IDiscussionLayer)
self.typetool = self.portal.portal_types
self.portal_discussion = getToolByName(
self.portal,
'portal_discussion',
"portal_discussion",
None,
)
# Allow discussion
@ -52,7 +43,7 @@ class ConversationTest(unittest.TestCase):
settings.globally_enabled = True
workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish')
workflow.doActionFor(self.portal.doc1, "publish")
def test_add_comment(self):
# Create a conversation. In this case we doesn't assign it to an
@ -62,8 +53,8 @@ class ConversationTest(unittest.TestCase):
# Add a comment. Note: in real life, we always create comments via the
# factory to allow different factories to be swapped in
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
new_id = conversation.addComment(comment)
@ -79,20 +70,19 @@ class ConversationTest(unittest.TestCase):
self.assertEqual(len(tuple(conversation.getThreads())), 1)
self.assertEqual(conversation.total_comments(), 1)
self.assertTrue(
conversation.last_comment_date - datetime.utcnow() <
timedelta(seconds=1),
conversation.last_comment_date - datetime.utcnow() < timedelta(seconds=1),
)
def test_private_comment(self):
conversation = IConversation(self.portal.doc1)
comment = createObject('plone.Comment')
comment.author_username = 'nobody'
comment = createObject("plone.Comment")
comment.author_username = "nobody"
conversation.addComment(comment)
comment.manage_permission('View', roles=tuple())
comment.manage_permission("View", roles=tuple())
self.assertEqual(0, conversation.total_comments())
self.assertEqual(None, conversation.last_comment_date)
self.assertEqual(['nobody'], list(conversation.commentators))
self.assertEqual(["nobody"], list(conversation.commentators))
self.assertEqual([], list(conversation.public_commentators))
def test_delete_comment(self):
@ -103,8 +93,8 @@ class ConversationTest(unittest.TestCase):
# Add a comment. Note: in real life, we always create comments via the
# factory to allow different factories to be swapped in
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
new_id = conversation.addComment(comment)
@ -139,23 +129,23 @@ class ConversationTest(unittest.TestCase):
# +- Comment 2_1
# Create all comments
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
comment1_1 = createObject('plone.Comment')
comment1_1.text = 'Comment text'
comment1_1 = createObject("plone.Comment")
comment1_1.text = "Comment text"
comment1_1_1 = createObject('plone.Comment')
comment1_1_1.text = 'Comment text'
comment1_1_1 = createObject("plone.Comment")
comment1_1_1.text = "Comment text"
comment1_2 = createObject('plone.Comment')
comment1_2.text = 'Comment text'
comment1_2 = createObject("plone.Comment")
comment1_2.text = "Comment text"
comment2 = createObject('plone.Comment')
comment2.text = 'Comment text'
comment2 = createObject("plone.Comment")
comment2.text = "Comment text"
comment2_1 = createObject('plone.Comment')
comment2_1.text = 'Comment text'
comment2_1 = createObject("plone.Comment")
comment2_1.text = "Comment text"
# Create the nested comment structure
new_id_1 = conversation.addComment(comment1)
@ -175,21 +165,24 @@ class ConversationTest(unittest.TestCase):
del conversation[new_id_1]
self.assertEqual([
{'comment': comment2, 'depth': 0, 'id': new_id_2},
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1},
], list(conversation.getThreads()))
self.assertEqual(
[
{"comment": comment2, "depth": 0, "id": new_id_2},
{"comment": comment2_1, "depth": 1, "id": new_id_2_1},
],
list(conversation.getThreads()),
)
def test_delete_comment_when_content_object_is_deleted(self):
# Make sure all comments of a content object are deleted when the
# object itself is deleted.
conversation = IConversation(self.portal.doc1)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
conversation.addComment(comment)
# Delete the content object
self.portal.manage_delObjects(['doc1'])
self.portal.manage_delObjects(["doc1"])
# Make sure the comment has been deleted as well
self.assertEqual(len(list(conversation.getComments())), 0)
@ -198,8 +191,8 @@ class ConversationTest(unittest.TestCase):
def test_comments_enabled_on_doc_in_subfolder(self):
typetool = self.portal.portal_types
typetool.constructContent('Folder', self.portal, 'folder1')
typetool.constructContent('Document', self.portal.folder1, 'doc2')
typetool.constructContent("Folder", self.portal, "folder1")
typetool.constructContent("Document", self.portal.folder1, "doc2")
folder = self.portal.folder1
@ -209,13 +202,13 @@ class ConversationTest(unittest.TestCase):
self.assertFalse(aq_base(folder).allow_discussion)
doc = self.portal.folder1.doc2
conversation = doc.restrictedTraverse('@@conversation_view')
conversation = doc.restrictedTraverse("@@conversation_view")
self.assertEqual(conversation.enabled(), False)
# We have to allow discussion on Document content type, since
# otherwise allow_discussion will always return False
portal_types = getToolByName(self.portal, 'portal_types')
document_fti = getattr(portal_types, 'Document')
portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, "Document")
document_fti.manage_changeProperties(allow_discussion=True)
self.assertEqual(conversation.enabled(), True)
@ -223,13 +216,12 @@ class ConversationTest(unittest.TestCase):
def test_disable_commenting_globally(self):
# Create a conversation.
conversation = self.portal.doc1.restrictedTraverse(
'@@conversation_view')
conversation = self.portal.doc1.restrictedTraverse("@@conversation_view")
# We have to allow discussion on Document content type, since
# otherwise allow_discussion will always return False
portal_types = getToolByName(self.portal, 'portal_types')
document_fti = getattr(portal_types, 'Document')
portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, "Document")
document_fti.manage_changeProperties(allow_discussion=True)
# Check if conversation is enabled now
@ -249,14 +241,14 @@ class ConversationTest(unittest.TestCase):
def test_allow_discussion_for_news_items(self):
self.typetool.constructContent('News Item', self.portal, 'newsitem')
self.typetool.constructContent("News Item", self.portal, "newsitem")
newsitem = self.portal.newsitem
conversation = newsitem.restrictedTraverse('@@conversation_view')
conversation = newsitem.restrictedTraverse("@@conversation_view")
# We have to allow discussion on Document content type, since
# otherwise allow_discussion will always return False
portal_types = getToolByName(self.portal, 'portal_types')
document_fti = getattr(portal_types, 'News Item')
portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, "News Item")
document_fti.manage_changeProperties(allow_discussion=True)
# Check if conversation is enabled now
@ -278,23 +270,23 @@ class ConversationTest(unittest.TestCase):
# Create a conversation.
conversation = self.portal.doc1.restrictedTraverse(
'@@conversation_view',
"@@conversation_view",
)
# The Document content type is disabled by default
self.assertEqual(conversation.enabled(), False)
# Allow discussion on Document content type
portal_types = getToolByName(self.portal, 'portal_types')
document_fti = getattr(portal_types, 'Document')
portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, "Document")
document_fti.manage_changeProperties(allow_discussion=True)
# Check if conversation is enabled now
self.assertEqual(conversation.enabled(), True)
# Disallow discussion on Document content type
portal_types = getToolByName(self.portal, 'portal_types')
document_fti = getattr(portal_types, 'Document')
portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, "Document")
document_fti.manage_changeProperties(allow_discussion=False)
# Check if conversation is enabled now
@ -306,17 +298,17 @@ class ConversationTest(unittest.TestCase):
# plone.app.contenttypes does not have this restriction any longer.
# Create a folder
self.typetool.constructContent('Folder', self.portal, 'f1')
self.typetool.constructContent("Folder", self.portal, "f1")
# Usually we don't create a conversation on a folder
conversation = self.portal.f1.restrictedTraverse('@@conversation_view')
conversation = self.portal.f1.restrictedTraverse("@@conversation_view")
# Allow discussion for the folder
self.portal.f1.allow_discussion = True
# Allow discussion on Folder content type
portal_types = getToolByName(self.portal, 'portal_types')
document_fti = getattr(portal_types, 'Folder')
portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, "Folder")
document_fti.manage_changeProperties(allow_discussion=True)
self.assertTrue(conversation.enabled())
@ -326,7 +318,7 @@ class ConversationTest(unittest.TestCase):
# Create a conversation.
conversation = self.portal.doc1.restrictedTraverse(
'@@conversation_view',
"@@conversation_view",
)
# Discussion is disallowed by default
@ -351,13 +343,13 @@ class ConversationTest(unittest.TestCase):
# Add a comment. Note: in real life, we always create comments via the
# factory to allow different factories to be swapped in
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
new_id1 = conversation.addComment(comment1)
comment2 = createObject('plone.Comment')
comment2.text = 'Comment text'
comment2 = createObject("plone.Comment")
comment2.text = "Comment text"
new_id2 = conversation.addComment(comment2)
@ -384,17 +376,17 @@ class ConversationTest(unittest.TestCase):
self.assertTrue(comment2 in conversation.values())
# check if comment ids are in iterkeys
self.assertTrue(new_id1 in six.iterkeys(conversation))
self.assertTrue(new_id2 in six.iterkeys(conversation))
self.assertFalse(123 in six.iterkeys(conversation))
self.assertTrue(new_id1 in conversation.keys())
self.assertTrue(new_id2 in conversation.keys())
self.assertFalse(123 in conversation.keys())
# check if comment objects are in itervalues
self.assertTrue(comment1 in six.itervalues(conversation))
self.assertTrue(comment2 in six.itervalues(conversation))
self.assertTrue(comment1 in conversation.values())
self.assertTrue(comment2 in conversation.values())
# check if iteritems returns (key, comment object) pairs
self.assertTrue((new_id1, comment1) in six.iteritems(conversation))
self.assertTrue((new_id2, comment2) in six.iteritems(conversation))
self.assertTrue((new_id1, comment1) in conversation.items())
self.assertTrue((new_id2, comment2) in conversation.items())
# TODO test acquisition wrapping # noqa T000
# self.assertTrue(aq_base(aq_parent(comment1)) is conversation)
@ -408,14 +400,14 @@ class ConversationTest(unittest.TestCase):
# comments via the factory to allow different factories to be
# swapped in
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
comment2 = createObject('plone.Comment')
comment2.text = 'Comment text'
comment2 = createObject("plone.Comment")
comment2.text = "Comment text"
comment3 = createObject('plone.Comment')
comment3.text = 'Comment text'
comment3 = createObject("plone.Comment")
comment3.text = "Comment text"
conversation.addComment(comment1)
conversation.addComment(comment2)
@ -437,49 +429,49 @@ class ConversationTest(unittest.TestCase):
# Note: in real life, we always create
# comments via the factory to allow different factories to be
# swapped in
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1.author_username = 'Jim'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
comment1.author_username = "Jim"
conversation.addComment(comment1)
comment2 = createObject('plone.Comment')
comment2.text = 'Comment text'
comment2.author_username = 'Joe'
comment2 = createObject("plone.Comment")
comment2.text = "Comment text"
comment2.author_username = "Joe"
conversation.addComment(comment2)
comment3 = createObject('plone.Comment')
comment3.text = 'Comment text'
comment3.author_username = 'Jack'
comment3 = createObject("plone.Comment")
comment3.text = "Comment text"
comment3.author_username = "Jack"
new_comment3_id = conversation.addComment(comment3)
comment4 = createObject('plone.Comment')
comment4.text = 'Comment text'
comment4.author_username = 'Jack'
comment4 = createObject("plone.Comment")
comment4.text = "Comment text"
comment4.author_username = "Jack"
new_comment4_id = conversation.addComment(comment4)
# check if all commentators are in the commentators list
self.assertEqual(conversation.total_comments(), 4)
self.assertTrue('Jim' in conversation.commentators)
self.assertTrue('Joe' in conversation.commentators)
self.assertTrue('Jack' in conversation.commentators)
self.assertTrue("Jim" in conversation.commentators)
self.assertTrue("Joe" in conversation.commentators)
self.assertTrue("Jack" in conversation.commentators)
# remove the comment from Jack
del conversation[new_comment3_id]
# check if Jack is still in the commentators list (since
# he had added two comments)
self.assertTrue('Jim' in conversation.commentators)
self.assertTrue('Joe' in conversation.commentators)
self.assertTrue('Jack' in conversation.commentators)
self.assertTrue("Jim" in conversation.commentators)
self.assertTrue("Joe" in conversation.commentators)
self.assertTrue("Jack" in conversation.commentators)
self.assertEqual(conversation.total_comments(), 3)
# remove the second comment from Jack
del conversation[new_comment4_id]
# check if Jack has been removed from the commentators list
self.assertTrue('Jim' in conversation.commentators)
self.assertTrue('Joe' in conversation.commentators)
self.assertFalse('Jack' in conversation.commentators)
self.assertTrue("Jim" in conversation.commentators)
self.assertTrue("Joe" in conversation.commentators)
self.assertFalse("Jack" in conversation.commentators)
self.assertEqual(conversation.total_comments(), 2)
def test_last_comment_date(self):
@ -494,29 +486,29 @@ class ConversationTest(unittest.TestCase):
# Note: in real life, we always create
# comments via the factory to allow different factories to be
# swapped in
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
comment1.creation_date = datetime.utcnow() - timedelta(4)
conversation.addComment(comment1)
comment2 = createObject('plone.Comment')
comment2.text = 'Comment text'
comment2 = createObject("plone.Comment")
comment2.text = "Comment text"
comment2.creation_date = datetime.utcnow() - timedelta(2)
new_comment2_id = conversation.addComment(comment2)
comment3 = createObject('plone.Comment')
comment3.text = 'Comment text'
comment3 = createObject("plone.Comment")
comment3.text = "Comment text"
comment3.creation_date = datetime.utcnow() - timedelta(1)
new_comment3_id = conversation.addComment(comment3)
# check if the latest comment is exactly one day old
self.assertTrue(
conversation.last_comment_date < datetime.utcnow() -
timedelta(hours=23, minutes=59, seconds=59),
conversation.last_comment_date
< datetime.utcnow() - timedelta(hours=23, minutes=59, seconds=59),
)
self.assertTrue(
conversation.last_comment_date >
datetime.utcnow() - timedelta(days=1, seconds=1),
conversation.last_comment_date
> datetime.utcnow() - timedelta(days=1, seconds=1),
)
# remove the latest comment
@ -525,12 +517,12 @@ class ConversationTest(unittest.TestCase):
# check if the latest comment has been updated
# the latest comment should be exactly two days old
self.assertTrue(
conversation.last_comment_date < datetime.utcnow() -
timedelta(days=1, hours=23, minutes=59, seconds=59),
conversation.last_comment_date
< datetime.utcnow() - timedelta(days=1, hours=23, minutes=59, seconds=59),
)
self.assertTrue(
conversation.last_comment_date > datetime.utcnow() -
timedelta(days=2, seconds=1),
conversation.last_comment_date
> datetime.utcnow() - timedelta(days=2, seconds=1),
)
# remove the latest comment again
@ -539,12 +531,12 @@ class ConversationTest(unittest.TestCase):
# check if the latest comment has been updated
# the latest comment should be exactly four days old
self.assertTrue(
conversation.last_comment_date < datetime.utcnow() -
timedelta(days=3, hours=23, minutes=59, seconds=59),
conversation.last_comment_date
< datetime.utcnow() - timedelta(days=3, hours=23, minutes=59, seconds=59),
)
self.assertTrue(
conversation.last_comment_date > datetime.utcnow() -
timedelta(days=4, seconds=2),
conversation.last_comment_date
> datetime.utcnow() - timedelta(days=4, seconds=2),
)
def test_get_comments_full(self):
@ -572,23 +564,23 @@ class ConversationTest(unittest.TestCase):
# +- Comment 2_1
# Create all comments
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
comment1_1 = createObject('plone.Comment')
comment1_1.text = 'Comment text'
comment1_1 = createObject("plone.Comment")
comment1_1.text = "Comment text"
comment1_1_1 = createObject('plone.Comment')
comment1_1_1.text = 'Comment text'
comment1_1_1 = createObject("plone.Comment")
comment1_1_1.text = "Comment text"
comment1_2 = createObject('plone.Comment')
comment1_2.text = 'Comment text'
comment1_2 = createObject("plone.Comment")
comment1_2.text = "Comment text"
comment2 = createObject('plone.Comment')
comment2.text = 'Comment text'
comment2 = createObject("plone.Comment")
comment2.text = "Comment text"
comment2_1 = createObject('plone.Comment')
comment2_1.text = 'Comment text'
comment2_1 = createObject("plone.Comment")
comment2_1.text = "Comment text"
# Create the nested comment structure
new_id_1 = conversation.addComment(comment1)
@ -608,14 +600,17 @@ class ConversationTest(unittest.TestCase):
# Get threads
self.assertEqual([
{'comment': comment1, 'depth': 0, 'id': new_id_1},
{'comment': comment1_1, 'depth': 1, 'id': new_id_1_1},
{'comment': comment1_1_1, 'depth': 2, 'id': new_id_1_1_1},
{'comment': comment1_2, 'depth': 1, 'id': new_id_1_2},
{'comment': comment2, 'depth': 0, 'id': new_id_2},
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1},
], list(conversation.getThreads()))
self.assertEqual(
[
{"comment": comment1, "depth": 0, "id": new_id_1},
{"comment": comment1_1, "depth": 1, "id": new_id_1_1},
{"comment": comment1_1_1, "depth": 2, "id": new_id_1_1_1},
{"comment": comment1_2, "depth": 1, "id": new_id_1_2},
{"comment": comment2, "depth": 0, "id": new_id_2},
{"comment": comment2_1, "depth": 1, "id": new_id_2_1},
],
list(conversation.getThreads()),
)
def test_get_threads_batched(self):
# TODO: test start, size, root and depth arguments to getThreads() # noqa T000
@ -626,16 +621,16 @@ class ConversationTest(unittest.TestCase):
# make sure we can traverse to conversations and get a URL and path
conversation = self.portal.doc1.restrictedTraverse(
'++conversation++default',
"++conversation++default",
)
self.assertTrue(IConversation.providedBy(conversation))
self.assertEqual(
('', 'plone', 'doc1', '++conversation++default'),
("", "plone", "doc1", "++conversation++default"),
conversation.getPhysicalPath(),
)
self.assertEqual(
'http://nohost/plone/doc1/++conversation++default',
"http://nohost/plone/doc1/++conversation++default",
conversation.absolute_url(),
)
@ -644,7 +639,7 @@ class ConversationTest(unittest.TestCase):
# can't be converted to int
conversation = self.portal.doc1.restrictedTraverse(
'++conversation++default/ThisCantBeRight',
"++conversation++default/ThisCantBeRight",
)
self.assertEqual(conversation, None)
@ -658,17 +653,16 @@ class ConversationTest(unittest.TestCase):
self.assertTrue(conversation.__parent__)
self.assertTrue(aq_parent(conversation))
self.assertEqual(conversation.__parent__.getId(), 'doc1')
self.assertEqual(conversation.__parent__.getId(), "doc1")
def test_discussion_item_not_in_bad_types(self):
self.assertFalse('Discussion Item' in BAD_TYPES)
self.assertFalse("Discussion Item" in BAD_TYPES)
def test_no_comment(self):
IConversation(self.portal.doc1)
# Make sure no conversation has been created
self.assertTrue(
'plone.app.discussion:conversation' not in
IAnnotations(self.portal.doc1),
"plone.app.discussion:conversation" not in IAnnotations(self.portal.doc1),
)
@ -677,21 +671,20 @@ class ConversationEnabledForDexterityTypesTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
interface.alsoProvides(
self.portal.REQUEST,
interfaces.IDiscussionLayer,
IDiscussionLayer,
)
if DEXTERITY:
interface.alsoProvides(
self.portal.doc1,
IDexterityContent,
)
interface.alsoProvides(
self.portal.doc1,
IDexterityContent,
)
def _makeOne(self, *args, **kw):
return self.portal.doc1.restrictedTraverse('@@conversation_view')
return self.portal.doc1.restrictedTraverse("@@conversation_view")
def _globally_enable_discussion(self, value):
registry = queryUtility(IRegistry)
@ -699,43 +692,38 @@ class ConversationEnabledForDexterityTypesTest(unittest.TestCase):
settings.globally_enabled = value
def _enable_discussion_on_portal_type(self, portal_type, allow_discussion):
portal_types = getToolByName(self.portal, 'portal_types')
portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, portal_type)
document_fti.manage_changeProperties(allow_discussion=allow_discussion)
def test_conversation_is_not_enabled_by_default(self):
if DEXTERITY:
conversation = self._makeOne(self.portal.doc1)
self.assertFalse(conversation.enabled())
conversation = self._makeOne(self.portal.doc1)
self.assertFalse(conversation.enabled())
def test_conversation_is_not_enabled_by_default_on_portal_type(self):
if DEXTERITY:
self._globally_enable_discussion(True)
conversation = self._makeOne(self.portal.doc1)
self.assertFalse(conversation.enabled())
self._globally_enable_discussion(True)
conversation = self._makeOne(self.portal.doc1)
self.assertFalse(conversation.enabled())
def test_conversation_needs_to_be_enabled_globally_and_for_type(self):
if DEXTERITY:
self._globally_enable_discussion(True)
self._enable_discussion_on_portal_type('Document', True)
conversation = self._makeOne(self.portal.doc1)
self.assertTrue(conversation.enabled())
self._globally_enable_discussion(True)
self._enable_discussion_on_portal_type("Document", True)
conversation = self._makeOne(self.portal.doc1)
self.assertTrue(conversation.enabled())
def test_disable_discussion(self):
if DEXTERITY:
self._globally_enable_discussion(True)
self._enable_discussion_on_portal_type('Document', True)
self.portal.doc1.allow_discussion = False
conversation = self._makeOne(self.portal.doc1)
self.assertFalse(conversation.enabled())
self._globally_enable_discussion(True)
self._enable_discussion_on_portal_type("Document", True)
self.portal.doc1.allow_discussion = False
conversation = self._makeOne(self.portal.doc1)
self.assertFalse(conversation.enabled())
def test_enable_discussion(self):
if DEXTERITY:
self._globally_enable_discussion(True)
self._enable_discussion_on_portal_type('Document', True)
self.portal.doc1.allow_discussion = True
conversation = self._makeOne(self.portal.doc1)
self.assertTrue(conversation.enabled())
self._globally_enable_discussion(True)
self._enable_discussion_on_portal_type("Document", True)
self.portal.doc1.allow_discussion = True
conversation = self._makeOne(self.portal.doc1)
self.assertTrue(conversation.enabled())
class RepliesTest(unittest.TestCase):
@ -745,11 +733,11 @@ class RepliesTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish')
workflow.doActionFor(self.portal.doc1, "publish")
def test_add_comment(self):
# Add comments to a ConversationReplies adapter
@ -760,8 +748,8 @@ class RepliesTest(unittest.TestCase):
replies = IReplies(conversation)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
new_id = replies.addComment(comment)
@ -787,8 +775,8 @@ class RepliesTest(unittest.TestCase):
replies = IReplies(conversation)
# Add a comment.
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
new_id = replies.addComment(comment)
@ -826,39 +814,39 @@ class RepliesTest(unittest.TestCase):
# +- Comment 2_1
# Create all comments
comment1 = createObject('plone.Comment')
comment1.text = 'Comment text'
comment1 = createObject("plone.Comment")
comment1.text = "Comment text"
comment1_1 = createObject('plone.Comment')
comment1_1.text = 'Comment text'
comment1_1 = createObject("plone.Comment")
comment1_1.text = "Comment text"
comment1_1_1 = createObject('plone.Comment')
comment1_1_1.text = 'Comment text'
comment1_1_1 = createObject("plone.Comment")
comment1_1_1.text = "Comment text"
comment1_2 = createObject('plone.Comment')
comment1_2.text = 'Comment text'
comment1_2 = createObject("plone.Comment")
comment1_2.text = "Comment text"
comment2 = createObject('plone.Comment')
comment2.text = 'Comment text'
comment2 = createObject("plone.Comment")
comment2.text = "Comment text"
comment2_1 = createObject('plone.Comment')
comment2_1.text = 'Comment text'
comment2_1 = createObject("plone.Comment")
comment2_1.text = "Comment text"
# Create the nested comment structure
new_id_1 = replies.addComment(comment1)
comment1 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_1),
f"++conversation++default/{new_id_1}",
)
replies_to_comment1 = IReplies(comment1)
new_id_2 = replies.addComment(comment2)
comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_2),
f"++conversation++default/{new_id_2}",
)
replies_to_comment2 = IReplies(comment2)
new_id_1_1 = replies_to_comment1.addComment(comment1_1)
comment1_1 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_1_1),
f"++conversation++default/{new_id_1_1}",
)
replies_to_comment1_1 = IReplies(comment1_1)
replies_to_comment1_1.addComment(comment1_1_1)

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IReplies
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from ..interfaces import IConversation
from ..interfaces import IReplies
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from Zope2.App import zcml
@ -18,9 +17,9 @@ import unittest
#
class EventsRegistry(object):
""" Fake registry to be used while testing discussion events
"""
class EventsRegistry:
"""Fake registry to be used while testing discussion events"""
commentAdded = False
commentModified = False
commentRemoved = False
@ -28,6 +27,7 @@ class EventsRegistry(object):
replyModified = False
replyRemoved = False
#
# Fake event handlers
#
@ -63,19 +63,19 @@ def reply_removed(doc, evt):
class CommentEventsTest(unittest.TestCase):
""" Test custom comments events
"""
"""Test custom comments events"""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
# Setup sandbox
self.portal = self.layer['portal']
self.request = self.layer['request']
self.portal = self.layer["portal"]
self.request = self.layer["request"]
self.registry = EventsRegistry
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.document = self.portal['doc1']
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.document = self.portal["doc1"]
#
# Subscribers
@ -104,23 +104,23 @@ class CommentEventsTest(unittest.TestCase):
</configure>
"""
zcml.load_config('configure.zcml', Products.Five)
zcml.load_config("configure.zcml", Products.Five)
zcml.load_string(configure)
def test_addEvent(self):
self.assertFalse(self.registry.commentAdded)
comment = createObject('plone.Comment')
comment = createObject("plone.Comment")
conversation = IConversation(self.document)
conversation.addComment(comment)
self.assertTrue(self.registry.commentAdded)
def test_modifyEvent(self):
self.assertFalse(self.registry.commentModified)
comment = createObject('plone.Comment')
comment = createObject("plone.Comment")
conversation = IConversation(self.document)
new_id = conversation.addComment(comment)
comment = self.document.restrictedTraverse(
'++conversation++default/{0}'.format(new_id),
f"++conversation++default/{new_id}",
)
comment.text = "foo"
notify(ObjectModifiedEvent(comment))
@ -128,7 +128,7 @@ class CommentEventsTest(unittest.TestCase):
def test_removedEvent(self):
self.assertFalse(self.registry.commentRemoved)
comment = createObject('plone.Comment')
comment = createObject("plone.Comment")
conversation = IConversation(self.document)
cid = conversation.addComment(comment)
del conversation[cid]
@ -136,17 +136,17 @@ class CommentEventsTest(unittest.TestCase):
class RepliesEventsTest(unittest.TestCase):
""" Test custom replies events
"""
"""Test custom replies events"""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
self.request = self.layer['request']
self.portal = self.layer["portal"]
self.request = self.layer["request"]
self.registry = EventsRegistry
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.document = self.portal['doc1']
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.document = self.portal["doc1"]
#
# Subscribers
@ -175,7 +175,7 @@ class RepliesEventsTest(unittest.TestCase):
</configure>
"""
zcml.load_config('configure.zcml', Products.Five)
zcml.load_config("configure.zcml", Products.Five)
zcml.load_string(configure)
def test_addEvent(self):
@ -184,15 +184,15 @@ class RepliesEventsTest(unittest.TestCase):
conversation = IConversation(self.document)
replies = IReplies(conversation)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
new_id = replies.addComment(comment)
comment = self.document.restrictedTraverse(
'++conversation++default/{0}'.format(new_id),
f"++conversation++default/{new_id}",
)
re_comment = createObject('plone.Comment')
re_comment.text = 'Comment text'
re_comment = createObject("plone.Comment")
re_comment.text = "Comment text"
replies = IReplies(comment)
replies.addComment(re_comment)
@ -204,14 +204,14 @@ class RepliesEventsTest(unittest.TestCase):
conversation = IConversation(self.document)
replies = IReplies(conversation)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment_id = replies.addComment(comment)
comment = self.document.restrictedTraverse(
'++conversation++default/{0}'.format(comment_id),
f"++conversation++default/{comment_id}",
)
re_comment = createObject('plone.Comment')
re_comment.text = 'Comment text'
re_comment = createObject("plone.Comment")
re_comment.text = "Comment text"
replies = IReplies(comment)
new_id = replies.addComment(re_comment)
reply = replies[new_id]
@ -225,15 +225,15 @@ class RepliesEventsTest(unittest.TestCase):
conversation = IConversation(self.portal.doc1)
replies = IReplies(conversation)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
new_id = replies.addComment(comment)
comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id),
f"++conversation++default/{new_id}",
)
re_comment = createObject('plone.Comment')
re_comment.text = 'Comment text'
re_comment = createObject("plone.Comment")
re_comment.text = "Comment text"
replies = IReplies(comment)
new_re_id = replies.addComment(re_comment)

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
"""Functional Doctests for plone.app.discussion.
These test are only triggered when Plone 4 (and plone.testing) is installed.
"""
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING # noqa
from ..testing import PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING # noqa
from plone.testing import layered
import doctest
@ -12,29 +11,29 @@ import unittest
optionflags = (
doctest.ELLIPSIS |
doctest.NORMALIZE_WHITESPACE |
doctest.REPORT_ONLY_FIRST_FAILURE
doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_ONLY_FIRST_FAILURE
)
normal_testfiles = [
'functional_test_comments.txt',
'functional_test_comment_review_workflow.txt',
"functional_test_comments.txt",
"functional_test_comment_review_workflow.txt",
]
def test_suite():
suite = unittest.TestSuite()
suite.addTests([
layered(
doctest.DocFileSuite(
test,
optionflags=optionflags,
globs={
'pprint': pprint.pprint,
}
),
layer=PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING,
)
for test in normal_testfiles
])
suite.addTests(
[
layered(
doctest.DocFileSuite(
test,
optionflags=optionflags,
globs={
"pprint": pprint.pprint,
},
),
layer=PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING,
)
for test in normal_testfiles
]
)
return suite

View File

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
"""Test for the plone.app.discussion indexers
"""
from DateTime import DateTime
from .. import catalog
from ..interfaces import IConversation
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from datetime import datetime
from plone.app.discussion import catalog
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from DateTime import DateTime
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.indexer.delegate import DelegatingIndexerFactory
@ -26,41 +25,40 @@ sed diam voluptua. At [...]"""
class ConversationIndexersTest(unittest.TestCase):
"""Conversation Indexer Tests
"""
"""Conversation Indexer Tests"""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish')
workflow.doActionFor(self.portal.doc1, "publish")
# Create a conversation.
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1.text = 'Comment Text'
comment1.creator = 'jim'
comment1.author_username = 'Jim'
comment1 = createObject("plone.Comment")
comment1.text = "Comment Text"
comment1.creator = "jim"
comment1.author_username = "Jim"
comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12)
comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12)
self.new_id1 = conversation.addComment(comment1)
comment2 = createObject('plone.Comment')
comment2.text = 'Comment Text'
comment2.creator = 'emma'
comment2.author_username = 'Emma'
comment2 = createObject("plone.Comment")
comment2.text = "Comment Text"
comment2.creator = "emma"
comment2.author_username = "Emma"
comment2.creation_date = datetime(2007, 12, 13, 4, 18, 12)
comment2.modification_date = datetime(2007, 12, 13, 4, 18, 12)
self.new_id2 = conversation.addComment(comment2)
comment3 = createObject('plone.Comment')
comment3.text = 'Comment Text'
comment3.creator = 'lukas'
comment3.author_username = 'Lukas'
comment3 = createObject("plone.Comment")
comment3.text = "Comment Text"
comment3.creator = "lukas"
comment3.author_username = "Lukas"
comment3.creation_date = datetime(2009, 4, 12, 11, 12, 12)
comment3.modification_date = datetime(2009, 4, 12, 11, 12, 12)
self.new_id3 = conversation.addComment(comment3)
@ -68,10 +66,12 @@ class ConversationIndexersTest(unittest.TestCase):
self.conversation = conversation
def test_conversation_total_comments(self):
self.assertTrue(isinstance(
catalog.total_comments,
DelegatingIndexerFactory,
))
self.assertTrue(
isinstance(
catalog.total_comments,
DelegatingIndexerFactory,
)
)
self.assertEqual(catalog.total_comments(self.portal.doc1)(), 3)
del self.conversation[self.new_id1]
self.assertEqual(catalog.total_comments(self.portal.doc1)(), 2)
@ -80,10 +80,12 @@ class ConversationIndexersTest(unittest.TestCase):
self.assertEqual(catalog.total_comments(self.portal.doc1)(), 0)
def test_conversation_last_comment_date(self):
self.assertTrue(isinstance(
catalog.last_comment_date,
DelegatingIndexerFactory,
))
self.assertTrue(
isinstance(
catalog.last_comment_date,
DelegatingIndexerFactory,
)
)
self.assertEqual(
catalog.last_comment_date(self.portal.doc1)(),
datetime(2009, 4, 12, 11, 12, 12),
@ -110,8 +112,8 @@ class CommentIndexersTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
# Create a conversation. In this case we doesn't assign it to an
# object, as we just want to check the Conversation object API.
@ -120,10 +122,10 @@ class CommentIndexersTest(unittest.TestCase):
# Add a comment. Note: in real life, we always create comments via the
# factory to allow different factories to be swapped in
comment = createObject('plone.Comment')
comment.text = 'Lorem ipsum dolor sit amet.'
comment.creator = 'jim'
comment.author_name = 'Jim'
comment = createObject("plone.Comment")
comment.text = "Lorem ipsum dolor sit amet."
comment.creator = "jim"
comment.author_name = "Jim"
comment.creation_date = datetime(2006, 9, 17, 14, 18, 12)
comment.modification_date = datetime(2008, 3, 12, 7, 32, 52)
@ -132,60 +134,61 @@ class CommentIndexersTest(unittest.TestCase):
self.conversation = conversation
def test_title(self):
self.assertEqual(catalog.title(self.comment)(), 'Jim on Document 1')
self.assertEqual(catalog.title(self.comment)(), "Jim on Document 1")
self.assertTrue(isinstance(catalog.title, DelegatingIndexerFactory))
def test_description(self):
self.assertEqual(
catalog.description(self.comment)(),
'Lorem ipsum dolor sit amet.',
"Lorem ipsum dolor sit amet.",
)
self.assertTrue(
isinstance(catalog.description, DelegatingIndexerFactory))
self.assertTrue(isinstance(catalog.description, DelegatingIndexerFactory))
def test_description_long(self):
# Create a 50 word comment and make sure the description returns
# only the first 25 words
comment_long = createObject('plone.Comment')
comment_long.title = 'Long Comment'
comment_long = createObject("plone.Comment")
comment_long.title = "Long Comment"
comment_long.text = LONG_TEXT
self.conversation.addComment(comment_long)
self.assertEqual(
catalog.description(comment_long)(),
LONG_TEXT_CUT.replace('\n', ' '),
LONG_TEXT_CUT.replace("\n", " "),
)
def test_dates(self):
# Test if created, modified, effective etc. are set correctly
self.assertEqual(
catalog.created(self.comment)(),
DateTime(2006, 9, 17, 14, 18, 12, 'GMT'),
DateTime(2006, 9, 17, 14, 18, 12, "GMT"),
)
self.assertEqual(
catalog.effective(self.comment)(),
DateTime(2006, 9, 17, 14, 18, 12, 'GMT'),
DateTime(2006, 9, 17, 14, 18, 12, "GMT"),
)
self.assertEqual(
catalog.modified(self.comment)(),
DateTime(2008, 3, 12, 7, 32, 52, 'GMT'),
DateTime(2008, 3, 12, 7, 32, 52, "GMT"),
)
def test_searchable_text(self):
# Test if searchable text is a concatenation of title and comment text
self.assertEqual(
catalog.searchable_text(self.comment)(),
('Lorem ipsum dolor sit amet.'),
("Lorem ipsum dolor sit amet."),
)
self.assertTrue(
isinstance(
catalog.searchable_text,
DelegatingIndexerFactory,
)
)
self.assertTrue(isinstance(
catalog.searchable_text,
DelegatingIndexerFactory,
))
def test_creator(self):
self.assertEqual(catalog.creator(self.comment)(), ('jim'))
self.assertEqual(catalog.creator(self.comment)(), ("jim"))
def test_in_response_to(self):
# make sure in_response_to returns the title or id of the content
# object the comment was added to
self.assertEqual(catalog.in_response_to(self.comment)(), 'Document 1')
self.assertEqual(catalog.in_response_to(self.comment)(), "Document 1")

View File

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.browser.moderation import BulkActionsView
from plone.app.discussion.browser.moderation import DeleteComment
from plone.app.discussion.browser.moderation import CommentTransition
from plone.app.discussion.browser.moderation import View
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from ..browser.moderation import BulkActionsView
from ..browser.moderation import CommentTransition
from ..browser.moderation import DeleteComment
from ..browser.moderation import View
from ..interfaces import IConversation
from ..interfaces import IDiscussionSettings
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.registry.interfaces import IRegistry
@ -21,59 +20,57 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.app = self.layer['app']
self.portal = self.layer['portal']
self.request = self.layer['request']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.wf = getToolByName(self.portal,
'portal_workflow',
None)
self.app = self.layer["app"]
self.portal = self.layer["portal"]
self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.wf = getToolByName(self.portal, "portal_workflow", None)
self.context = self.portal
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',),
'comment_review_workflow',
("Discussion Item",),
"comment_review_workflow",
)
self.wf_tool = self.portal.portal_workflow
# Add a conversation with three comments
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1.title = 'Comment 1'
comment1.text = 'Comment text'
comment1.Creator = 'Jim'
comment1 = createObject("plone.Comment")
comment1.title = "Comment 1"
comment1.text = "Comment text"
comment1.Creator = "Jim"
new_id_1 = conversation.addComment(comment1)
self.comment1 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_1),
f"++conversation++default/{new_id_1}",
)
comment2 = createObject('plone.Comment')
comment2.title = 'Comment 2'
comment2.text = 'Comment text'
comment2.Creator = 'Joe'
comment2 = createObject("plone.Comment")
comment2.title = "Comment 2"
comment2.text = "Comment text"
comment2.Creator = "Joe"
new_id_2 = conversation.addComment(comment2)
self.comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_2),
f"++conversation++default/{new_id_2}",
)
comment3 = createObject('plone.Comment')
comment3.title = 'Comment 3'
comment3.text = 'Comment text'
comment3.Creator = 'Emma'
comment3 = createObject("plone.Comment")
comment3.title = "Comment 3"
comment3.text = "Comment text"
comment3.Creator = "Emma"
new_id_3 = conversation.addComment(comment3)
self.comment3 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_3),
f"++conversation++default/{new_id_3}",
)
self.conversation = conversation
def test_default_bulkaction(self):
# Make sure no error is raised when no bulk actions has been supplied
self.request.set('form.select.BulkAction', '-1')
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
self.request.set("form.select.BulkAction", "-1")
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request)
self.assertFalse(view())
def test_publish(self):
self.request.set('form.select.BulkAction', 'publish')
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
self.request.set("form.select.BulkAction", "publish")
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request)
view()
@ -81,16 +78,16 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Count published comments
published_comments = 0
for r in self.conversation.getThreads():
comment_obj = r['comment']
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state')
if workflow_status == 'published':
comment_obj = r["comment"]
workflow_status = self.wf.getInfoFor(comment_obj, "review_state")
if workflow_status == "published":
published_comments += 1
# Make sure the comment has been published
self.assertEqual(published_comments, 1)
def test_mark_as_spam(self):
self.request.set('form.select.BulkAction', 'mark_as_spam')
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
self.request.set("form.select.BulkAction", "mark_as_spam")
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request)
@ -99,9 +96,9 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Count spam comments
spam_comments = 0
for r in self.conversation.getThreads():
comment_obj = r['comment']
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state')
if workflow_status == 'spam':
comment_obj = r["comment"]
workflow_status = self.wf.getInfoFor(comment_obj, "review_state")
if workflow_status == "spam":
spam_comments += 1
# Make sure the comment has been marked as spam
self.assertEqual(spam_comments, 1)
@ -110,9 +107,14 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Initially we have three comments
self.assertEqual(len(self.conversation.objectIds()), 3)
# Delete two comments with bulk actions
self.request.set('form.select.BulkAction', 'delete')
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath()),
'/'.join(self.comment3.getPhysicalPath())])
self.request.set("form.select.BulkAction", "delete")
self.request.set(
"paths",
[
"/".join(self.comment1.getPhysicalPath()),
"/".join(self.comment3.getPhysicalPath()),
],
)
view = BulkActionsView(self.app, self.request)
view()

View File

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.browser.moderation import BulkActionsView
from plone.app.discussion.browser.moderation import DeleteComment
from plone.app.discussion.browser.moderation import CommentTransition
from plone.app.discussion.browser.moderation import View
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from ..browser.moderation import BulkActionsView
from ..browser.moderation import CommentTransition
from ..browser.moderation import DeleteComment
from ..browser.moderation import View
from ..interfaces import IConversation
from ..interfaces import IDiscussionSettings
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.registry.interfaces import IRegistry
@ -21,37 +20,37 @@ class ModerationViewTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.app = self.layer['app']
self.portal = self.layer['portal']
self.request = self.layer['request']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal_discussion = getToolByName(self.portal,
'portal_discussion',
None)
self.membership_tool = getToolByName(self.portal,
'portal_membership')
self.app = self.layer["app"]
self.portal = self.layer["portal"]
self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.portal_discussion = getToolByName(self.portal, "portal_discussion", None)
self.membership_tool = getToolByName(self.portal, "portal_membership")
self.memberdata = self.portal.portal_memberdata
request = self.app.REQUEST
context = getattr(self.portal, 'doc1')
context = getattr(self.portal, "doc1")
self.view = View(context, request)
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), 'comment_review_workflow')
("Discussion Item",), "comment_review_workflow"
)
self.wf_tool = self.portal.portal_workflow
def test_moderation_enabled(self):
"""Make sure that moderation_enabled returns true if the comment
workflow implements a 'pending' state.
workflow implements a 'pending' state.
"""
# If workflow is not set, enabled must return False
self.wf_tool.setChainForPortalTypes(('Discussion Item',), ())
self.wf_tool.setChainForPortalTypes(("Discussion Item",), ())
self.assertEqual(self.view.moderation_enabled(), False)
# The comment_one_state_workflow does not have a 'pending' state
self.wf_tool.setChainForPortalTypes(('Discussion Item',),
('comment_one_state_workflow,'))
self.wf_tool.setChainForPortalTypes(
("Discussion Item",), ("comment_one_state_workflow,")
)
self.assertEqual(self.view.moderation_enabled(), False)
# The comment_review_workflow does have a 'pending' state
self.wf_tool.setChainForPortalTypes(('Discussion Item',),
('comment_review_workflow,'))
self.wf_tool.setChainForPortalTypes(
("Discussion Item",), ("comment_review_workflow,")
)
self.assertEqual(self.view.moderation_enabled(), True)
@ -60,59 +59,57 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.app = self.layer['app']
self.portal = self.layer['portal']
self.request = self.layer['request']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.wf = getToolByName(self.portal,
'portal_workflow',
None)
self.app = self.layer["app"]
self.portal = self.layer["portal"]
self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.wf = getToolByName(self.portal, "portal_workflow", None)
self.context = self.portal
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',),
'comment_review_workflow',
("Discussion Item",),
"comment_review_workflow",
)
self.wf_tool = self.portal.portal_workflow
# Add a conversation with three comments
conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment')
comment1.title = 'Comment 1'
comment1.text = 'Comment text'
comment1.Creator = 'Jim'
comment1 = createObject("plone.Comment")
comment1.title = "Comment 1"
comment1.text = "Comment text"
comment1.Creator = "Jim"
new_id_1 = conversation.addComment(comment1)
self.comment1 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_1),
f"++conversation++default/{new_id_1}",
)
comment2 = createObject('plone.Comment')
comment2.title = 'Comment 2'
comment2.text = 'Comment text'
comment2.Creator = 'Joe'
comment2 = createObject("plone.Comment")
comment2.title = "Comment 2"
comment2.text = "Comment text"
comment2.Creator = "Joe"
new_id_2 = conversation.addComment(comment2)
self.comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_2),
f"++conversation++default/{new_id_2}",
)
comment3 = createObject('plone.Comment')
comment3.title = 'Comment 3'
comment3.text = 'Comment text'
comment3.Creator = 'Emma'
comment3 = createObject("plone.Comment")
comment3.title = "Comment 3"
comment3.text = "Comment text"
comment3.Creator = "Emma"
new_id_3 = conversation.addComment(comment3)
self.comment3 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_3),
f"++conversation++default/{new_id_3}",
)
self.conversation = conversation
def test_default_bulkaction(self):
# Make sure no error is raised when no bulk actions has been supplied
self.request.set('form.select.BulkAction', '-1')
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
self.request.set("form.select.BulkAction", "-1")
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request)
self.assertFalse(view())
def test_publish(self):
self.request.set('form.select.BulkAction', 'publish')
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
self.request.set("form.select.BulkAction", "publish")
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request)
view()
@ -120,9 +117,9 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Count published comments
published_comments = 0
for r in self.conversation.getThreads():
comment_obj = r['comment']
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state')
if workflow_status == 'published':
comment_obj = r["comment"]
workflow_status = self.wf.getInfoFor(comment_obj, "review_state")
if workflow_status == "published":
published_comments += 1
# Make sure the comment has been published
self.assertEqual(published_comments, 1)
@ -131,9 +128,14 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Initially we have three comments
self.assertEqual(len(self.conversation.objectIds()), 3)
# Delete two comments with bulk actions
self.request.set('form.select.BulkAction', 'delete')
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath()),
'/'.join(self.comment3.getPhysicalPath())])
self.request.set("form.select.BulkAction", "delete")
self.request.set(
"paths",
[
"/".join(self.comment1.getPhysicalPath()),
"/".join(self.comment3.getPhysicalPath()),
],
)
view = BulkActionsView(self.app, self.request)
view()
@ -151,41 +153,41 @@ class RedirectionTest(unittest.TestCase):
def setUp(self):
# Update settings.
self.portal = self.layer['portal']
self.request = self.layer['request']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
# applyProfile(self.portal, 'plone.app.discussion:default')
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings)
settings.globally_enabled = True
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',),
('comment_review_workflow',),
("Discussion Item",),
("comment_review_workflow",),
)
# Create page plus comment.
self.portal.invokeFactory(
id='page',
title='Page 1',
type_name='Document',
id="page",
title="Page 1",
type_name="Document",
)
self.page = self.portal.page
self.conversation = IConversation(self.page)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
self.comment_id = self.conversation.addComment(comment)
self.comment = list(self.conversation.getComments())[0]
def test_regression(self):
page_url = self.page.absolute_url()
self.request['HTTP_REFERER'] = page_url
self.request["HTTP_REFERER"] = page_url
for Klass in (DeleteComment, CommentTransition):
view = Klass(self.comment, self.request)
view.__parent__ = self.comment
self.assertEqual(page_url, view())
def test_valid_next_url(self):
self.request['HTTP_REFERER'] = 'http://attacker.com'
self.request["HTTP_REFERER"] = "http://attacker.com"
for Klass in (DeleteComment, CommentTransition):
view = Klass(self.comment, self.request)
view.__parent__ = self.comment
self.assertNotEqual('http://attacker.com', view())
self.assertNotEqual("http://attacker.com", view())

View File

@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
from ..interfaces import IConversation
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from Acquisition import aq_base
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from persistent.list import PersistentList
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.base.interfaces import IMailSchema
from plone.registry.interfaces import IRegistry
from Products.CMFPlone.interfaces import IMailSchema
from Products.CMFPlone.tests.utils import MockMailHost
from Products.MailHost.interfaces import IMailHost
from Products.MailHost.MailHost import _mungeHeaders
from Products.MailHost.MailHost import MailBase
from zope.component import createObject
from zope.component import getSiteManager
from zope.component import getUtility
@ -16,50 +17,85 @@ from zope.component import queryUtility
import unittest
class MockMailHost(MailBase):
"""A MailHost that collects messages instead of sending them."""
def __init__(self, id):
self.reset()
def reset(self):
self.messages = PersistentList()
def _send(self, mfrom, mto, messageText, immediate=False):
"""Send the message"""
self.messages.append(messageText)
def send(
self,
messageText,
mto=None,
mfrom=None,
subject=None,
encode=None,
immediate=False,
charset=None,
msg_type=None,
):
"""send *messageText* modified by the other parameters.
*messageText* can either be an ``email.message.Message``
or a string.
Note that Products.MailHost 4.10 had changes here.
"""
msg, mto, mfrom = _mungeHeaders(
messageText, mto, mfrom, subject, charset, msg_type, encode
)
self.messages.append(msg)
class TestUserNotificationUnit(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
# Set up a mock mailhost
self.portal._original_MailHost = self.portal.MailHost
self.portal.MailHost = mailhost = MockMailHost('MailHost')
self.portal.MailHost = mailhost = MockMailHost("MailHost")
sm = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(mailhost, provided=IMailHost)
# We need to fake a valid mail setup
registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
mail_settings.email_from_address = 'portal@plone.test'
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mail_settings.email_from_address = "portal@plone.test"
self.mailhost = self.portal.MailHost
# Enable user notification setting
registry = queryUtility(IRegistry)
registry['plone.app.discussion.interfaces.IDiscussionSettings' +
'.user_notification_enabled'] = True
# Archetypes content types store data as utf-8 encoded strings
# The missing u in front of a string is therefor not missing
self.portal.doc1.title = 'Kölle Alaaf' # What is 'Fasching'?
registry[
"plone.app.discussion.interfaces.IDiscussionSettings"
+ ".user_notification_enabled"
] = True
self.portal.doc1.title = "Kölle Alaaf" # What is 'Fasching'?
self.conversation = IConversation(self.portal.doc1)
def beforeTearDown(self):
self.portal.MailHost = self.portal._original_MailHost
sm = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(aq_base(self.portal._original_MailHost),
provided=IMailHost)
sm.registerUtility(aq_base(self.portal._original_MailHost), provided=IMailHost)
def test_notify_user(self):
# Add a comment with user notification enabled. Add another comment
# and make sure an email is send to the user of the first comment.
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment.user_notification = True
comment.author_email = 'john@plone.test'
comment.author_email = "john@plone.test"
self.conversation.addComment(comment)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment_id = self.conversation.addComment(comment)
@ -67,52 +103,46 @@ class TestUserNotificationUnit(unittest.TestCase):
self.assertTrue(self.mailhost.messages[0])
msg = self.mailhost.messages[0]
msg = msg.decode("utf-8")
self.assertIn('To: john@plone.test', msg)
self.assertIn('From: portal@plone.test', msg)
self.assertIn("To: john@plone.test", msg)
self.assertIn("From: portal@plone.test", msg)
# We expect the headers to be properly header encoded (7-bit):
self.assertIn(
'Subject: =?utf-8?q?A_comment_has_been_posted=2E?=',
msg)
self.assertIn("Subject: =?utf-8?q?A_comment_has_been_posted=2E?=", msg)
# The output should be encoded in a reasonable manner
# (in this case quoted-printable).
# Depending on which Python version and which Products.MailHost version,
# you may get lines separated by '\n' or '\r\n' in here.
msg = msg.replace('\r\n', '\n')
self.assertIn(
'A comment on "K=C3=B6lle Alaaf" has been posted here:',
msg)
self.assertIn(
'http://nohost/plone/d=\noc1/view#{0}'.format(comment_id),
msg)
self.assertIn('Comment text', msg)
self.assertNotIn('Approve comment', msg)
self.assertNotIn('Delete comment', msg)
msg = msg.replace("\r\n", "\n")
self.assertIn('A comment on "K=C3=B6lle Alaaf" has been posted here:', msg)
self.assertIn(f"http://nohost/plone/d=\noc1/view#{comment_id}", msg)
self.assertIn("Comment text", msg)
self.assertNotIn("Approve comment", msg)
self.assertNotIn("Delete comment", msg)
def test_do_not_notify_user_when_notification_is_disabled(self):
registry = queryUtility(IRegistry)
registry[
'plone.app.discussion.interfaces.IDiscussionSettings.' +
'user_notification_enabled'
"plone.app.discussion.interfaces.IDiscussionSettings."
+ "user_notification_enabled"
] = False
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment.user_notification = True
comment.author_email = 'john@plone.test'
comment.author_email = "john@plone.test"
self.conversation.addComment(comment)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
self.conversation.addComment(comment)
self.assertEqual(len(self.mailhost.messages), 0)
def test_do_not_notify_user_when_email_address_is_given(self):
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment.user_notification = True
self.conversation.addComment(comment)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
self.conversation.addComment(comment)
@ -122,15 +152,15 @@ class TestUserNotificationUnit(unittest.TestCase):
# Set sender mail address to none and make sure no email is send to
# the moderator.
registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mail_settings.email_from_address = None
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment.user_notification = True
comment.author_email = 'john@plone.test'
comment.author_email = "john@plone.test"
self.conversation.addComment(comment)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
self.conversation.addComment(comment)
self.assertEqual(len(self.mailhost.messages), 0)
@ -139,15 +169,15 @@ class TestUserNotificationUnit(unittest.TestCase):
# When a user has added two comments in a conversation and has
# both times requested email notification, do not send him two
# emails when another comment has been added.
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment.user_notification = True
comment.author_email = 'john@plone.test'
comment.author_email = "john@plone.test"
self.conversation.addComment(comment)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment.user_notification = True
comment.author_email = 'john@plone.test'
comment.author_email = "john@plone.test"
self.conversation.addComment(comment)
@ -163,48 +193,45 @@ class TestModeratorNotificationUnit(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
# Set up a mock mailhost
self.portal._original_MailHost = self.portal.MailHost
self.portal.MailHost = mailhost = MockMailHost('MailHost')
self.portal.MailHost = mailhost = MockMailHost("MailHost")
sm = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(mailhost, provided=IMailHost)
# We need to fake a valid mail setup
registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
mail_settings.email_from_address = 'portal@plone.test'
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mail_settings.email_from_address = "portal@plone.test"
self.mailhost = self.portal.MailHost
# Enable comment moderation
self.portal.portal_types['Document'].allow_discussion = True
self.portal.portal_types["Document"].allow_discussion = True
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',),
('comment_review_workflow',),
("Discussion Item",),
("comment_review_workflow",),
)
# Enable moderator notification setting
registry = queryUtility(IRegistry)
registry[
'plone.app.discussion.interfaces.IDiscussionSettings.' +
'moderator_notification_enabled'
"plone.app.discussion.interfaces.IDiscussionSettings."
+ "moderator_notification_enabled"
] = True
# Archetypes content types store data as utf-8 encoded strings
# The missing u in front of a string is therefor not missing
self.portal.doc1.title = 'Kölle Alaaf' # What is 'Fasching'?
self.portal.doc1.title = "Kölle Alaaf" # What is 'Fasching'?
self.conversation = IConversation(self.portal.doc1)
def beforeTearDown(self):
self.portal.MailHost = self.portal._original_MailHost
sm = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(aq_base(self.portal._original_MailHost),
provided=IMailHost)
sm.registerUtility(aq_base(self.portal._original_MailHost), provided=IMailHost)
def test_notify_moderator(self):
"""Add a comment and make sure an email is send to the moderator."""
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment.author_email = 'john@plone.test'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment.author_email = "john@plone.test"
comment_id = self.conversation.addComment(comment)
@ -212,54 +239,41 @@ class TestModeratorNotificationUnit(unittest.TestCase):
self.assertTrue(self.mailhost.messages[0])
msg = self.mailhost.messages[0]
msg = msg.decode("utf-8")
self.assertTrue('To: portal@plone.test' in msg)
self.assertTrue('From: portal@plone.test' in msg)
self.assertTrue("To: portal@plone.test" in msg)
self.assertTrue("From: portal@plone.test" in msg)
# We expect the headers to be properly header encoded (7-bit):
self.assertTrue(
'Subject: =?utf-8?q?A_comment_has_been_posted=2E?='
in msg)
self.assertTrue("Subject: =?utf-8?q?A_comment_has_been_posted=2E?=" in msg)
# The output should be encoded in a reasonable manner
# (in this case quoted-printable):
self.assertTrue(
'A comment on "K=C3=B6lle Alaaf" has been posted'
in msg
)
self.assertIn(
'http://nohost/plone/doc1/view#{0}'.format(comment_id),
msg
)
self.assertIn(
comment.author_email,
msg
)
self.assertIn(
comment.text,
msg
)
self.assertTrue('A comment on "K=C3=B6lle Alaaf" has been posted' in msg)
self.assertIn(f"http://nohost/plone/doc1/view#{comment_id}", msg)
self.assertIn(comment.author_email, msg)
self.assertIn(comment.text, msg)
def test_notify_moderator_specific_address(self):
# A moderator email address can be specified in the control panel.
registry = queryUtility(IRegistry)
registry['plone.app.discussion.interfaces.IDiscussionSettings' +
'.moderator_email'] = 'test@example.com'
comment = createObject('plone.Comment')
comment.text = 'Comment text'
registry[
"plone.app.discussion.interfaces.IDiscussionSettings" + ".moderator_email"
] = "test@example.com"
comment = createObject("plone.Comment")
comment.text = "Comment text"
self.conversation.addComment(comment)
self.assertEqual(len(self.mailhost.messages), 1)
msg = self.mailhost.messages[0]
msg = msg.decode("utf-8")
self.assertTrue('To: test@example.com' in msg)
self.assertTrue("To: test@example.com" in msg)
def test_do_not_notify_moderator_when_no_sender_is_available(self):
# Set sender mail address to nonw and make sure no email is send to the
# moderator.
registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mail_settings.email_from_address = None
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
self.conversation.addComment(comment)
@ -269,10 +283,12 @@ class TestModeratorNotificationUnit(unittest.TestCase):
# Disable moderator notification setting and make sure no email is send
# to the moderator.
registry = queryUtility(IRegistry)
registry['plone.app.discussion.interfaces.IDiscussionSettings.' +
'moderator_notification_enabled'] = False
comment = createObject('plone.Comment')
comment.text = 'Comment text'
registry[
"plone.app.discussion.interfaces.IDiscussionSettings."
+ "moderator_notification_enabled"
] = False
comment = createObject("plone.Comment")
comment.text = "Comment text"
self.conversation.addComment(comment)

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_ROBOT_TESTING
from ..testing import PLONE_APP_DISCUSSION_ROBOT_TESTING
from plone.app.testing import ROBOT_TEST_LEVEL
from plone.testing import layered
@ -11,19 +10,21 @@ import unittest
def test_suite():
suite = unittest.TestSuite()
current_dir = os.path.abspath(os.path.dirname(__file__))
robot_dir = os.path.join(current_dir, 'robot')
robot_dir = os.path.join(current_dir, "robot")
robot_tests = [
os.path.join('robot', doc) for doc in
os.listdir(robot_dir) if doc.endswith('.robot') and
doc.startswith('test_')
os.path.join("robot", doc)
for doc in os.listdir(robot_dir)
if doc.endswith(".robot") and doc.startswith("test_")
]
for robot_test in robot_tests:
robottestsuite = robotsuite.RobotTestSuite(robot_test)
robottestsuite.level = ROBOT_TEST_LEVEL
suite.addTests([
layered(
robottestsuite,
layer=PLONE_APP_DISCUSSION_ROBOT_TESTING,
),
])
suite.addTests(
[
layered(
robottestsuite,
layer=PLONE_APP_DISCUSSION_ROBOT_TESTING,
),
]
)
return suite

View File

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
"""Test plone.app.discussion workflow and permissions.
"""
from ..interfaces import IConversation
from ..interfaces import IDiscussionLayer
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from AccessControl import Unauthorized
from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionLayer
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.testing import login
from plone.app.testing import logout
from plone.app.testing import setRoles
@ -19,48 +18,51 @@ import unittest
class WorkflowSetupTest(unittest.TestCase):
"""Make sure the workflows are set up properly.
"""
"""Make sure the workflows are set up properly."""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal.invokeFactory('Folder', 'test-folder')
self.folder = self.portal['test-folder']
self.portal.portal_types['Document'].allow_discussion = True
self.folder.invokeFactory('Document', 'doc1')
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.portal.invokeFactory("Folder", "test-folder")
self.folder = self.portal["test-folder"]
self.portal.portal_types["Document"].allow_discussion = True
self.folder.invokeFactory("Document", "doc1")
self.doc = self.folder.doc1
def test_workflows_installed(self):
"""Make sure both comment workflows have been installed properly.
"""
self.assertTrue('comment_one_state_workflow' in
self.portal.portal_workflow.objectIds())
self.assertTrue('comment_review_workflow' in
self.portal.portal_workflow.objectIds())
"""Make sure both comment workflows have been installed properly."""
self.assertTrue(
"comment_one_state_workflow" in self.portal.portal_workflow.objectIds()
)
self.assertTrue(
"comment_review_workflow" in self.portal.portal_workflow.objectIds()
)
def test_default_workflow(self):
"""Make sure one_state_workflow is the default workflow.
"""
"""Make sure one_state_workflow is the default workflow."""
self.assertEqual(
('comment_one_state_workflow',),
("comment_one_state_workflow",),
self.portal.portal_workflow.getChainForPortalType(
'Discussion Item',
"Discussion Item",
),
)
def test_review_comments_permission(self):
# 'Review comments' in self.portal.permissionsOfRole('Admin')
setRoles(self.portal, TEST_USER_ID, ['Reviewer'])
self.assertTrue(self.portal.portal_membership.checkPermission(
'Review comments', self.folder), self.folder)
setRoles(self.portal, TEST_USER_ID, ['Member'])
setRoles(self.portal, TEST_USER_ID, ["Reviewer"])
self.assertTrue(
self.portal.portal_membership.checkPermission(
"Review comments", self.folder
),
self.folder,
)
setRoles(self.portal, TEST_USER_ID, ["Member"])
self.assertFalse(
self.portal.portal_membership.checkPermission(
'Review comments',
"Review comments",
self.folder,
),
self.folder,
@ -71,31 +73,30 @@ class WorkflowSetupTest(unittest.TestCase):
class PermissionsSetupTest(unittest.TestCase):
"""Make sure the permissions are set up properly.
"""
"""Make sure the permissions are set up properly."""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
mtool = self.portal.portal_membership
self.checkPermission = mtool.checkPermission
def test_reply_to_item_permission_assigned(self):
"""Make sure the 'Reply to item' permission is properly assigned.
By default this permission is assigned to 'Member' and 'Manager'.
plone.app.discussion assigns this permission to 'Authenticated' as
well to emulate the behavior of the old commenting system.
By default this permission is assigned to 'Member' and 'Manager'.
plone.app.discussion assigns this permission to 'Authenticated' as
well to emulate the behavior of the old commenting system.
"""
ReplyToItemPerm = 'Reply to item'
ReplyToItemPerm = "Reply to item"
# should be allowed as Member
self.assertTrue(self.checkPermission(ReplyToItemPerm, self.portal))
# should be allowed as Authenticated
setRoles(self.portal, TEST_USER_ID, ['Authenticated'])
setRoles(self.portal, TEST_USER_ID, ["Authenticated"])
self.assertTrue(self.checkPermission(ReplyToItemPerm, self.portal))
# should be allowed as Manager
setRoles(self.portal, TEST_USER_ID, ['Manager'])
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.assertTrue(self.checkPermission(ReplyToItemPerm, self.portal))
# should not be allowed as anonymous
logout()
@ -103,70 +104,66 @@ class PermissionsSetupTest(unittest.TestCase):
class CommentOneStateWorkflowTest(unittest.TestCase):
"""Test the comment_one_state_workflow that ships with plone.app.discussion.
"""
"""Test the comment_one_state_workflow that ships with plone.app.discussion."""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal.invokeFactory('Folder', 'test-folder')
self.folder = self.portal['test-folder']
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.portal.invokeFactory("Folder", "test-folder")
self.folder = self.portal["test-folder"]
self.catalog = self.portal.portal_catalog
self.workflow = self.portal.portal_workflow
self.folder.invokeFactory('Document', 'doc1')
self.folder.invokeFactory("Document", "doc1")
self.doc = self.folder.doc1
# Add a comment
conversation = IConversation(self.folder.doc1)
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
cid = conversation.addComment(comment)
self.comment = self.folder.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(cid),
f"++conversation++default/{cid}",
)
self.portal.acl_users._doAddUser('member', 'secret', ['Member'], [])
self.portal.acl_users._doAddUser(
'reviewer', 'secret', ['Reviewer'], [])
self.portal.acl_users._doAddUser('manager', 'secret', ['Manager'], [])
self.portal.acl_users._doAddUser('editor', ' secret', ['Editor'], [])
self.portal.acl_users._doAddUser('reader', 'secret', ['Reader'], [])
self.portal.acl_users._doAddUser("member", "secret", ["Member"], [])
self.portal.acl_users._doAddUser("reviewer", "secret", ["Reviewer"], [])
self.portal.acl_users._doAddUser("manager", "secret", ["Manager"], [])
self.portal.acl_users._doAddUser("editor", " secret", ["Editor"], [])
self.portal.acl_users._doAddUser("reader", "secret", ["Reader"], [])
def test_initial_workflow_state(self):
"""Make sure the initial workflow state of a comment is 'private'.
"""
"""Make sure the initial workflow state of a comment is 'private'."""
self.assertEqual(
self.workflow.getInfoFor(self.doc, 'review_state'),
'private',
self.workflow.getInfoFor(self.doc, "review_state"),
"private",
)
def test_view_comments(self):
"""Make sure published comments can be viewed by everyone.
"""
"""Make sure published comments can be viewed by everyone."""
# Owner is allowed
# self.login(default_user)
# self.assertTrue(checkPerm(View, self.doc))
# Member is allowed
login(self.portal, TEST_USER_NAME)
workflow = self.portal.portal_workflow
workflow.doActionFor(self.doc, 'publish')
workflow.doActionFor(self.doc, "publish")
login(self.portal, 'member')
login(self.portal, "member")
self.assertTrue(checkPerm(View, self.comment))
# Reviewer is allowed
login(self.portal, 'reviewer')
login(self.portal, "reviewer")
self.assertTrue(checkPerm(View, self.comment))
# Anonymous is allowed
logout()
self.assertTrue(checkPerm(View, self.comment))
# Editor is allowed
login(self.portal, 'editor')
login(self.portal, "editor")
self.assertTrue(checkPerm(View, self.comment))
# Reader is allowed
login(self.portal, 'reader')
login(self.portal, "reader")
self.assertTrue(checkPerm(View, self.comment))
def test_comment_on_private_content_not_visible_to_world(self):
@ -175,8 +172,9 @@ class CommentOneStateWorkflowTest(unittest.TestCase):
def test_migration(self):
from plone.app.discussion.upgrades import upgrade_comment_workflows
# Fake permission according to earlier one_comment_workflow.
self.comment._View_Permission = ('Anonymous',)
self.comment._View_Permission = ("Anonymous",)
# Anonymous can see the comment.
logout()
self.assertTrue(checkPerm(View, self.comment))
@ -185,8 +183,8 @@ class CommentOneStateWorkflowTest(unittest.TestCase):
upgrade_comment_workflows(self.portal.portal_setup)
# The workflow chain is still what we want.
self.assertEqual(
self.portal.portal_workflow.getChainFor('Discussion Item'),
('comment_one_state_workflow',),
self.portal.portal_workflow.getChainFor("Discussion Item"),
("comment_one_state_workflow",),
)
# A Manager can still see the comment.
self.assertTrue(checkPerm(View, self.comment))
@ -196,112 +194,112 @@ class CommentOneStateWorkflowTest(unittest.TestCase):
class CommentReviewWorkflowTest(unittest.TestCase):
"""Test the comment_review_workflow that ships with plone.app.discussion.
"""
"""Test the comment_review_workflow that ships with plone.app.discussion."""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self):
self.portal = self.layer['portal']
setRoles(self.portal, TEST_USER_ID, ['Manager'])
self.portal.invokeFactory('Folder', 'test-folder')
self.folder = self.portal['test-folder']
self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.portal.invokeFactory("Folder", "test-folder")
self.folder = self.portal["test-folder"]
# Allow discussion on the Document content type
self.portal.portal_types['Document'].allow_discussion = True
self.portal.portal_types["Document"].allow_discussion = True
# Set workflow for Discussion item to review workflow
self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',),
('comment_review_workflow',),
("Discussion Item",),
("comment_review_workflow",),
)
# Create a conversation for this Document
conversation = IConversation(self.portal.doc1)
# Add a comment.
comment = createObject('plone.Comment')
comment.text = 'Comment text'
comment = createObject("plone.Comment")
comment.text = "Comment text"
comment_id = conversation.addComment(comment)
comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(comment_id),
f"++conversation++default/{comment_id}",
)
self.conversation = conversation
self.comment_id = comment_id
self.comment = comment
setRoles(self.portal, TEST_USER_ID, ['Reviewer'])
setRoles(self.portal, TEST_USER_ID, ["Reviewer"])
alsoProvides(self.portal.REQUEST, IDiscussionLayer)
def test_delete(self):
self.portal.REQUEST.form['comment_id'] = self.comment_id
view = self.comment.restrictedTraverse('@@moderate-delete-comment')
self.portal.REQUEST.form["comment_id"] = self.comment_id
view = self.comment.restrictedTraverse("@@moderate-delete-comment")
view()
self.assertFalse(self.comment_id in self.conversation.objectIds())
def test_delete_as_anonymous(self):
# Make sure that anonymous users can not delete comments
logout()
self.portal.REQUEST.form['comment_id'] = self.comment_id
self.portal.REQUEST.form["comment_id"] = self.comment_id
self.assertRaises(
Unauthorized,
self.comment.restrictedTraverse,
'@@moderate-delete-comment',
"@@moderate-delete-comment",
)
self.assertTrue(self.comment_id in self.conversation.objectIds())
def test_delete_as_user(self):
# Make sure that members can not delete comments
logout()
setRoles(self.portal, TEST_USER_ID, ['Member'])
self.portal.REQUEST.form['comment_id'] = self.comment_id
setRoles(self.portal, TEST_USER_ID, ["Member"])
self.portal.REQUEST.form["comment_id"] = self.comment_id
self.assertRaises(
Unauthorized,
self.comment.restrictedTraverse,
'@@moderate-delete-comment',
"@@moderate-delete-comment",
)
self.assertTrue(self.comment_id in self.conversation.objectIds())
def test_publish(self):
self.portal.REQUEST.form['comment_id'] = self.comment_id
self.portal.REQUEST.form['workflow_action'] = 'publish'
self.portal.REQUEST.form["comment_id"] = self.comment_id
self.portal.REQUEST.form["workflow_action"] = "publish"
self.assertEqual(
'pending',
"pending",
self.portal.portal_workflow.getInfoFor(
self.comment,
'review_state',
"review_state",
),
)
view = self.comment.restrictedTraverse('@@transmit-comment')
view = self.comment.restrictedTraverse("@@transmit-comment")
view()
self.assertEqual(
'published',
"published",
self.portal.portal_workflow.getInfoFor(
self.comment,
'review_state',
"review_state",
),
)
def test_publish_as_anonymous(self):
logout()
self.portal.REQUEST.form['comment_id'] = self.comment_id
self.portal.REQUEST.form['workflow_action'] = 'publish'
self.portal.REQUEST.form["comment_id"] = self.comment_id
self.portal.REQUEST.form["workflow_action"] = "publish"
self.assertEqual(
'pending', self.portal.portal_workflow.getInfoFor(
"pending",
self.portal.portal_workflow.getInfoFor(
self.comment,
'review_state',
"review_state",
),
)
self.assertRaises(
Unauthorized,
self.comment.restrictedTraverse,
'@@transmit-comment',
"@@transmit-comment",
)
self.assertEqual(
'pending',
"pending",
self.portal.portal_workflow.getInfoFor(
self.comment,
'review_state',
"review_state",
),
)
@ -312,15 +310,16 @@ class CommentReviewWorkflowTest(unittest.TestCase):
# publish comment and check again
login(self.portal, TEST_USER_NAME)
workflow = self.portal.portal_workflow
workflow.doActionFor(self.comment, 'publish')
workflow.doActionFor(self.comment, "publish")
logout()
self.assertFalse(checkPerm(View, self.comment))
def test_migration(self):
from plone.app.discussion.upgrades import upgrade_comment_workflows
# Fake permission according to earlier comment_review_workflow.
self.comment._View_Permission = ('Anonymous',)
self.comment._View_Permission = ("Anonymous",)
# Anonymous can see the comment.
logout()
self.assertTrue(checkPerm(View, self.comment))
@ -329,8 +328,9 @@ class CommentReviewWorkflowTest(unittest.TestCase):
upgrade_comment_workflows(self.portal.portal_setup)
# The workflow chain is still what we want.
self.assertEqual(
self.portal.portal_workflow.getChainFor('Discussion Item'),
('comment_review_workflow',))
self.portal.portal_workflow.getChainFor("Discussion Item"),
("comment_review_workflow",),
)
# A Manager can still see the comment.
self.assertTrue(checkPerm(View, self.comment))
# Anonymous cannot see the comment.

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""The portal_discussion tool, usually accessed via
queryUtility(ICommentingTool). The default implementation delegates to the
standard portal_catalog for indexing comments.
@ -17,62 +16,60 @@ from zope.component import queryUtility
@interface.implementer(ICommentingTool)
class CommentingTool(UniqueObject, SimpleItem):
meta_type = 'plone.app.discussion tool'
id = 'portal_discussion'
meta_type = "plone.app.discussion tool"
id = "portal_discussion"
def reindexObject(self, object):
# Reindex in catalog.
catalog = getToolByName(self, 'portal_catalog')
catalog = getToolByName(self, "portal_catalog")
return catalog.reindexObject(object)
indexObject = reindexObject
def unindexObject(self, object):
# Remove from catalog.
catalog = getToolByName(self, 'portal_catalog')
catalog = getToolByName(self, "portal_catalog")
return catalog.unindexObject(object)
def uniqueValuesFor(self, name):
# return unique values for FieldIndex name
catalog = getToolByName(self, 'portal_catalog')
catalog = getToolByName(self, "portal_catalog")
return catalog.uniqueValuesFor(name)
def searchResults(self, REQUEST=None, **kw):
# Calls ZCatalog.searchResults with extra arguments that
# limit the results to what the user is allowed to see.
catalog = getToolByName(self, 'portal_catalog')
catalog = getToolByName(self, "portal_catalog")
object_provides = [IComment.__identifier__]
if 'object_provides' in kw:
kw_provides = kw['object_provides']
if "object_provides" in kw:
kw_provides = kw["object_provides"]
if isinstance(str, kw_provides):
object_provides.append(kw_provides)
else:
object_provides.extend(kw_provides)
if REQUEST is not None and 'object_provides' in REQUEST.form:
rq_provides = REQUEST.form['object_provides']
del REQUEST.form['object_provides']
if REQUEST is not None and "object_provides" in REQUEST.form:
rq_provides = REQUEST.form["object_provides"]
del REQUEST.form["object_provides"]
if isinstance(str, rq_provides):
object_provides.append(rq_provides)
else:
object_provides.extend(rq_provides)
kw['object_provides'] = object_provides
kw["object_provides"] = object_provides
return catalog.searchResults(REQUEST, **kw)
def index_object(obj, event):
"""Index the object when added to the conversation
"""
"""Index the object when added to the conversation"""
tool = queryUtility(ICommentingTool)
if tool is not None:
tool.indexObject(obj)
def unindex_object(obj, event):
"""Unindex the object when removed
"""
"""Unindex the object when removed"""
tool = queryUtility(ICommentingTool)
if tool is not None:
tool.unindexObject(obj)

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import getToolByName
@ -7,8 +6,8 @@ from zope.component import getUtility
import logging
default_profile = 'profile-plone.app.discussion:default'
logger = logging.getLogger('plone.app.discussion')
default_profile = "profile-plone.app.discussion:default"
logger = logging.getLogger("plone.app.discussion")
def update_registry(context):
@ -17,7 +16,7 @@ def update_registry(context):
def update_rolemap(context):
context.runImportStepFromProfile(default_profile, 'rolemap')
context.runImportStepFromProfile(default_profile, "rolemap")
def upgrade_comment_workflows_retain_current_workflow(context):
@ -25,16 +24,16 @@ def upgrade_comment_workflows_retain_current_workflow(context):
# import step will change it to comment_one_state_workflow. This is good.
# If it was anything else, we should restore this. So get the original
# chain.
portal_type = 'Discussion Item'
wf_tool = getToolByName(context, 'portal_workflow')
portal_type = "Discussion Item"
wf_tool = getToolByName(context, "portal_workflow")
orig_chain = list(wf_tool.getChainFor(portal_type))
# Run the workflow step. This sets the chain to
# comment_one_state_workflow.
context.runImportStepFromProfile(default_profile, 'workflow')
context.runImportStepFromProfile(default_profile, "workflow")
# Restore original workflow chain if needed.
old_workflow = 'one_state_workflow'
old_workflow = "one_state_workflow"
if old_workflow not in orig_chain:
# Restore the chain. Probably comment_review_workflow.
wf_tool.setChainForPortalTypes([portal_type], orig_chain)
@ -43,7 +42,7 @@ def upgrade_comment_workflows_retain_current_workflow(context):
if old_workflow in orig_chain:
# Replace with new one.
idx = orig_chain.index(old_workflow)
orig_chain[idx] = 'comment_one_state_workflow'
orig_chain[idx] = "comment_one_state_workflow"
# Restore the chain.
wf_tool.setChainForPortalTypes([portal_type], orig_chain)
@ -51,9 +50,9 @@ def upgrade_comment_workflows_retain_current_workflow(context):
def upgrade_comment_workflows_apply_rolemapping(context):
# Now go over the comments, update their role mappings, and reindex the
# allowedRolesAndUsers index.
portal_type = 'Discussion Item'
catalog = getToolByName(context, 'portal_catalog')
wf_tool = getToolByName(context, 'portal_workflow')
portal_type = "Discussion Item"
catalog = getToolByName(context, "portal_catalog")
wf_tool = getToolByName(context, "portal_workflow")
new_chain = list(wf_tool.getChainFor(portal_type))
workflows = [wf_tool.getWorkflowById(wf_id) for wf_id in new_chain]
for brain in catalog.unrestrictedSearchResults(portal_type=portal_type):
@ -63,7 +62,7 @@ def upgrade_comment_workflows_apply_rolemapping(context):
wf.updateRoleMappingsFor(comment)
comment.reindexObjectSecurity()
except (AttributeError, KeyError):
logger.info('Could not reindex comment {0}'.format(brain.getURL()))
logger.info(f"Could not reindex comment {brain.getURL()}")
def upgrade_comment_workflows(context):
@ -72,7 +71,7 @@ def upgrade_comment_workflows(context):
def add_js_to_plone_legacy(context):
context.runImportStepFromProfile(default_profile, 'plone.app.registry')
context.runImportStepFromProfile(default_profile, "plone.app.registry")
def extend_review_workflow(context):

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from plone.app.discussion.interfaces import _
from zope.schema.vocabulary import SimpleTerm
from zope.schema.vocabulary import SimpleVocabulary
@ -7,6 +6,7 @@ from zope.schema.vocabulary import SimpleVocabulary
HAS_CAPTCHA = False
try:
import plone.formwidget.captcha # noqa
HAS_CAPTCHA = True # pragma: no cover
except ImportError:
pass
@ -14,6 +14,7 @@ except ImportError:
HAS_RECAPTCHA = False
try:
import plone.formwidget.recaptcha # noqa
HAS_RECAPTCHA = True # pragma: no cover
except ImportError:
pass
@ -21,6 +22,7 @@ except ImportError:
HAS_AKISMET = False
try:
import collective.akismet # noqa
HAS_AKISMET = True # pragma: no cover
except ImportError:
pass
@ -28,73 +30,48 @@ except ImportError:
HAS_NOROBOTS = False
try:
import collective.z3cform.norobots # noqa
HAS_NOROBOTS = True # pragma: no cover
except ImportError:
pass
def captcha_vocabulary(context):
"""Vocabulary with all available captcha implementations.
"""
"""Vocabulary with all available captcha implementations."""
terms = []
terms.append(
SimpleTerm(
value='disabled',
token='disabled',
title=_(u'Disabled')))
terms.append(SimpleTerm(value="disabled", token="disabled", title=_("Disabled")))
if HAS_CAPTCHA: # pragma: no cover
terms.append(
SimpleTerm(
value='captcha',
token='captcha',
title='Captcha'))
terms.append(SimpleTerm(value="captcha", token="captcha", title="Captcha"))
if HAS_RECAPTCHA: # pragma: no cover
terms.append(
SimpleTerm(
value='recaptcha',
token='recaptcha',
title='ReCaptcha'))
SimpleTerm(value="recaptcha", token="recaptcha", title="ReCaptcha")
)
if HAS_AKISMET: # pragma: no cover
terms.append(
SimpleTerm(
value='akismet',
token='akismet',
title='Akismet'))
terms.append(SimpleTerm(value="akismet", token="akismet", title="Akismet"))
if HAS_NOROBOTS: # pragma: no cover
terms.append(
SimpleTerm(
value='norobots',
token='norobots',
title='Norobots'))
terms.append(SimpleTerm(value="norobots", token="norobots", title="Norobots"))
return SimpleVocabulary(terms)
def text_transform_vocabulary(context):
"""Vocabulary with all available portal_transform transformations.
"""
"""Vocabulary with all available portal_transform transformations."""
terms = []
terms.append(SimpleTerm(value="text/plain", token="text/plain", title="Plain text"))
terms.append(SimpleTerm(value="text/html", token="text/html", title="HTML"))
terms.append(
SimpleTerm(
value='text/plain',
token='text/plain',
title='Plain text'))
value="text/x-web-markdown", token="text/x-web-markdown", title="Markdown"
)
)
terms.append(
SimpleTerm(
value='text/html',
token='text/html',
title='HTML'))
terms.append(
SimpleTerm(
value='text/x-web-markdown',
token='text/x-web-markdown',
title='Markdown'))
terms.append(
SimpleTerm(
value='text/x-web-intelligent',
token='text/x-web-intelligent',
title='Intelligent text'))
value="text/x-web-intelligent",
token="text/x-web-intelligent",
title="Intelligent text",
)
)
return SimpleVocabulary(terms)

View File

@ -11,3 +11,10 @@ universal = 1
[zest.releaser]
create-wheel = yes
[isort]
# black compatible Plone isort rules:
profile = black
force_alphabetical_sort = True
force_single_line = True
lines_after_imports = 2

113
setup.py
View File

@ -1,73 +1,62 @@
# encoding: utf-8
from setuptools import find_packages
from setuptools import setup
version = '4.0.0a7.dev0'
version = "4.0.0a7.dev0"
install_requires = [
'setuptools',
'plone.app.layout',
'plone.app.registry',
'plone.app.uuid',
'plone.app.z3cform',
'plone.indexer',
'plone.registry',
'plone.z3cform',
'six',
'ZODB3',
'zope.interface',
'zope.component',
'zope.annotation',
'zope.event',
'zope.container',
'zope.lifecycleevent',
'zope.site',
'z3c.form>=2.3.3',
"setuptools",
"plone.app.layout",
"plone.app.registry",
"plone.app.uuid",
"plone.app.z3cform",
"plone.base",
"plone.indexer",
"plone.z3cform",
"z3c.form>=2.3.3",
]
setup(name='plone.app.discussion',
version=version,
description='Enhanced discussion support for Plone',
long_description=open('README.rst').read() + '\n' +
open('CHANGES.rst').read(),
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Plone",
"Framework :: Plone :: 6.0",
"Framework :: Plone :: Core",
"Framework :: Zope :: 5",
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
keywords='plone discussion',
author='Timo Stollenwerk - Plone Foundation',
author_email='plone-developers@lists.sourceforge.net',
url='https://pypi.org/project/plone.app.discussion',
license='GPL',
packages=find_packages(),
namespace_packages=['plone', 'plone.app'],
include_package_data=True,
zip_safe=False,
install_requires=install_requires,
extras_require={
'test': [
'plone.app.testing',
'plone.stringinterp',
'plone.contentrules',
'plone.app.contentrules',
'plone.app.contenttypes[test]',
'plone.app.robotframework',
],
},
entry_points="""
setup(
name="plone.app.discussion",
version=version,
description="Enhanced discussion support for Plone",
long_description=open("README.rst").read() + "\n" + open("CHANGES.rst").read(),
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Plone",
"Framework :: Plone :: 6.0",
"Framework :: Plone :: Core",
"Framework :: Zope :: 5",
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
],
keywords="plone discussion",
author="Timo Stollenwerk - Plone Foundation",
author_email="plone-developers@lists.sourceforge.net",
url="https://pypi.org/project/plone.app.discussion",
license="GPL",
packages=find_packages(),
namespace_packages=["plone", "plone.app"],
include_package_data=True,
zip_safe=False,
install_requires=install_requires,
extras_require={
"test": [
"plone.app.testing",
"plone.stringinterp",
"plone.contentrules",
"plone.app.contentrules",
"plone.app.contenttypes[test]",
"plone.app.robotframework",
],
},
entry_points="""
[z3c.autoinclude.plugin]
target = plone
""",
)
)