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 # plone.app.discussion documentation build configuration file, created by
# sphinx-quickstart on Thu Mar 18 10:17:15 2010. # 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 # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', extensions = [
'sphinx.ext.doctest', "sphinx.ext.autodoc",
'sphinx.ext.intersphinx', "sphinx.ext.doctest",
'sphinx.ext.todo', "sphinx.ext.intersphinx",
'sphinx.ext.coverage', "sphinx.ext.todo",
'repoze.sphinx.autointerface' "sphinx.ext.coverage",
] "repoze.sphinx.autointerface",
]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ["_templates"]
# The suffix of source filenames. # The suffix of source filenames.
source_suffix = '.txt' source_suffix = ".txt"
# The encoding of source files. # The encoding of source files.
#source_encoding = 'utf-8' # source_encoding = 'utf-8'
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = "index"
# General information about the project. # General information about the project.
project = u'plone.app.discussion' project = "plone.app.discussion"
copyright = u'2010, Timo Stollenwerk - Plone Foundation' copyright = "2010, Timo Stollenwerk - Plone Foundation"
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the # |version| and |release|, also used in various other places throughout the
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '2.0' version = "2.0"
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '2.0' release = "2.0"
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.
#language = None # language = None
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:
#today = '' # today = ''
# Else, today_fmt is used as the format for a strftime call. # 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. # 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 # List of directories, relative to source directory, that shouldn't be searched
# for source files. # for source files.
exclude_trees = [] exclude_trees = []
# The reST default role (used for this markup: `text`) to use for all documents. # 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. # 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 # If true, the current module name will be prepended to all description
# unit titles (such as .. function::). # unit titles (such as .. function::).
#add_module_names = True # add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the # If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default. # output. They are ignored by default.
#show_authors = False # show_authors = False
# The name of the Pygments (syntax highlighting) style to use. # 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. # A list of ignored prefixes for module index sorting.
#modindex_common_prefix = [] # modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------- # -- Options for HTML output ---------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with # The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'. # 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 # 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 # further. For a list of options available for each theme, see the
# documentation. # documentation.
#html_theme_options = {} # html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory. # 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 # The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation". # "<project> v<release> documentation".
#html_title = None # html_title = None
# A shorter title for the navigation bar. Default is the same as html_title. # 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 # The name of an image file (relative to this directory) to place at the top
# of the sidebar. # 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 # 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 # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large. # pixels large.
#html_favicon = None # html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here, # 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, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # 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, # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format. # 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 # If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities. # typographically correct entities.
#html_use_smartypants = True # html_use_smartypants = True
# Custom sidebar templates, maps document names to template names. # 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 # Additional templates that should be rendered to pages, maps page names to
# template names. # template names.
#html_additional_pages = {} # html_additional_pages = {}
# If false, no module index is generated. # If false, no module index is generated.
#html_use_modindex = True # html_use_modindex = True
# If false, no index is generated. # 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. # 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. # 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 # 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 # contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served. # 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"). # 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. # Output file base name for HTML help builder.
htmlhelp_basename = 'ploneappdiscussiondoc' htmlhelp_basename = "ploneappdiscussiondoc"
# -- Options for LaTeX output -------------------------------------------- # -- Options for LaTeX output --------------------------------------------
# The paper size ('letter' or 'a4'). # The paper size ('letter' or 'a4').
#latex_paper_size = 'letter' # latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt'). # 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 # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ 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 name of an image file (relative to this directory) to place at the top of
# the title page. # the title page.
#latex_logo = None # latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts, # For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters. # not chapters.
#latex_use_parts = False # latex_use_parts = False
# Additional stuff for the LaTeX preamble. # Additional stuff for the LaTeX preamble.
#latex_preamble = '' # latex_preamble = ''
# Documents to append as an appendix to all manuals. # Documents to append as an appendix to all manuals.
#latex_appendices = [] # latex_appendices = []
# If false, no module index is generated. # 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. # 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. # Interface to define the fields we want to add to the comment form.
class ICommentExtenderFields(Interface): 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 # Persistent class that implements the ICommentExtenderFields interface
@adapter(Comment) @adapter(Comment)
class CommentExtenderFields(Persistent): class CommentExtenderFields(Persistent):
interface.implements(ICommentExtenderFields) interface.implements(ICommentExtenderFields)
website = u"" website = ""
# CommentExtenderFields factory # CommentExtenderFields factory
CommentExtenderFactory = factory(CommentExtenderFields) CommentExtenderFactory = factory(CommentExtenderFields)

View File

@ -20,8 +20,8 @@ configure.zcml::
Define an interface IMyDexterityContentType groked schema, I added:: Define an interface IMyDexterityContentType groked schema, I added::
allowDiscussion = schema.Bool( allowDiscussion = schema.Bool(
title=_(u"Allow Users to Comment"), title=_("Allow Users to Comment"),
description=_(u"Allow users to commemt on you. Comments description=_("Allow users to comment on you. Comments
are shown at the end of each page"), are shown at the end of each page"),
required=True, required=True,
default=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 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. # Captcha validator, see captcha.txt for design notes.
from persistent import Persistent from persistent import Persistent
from plone.app.discussion.browser.comments import CommentForm from plone.app.discussion.browser.comments import CommentForm
@ -21,9 +20,9 @@ from zope.publisher.interfaces.browser import IDefaultBrowserLayer
@adapter(Comment) @adapter(Comment)
@interface.implementer(ICaptcha) @interface.implementer(ICaptcha)
class Captcha(Persistent): class Captcha(Persistent):
"""Captcha input field. """Captcha input field."""
"""
captcha = u'' captcha = ""
Captcha = factory(Captcha) Captcha = factory(Captcha)
@ -47,22 +46,24 @@ class CaptchaExtender(extensible.FormExtender):
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
self.captcha = settings.captcha self.captcha = settings.captcha
portal_membership = getToolByName(self.context, 'portal_membership') portal_membership = getToolByName(self.context, "portal_membership")
self.isAnon = portal_membership.isAnonymousUser() self.isAnon = portal_membership.isAnonymousUser()
def update(self): 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 # Add a captcha field if captcha is enabled in the registry
self.add(ICaptcha, prefix='') self.add(ICaptcha, prefix="")
if self.captcha == 'captcha': if self.captcha == "captcha":
from plone.formwidget.captcha import CaptchaFieldWidget 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 from plone.formwidget.recaptcha import ReCaptchaFieldWidget
self.form.fields['captcha'].widgetFactory = \
ReCaptchaFieldWidget self.form.fields["captcha"].widgetFactory = ReCaptchaFieldWidget
elif self.captcha == 'norobots': elif self.captcha == "norobots":
from collective.z3cform.norobots import NorobotsFieldWidget from collective.z3cform.norobots import NorobotsFieldWidget
self.form.fields['captcha'].widgetFactory = NorobotsFieldWidget
self.form.fields["captcha"].widgetFactory = NorobotsFieldWidget
else: 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 .comments import CommentForm
from AccessControl import getSecurityManager from AccessControl import getSecurityManager
from Acquisition import aq_inner from Acquisition import aq_inner
@ -14,7 +13,6 @@ from zope.component import getMultiAdapter
from zope.component import getUtility from zope.component import getUtility
from zope.event import notify from zope.event import notify
from zope.lifecycleevent import ObjectModifiedEvent from zope.lifecycleevent import ObjectModifiedEvent
from .comments import CommentForm
class View(BrowserView): class View(BrowserView):
@ -38,8 +36,7 @@ class View(BrowserView):
context = aq_inner(self.context) context = aq_inner(self.context)
registry = getUtility(IRegistry) registry = getUtility(IRegistry)
view_action_types = registry.get( view_action_types = registry.get("plone.types_use_view_action_in_listings", [])
'plone.types_use_view_action_in_listings', [])
obj = aq_parent(aq_parent(context)) obj = aq_parent(aq_parent(context))
url = obj.absolute_url() url = obj.absolute_url()
@ -50,33 +47,34 @@ class View(BrowserView):
will redirect right to the binary object, bypassing comments. will redirect right to the binary object, bypassing comments.
""" """
if obj.portal_type in view_action_types: 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): class EditCommentForm(CommentForm):
"""Form to edit an existing comment.""" """Form to edit an existing comment."""
ignoreContext = True ignoreContext = True
id = 'edit-comment-form' id = "edit-comment-form"
label = _(u'edit_comment_form_title', default=u'Edit comment') label = _("edit_comment_form_title", default="Edit comment")
def updateWidgets(self): def updateWidgets(self):
super(EditCommentForm, self).updateWidgets() super().updateWidgets()
self.widgets['text'].value = self.context.text self.widgets["text"].value = self.context.text
# We have to rename the id, otherwise TinyMCE can't initialize # We have to rename the id, otherwise TinyMCE can't initialize
# because there are two textareas with the same id. # 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: if not target:
portal_state = getMultiAdapter((self.context, self.request), portal_state = getMultiAdapter(
name=u'plone_portal_state') (self.context, self.request), name="plone_portal_state"
)
target = portal_state.portal_url() target = portal_state.portal_url()
self.request.response.redirect(target) self.request.response.redirect(target)
@button.buttonAndHandler(_(u'label_save', @button.buttonAndHandler(_("label_save", default="Save"), name="comment")
default=u'Save'), name='comment')
def handleComment(self, action): def handleComment(self, action):
# Validate form # Validate form
@ -85,32 +83,28 @@ class EditCommentForm(CommentForm):
return return
# Check permissions # Check permissions
can_edit = getSecurityManager().checkPermission( can_edit = getSecurityManager().checkPermission("Edit comments", self.context)
'Edit comments', mtool = getToolByName(self.context, "portal_membership")
self.context)
mtool = getToolByName(self.context, 'portal_membership')
if mtool.isAnonymousUser() or not can_edit: if mtool.isAnonymousUser() or not can_edit:
return return
# Update text # Update text
self.context.text = data['text'] self.context.text = data["text"]
# Notify that the object has been modified # Notify that the object has been modified
notify(ObjectModifiedEvent(self.context)) notify(ObjectModifiedEvent(self.context))
# Redirect to comment # Redirect to comment
IStatusMessage(self.request).add(_(u'comment_edit_notification', IStatusMessage(self.request).add(
default='Comment was edited'), _("comment_edit_notification", default="Comment was edited"), type="info"
type='info') )
return self._redirect( return self._redirect(target=self.action.replace("@@edit-comment", "@@view"))
target=self.action.replace('@@edit-comment', '@@view'))
@button.buttonAndHandler(_(u'cancel_form_button', @button.buttonAndHandler(_("cancel_form_button", default="Cancel"), name="cancel")
default=u'Cancel'), name='cancel')
def handle_cancel(self, action): def handle_cancel(self, action):
IStatusMessage(self.request).add( IStatusMessage(self.request).add(
_(u'comment_edit_cancel_notification', _("comment_edit_cancel_notification", default="Edit comment cancelled"),
default=u'Edit comment cancelled'), type="info",
type='info') )
return self._redirect(target=self.context.absolute_url()) return self._redirect(target=self.context.absolute_url())

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from AccessControl import getSecurityManager from AccessControl import getSecurityManager
from AccessControl import Unauthorized from AccessControl import Unauthorized
from Acquisition import aq_inner 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 IDiscussionSettings
from plone.app.discussion.interfaces import IReplies from plone.app.discussion.interfaces import IReplies
from plone.app.layout.viewlets.common import ViewletBase from plone.app.layout.viewlets.common import ViewletBase
from plone.base.utils import safe_text
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
from plone.z3cform import z2 from plone.z3cform import z2
from plone.z3cform.fieldsets import extensible from plone.z3cform.fieldsets import extensible
from plone.z3cform.interfaces import IWrappedForm from plone.z3cform.interfaces import IWrappedForm
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.utils import safe_unicode
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.statusmessages.interfaces import IStatusMessage 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 button
from z3c.form import field from z3c.form import field
from z3c.form import form from z3c.form import form
@ -35,28 +34,28 @@ from zope.interface import alsoProvides
COMMENT_DESCRIPTION_PLAIN_TEXT = _( COMMENT_DESCRIPTION_PLAIN_TEXT = _(
u'comment_description_plain_text', "comment_description_plain_text",
default=u'You can add a comment by filling out the form below. ' default="You can add a comment by filling out the form below. "
u'Plain text formatting.', "Plain text formatting.",
) )
COMMENT_DESCRIPTION_MARKDOWN = _( COMMENT_DESCRIPTION_MARKDOWN = _(
u'comment_description_markdown', "comment_description_markdown",
default=u'You can add a comment by filling out the form below. ' default="You can add a comment by filling out the form below. "
u'Plain text formatting. You can use the Markdown syntax for ' "Plain text formatting. You can use the Markdown syntax for "
u'links and images.', "links and images.",
) )
COMMENT_DESCRIPTION_INTELLIGENT_TEXT = _( COMMENT_DESCRIPTION_INTELLIGENT_TEXT = _(
u'comment_description_intelligent_text', "comment_description_intelligent_text",
default=u'You can add a comment by filling out the form below. ' default="You can add a comment by filling out the form below. "
u'Plain text formatting. Web and email addresses are ' "Plain text formatting. Web and email addresses are "
u'transformed into clickable links.', "transformed into clickable links.",
) )
COMMENT_DESCRIPTION_MODERATION_ENABLED = _( COMMENT_DESCRIPTION_MODERATION_ENABLED = _(
u'comment_description_moderation_enabled', "comment_description_moderation_enabled",
default=u'Comments are moderated.', default="Comments are moderated.",
) )
@ -64,30 +63,31 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
ignoreContext = True # don't use context to get widget data ignoreContext = True # don't use context to get widget data
id = None id = None
label = _(u'Add a comment') label = _("Add a comment")
fields = field.Fields(IComment).omit('portal_type', fields = field.Fields(IComment).omit(
'__parent__', "portal_type",
'__name__', "__parent__",
'comment_id', "__name__",
'mime_type', "comment_id",
'creator', "mime_type",
'creation_date', "creator",
'modification_date', "creation_date",
'author_username', "modification_date",
'title') "author_username",
"title",
)
def updateFields(self): def updateFields(self):
super(CommentForm, self).updateFields() super().updateFields()
self.fields['user_notification'].widgetFactory = \ self.fields["user_notification"].widgetFactory = SingleCheckBoxFieldWidget
SingleCheckBoxFieldWidget
def updateWidgets(self): def updateWidgets(self):
super(CommentForm, self).updateWidgets() super().updateWidgets()
# Widgets # Widgets
self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE self.widgets["in_reply_to"].mode = interfaces.HIDDEN_MODE
self.widgets['text'].addClass('autoresize') self.widgets["text"].addClass("autoresize")
self.widgets['user_notification'].label = _(u'') self.widgets["user_notification"].label = _("")
# Reset widget field settings to their defaults, which may be changed # Reset widget field settings to their defaults, which may be changed
# further on. Otherwise, the email field might get set to required # further on. Otherwise, the email field might get set to required
# when an anonymous user visits, and then remain required when an # 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 # 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 # widget is new each time, but the field is the same item in memory as
# the previous time. # 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 # 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 # previous value on the field, so we need to reset it here. Changing
# the field in updateFields does not help. # 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 # Rename the id of the text widgets because there can be css-id
# clashes with the text field of documents when using and overlay # clashes with the text field of documents when using and overlay
# with TinyMCE. # with TinyMCE.
self.widgets['text'].id = 'form-widgets-comment-text' self.widgets["text"].id = "form-widgets-comment-text"
# Anonymous / Logged-in # Anonymous / Logged-in
mtool = getToolByName(self.context, 'portal_membership') mtool = getToolByName(self.context, "portal_membership")
anon = mtool.isAnonymousUser() anon = mtool.isAnonymousUser()
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
@ -119,56 +119,55 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
if settings.anonymous_email_enabled: if settings.anonymous_email_enabled:
# according to IDiscussionSettings.anonymous_email_enabled: # according to IDiscussionSettings.anonymous_email_enabled:
# 'If selected, anonymous user will have to give their email.' # 'If selected, anonymous user will have to give their email.'
self.widgets['author_email'].field.required = True self.widgets["author_email"].field.required = True
self.widgets['author_email'].required = True self.widgets["author_email"].required = True
else: else:
self.widgets['author_email'].mode = interfaces.HIDDEN_MODE self.widgets["author_email"].mode = interfaces.HIDDEN_MODE
else: else:
self.widgets['author_name'].mode = interfaces.HIDDEN_MODE self.widgets["author_name"].mode = interfaces.HIDDEN_MODE
self.widgets['author_email'].mode = interfaces.HIDDEN_MODE self.widgets["author_email"].mode = interfaces.HIDDEN_MODE
member = mtool.getAuthenticatedMember() member = mtool.getAuthenticatedMember()
member_email = member.getProperty('email') member_email = member.getProperty("email")
# Hide the user_notification checkbox if user notification is disabled # 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 # or the user is not logged in. Also check if the user has a valid
# email address # email address
member_email_is_empty = member_email == '' member_email_is_empty = member_email == ""
user_notification_disabled = not settings.user_notification_enabled user_notification_disabled = not settings.user_notification_enabled
if member_email_is_empty or user_notification_disabled or anon: 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): def updateActions(self):
super(CommentForm, self).updateActions() super().updateActions()
self.actions['cancel'].addClass('btn btn-secondary') self.actions["cancel"].addClass("btn btn-secondary")
self.actions['cancel'].addClass('hide') self.actions["cancel"].addClass("hide")
self.actions['comment'].addClass('btn btn-primary') self.actions["comment"].addClass("btn btn-primary")
def get_author(self, data): def get_author(self, data):
context = aq_inner(self.context) context = aq_inner(self.context)
# some attributes are not always set # some attributes are not always set
author_name = u'' author_name = ""
# Make sure author_name/ author_email is properly encoded # Make sure author_name/ author_email is properly encoded
if 'author_name' in data: if "author_name" in data:
author_name = safe_unicode(data['author_name']) author_name = safe_text(data["author_name"])
if 'author_email' in data: if "author_email" in data:
author_email = safe_unicode(data['author_email']) author_email = safe_text(data["author_email"])
# Set comment author properties for anonymous users or members # 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() anon = portal_membership.isAnonymousUser()
if not anon and getSecurityManager().checkPermission( if not anon and getSecurityManager().checkPermission("Reply to item", context):
'Reply to item', context):
# Member # Member
member = portal_membership.getAuthenticatedMember() member = portal_membership.getAuthenticatedMember()
email = safe_unicode(member.getProperty('email')) email = safe_text(member.getProperty("email"))
fullname = member.getProperty('fullname') fullname = member.getProperty("fullname")
if not fullname or fullname == '': if not fullname or fullname == "":
fullname = member.getUserName() fullname = member.getUserName()
fullname = safe_unicode(fullname) fullname = safe_text(fullname)
author_name = fullname author_name = fullname
email = safe_unicode(email) email = safe_text(email)
# XXX: according to IComment interface author_email must not be # noqa T000 # XXX: according to IComment interface author_email must not be # noqa T000
# set for logged in users, cite: # set for logged in users, cite:
# 'for anonymous comments only, set to None for logged in comments' # '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): def create_comment(self, data):
context = aq_inner(self.context) context = aq_inner(self.context)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False) 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) comment.author_name, comment.author_email = self.get_author(data)
# Set comment author properties for anonymous users or members # 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() anon = portal_membership.isAnonymousUser()
if anon and anonymous_comments: if anon and anonymous_comments:
# Anonymous Users # Anonymous Users
comment.user_notification = None comment.user_notification = None
elif not anon and getSecurityManager().checkPermission( elif not anon and getSecurityManager().checkPermission(
'Reply to item', context): "Reply to item", context
):
# Member # Member
member = portal_membership.getAuthenticatedMember() member = portal_membership.getAuthenticatedMember()
memberid = member.getId() memberid = member.getId()
user = member.getUser() user = member.getUser()
comment.changeOwnership(user, recursive=False) comment.changeOwnership(user, recursive=False)
comment.manage_setLocalRoles(memberid, ['Owner']) comment.manage_setLocalRoles(memberid, ["Owner"])
comment.creator = memberid comment.creator = memberid
comment.author_username = memberid comment.author_username = memberid
else: # pragma: no cover else: # pragma: no cover
raise Unauthorized( raise Unauthorized(
u'Anonymous user tries to post a comment, but anonymous ' "Anonymous user tries to post a comment, but anonymous "
u'commenting is disabled. Or user does not have the ' "commenting is disabled. Or user does not have the "
u"'reply to item' permission.", "'reply to item' permission.",
) )
return comment return comment
@button.buttonAndHandler(_(u'add_comment_button', default=u'Comment'), @button.buttonAndHandler(_("add_comment_button", default="Comment"), name="comment")
name='comment')
def handleComment(self, action): def handleComment(self, action):
context = aq_inner(self.context) context = aq_inner(self.context)
# Check if conversation is enabled on this content object # Check if conversation is enabled on this content object
if not self.__parent__.restrictedTraverse( if not self.__parent__.restrictedTraverse(
'@@conversation_view', "@@conversation_view",
).enabled(): ).enabled():
raise Unauthorized( raise Unauthorized(
'Discussion is not enabled for this content object.', "Discussion is not enabled for this content object.",
) )
# Validation form # Validation form
@ -246,28 +245,26 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
# Validate Captcha # Validate Captcha
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
portal_membership = getToolByName(self.context, 'portal_membership') portal_membership = getToolByName(self.context, "portal_membership")
captcha_enabled = settings.captcha != 'disabled' captcha_enabled = settings.captcha != "disabled"
anonymous_comments = settings.anonymous_comments anonymous_comments = settings.anonymous_comments
anon = portal_membership.isAnonymousUser() anon = portal_membership.isAnonymousUser()
if captcha_enabled and anonymous_comments and anon: if captcha_enabled and anonymous_comments and anon:
if 'captcha' not in data: if "captcha" not in data:
data['captcha'] = u'' data["captcha"] = ""
captcha = CaptchaValidator(self.context, captcha = CaptchaValidator(
self.request, self.context, self.request, None, ICaptcha["captcha"], None
None, )
ICaptcha['captcha'], captcha.validate(data["captcha"])
None)
captcha.validate(data['captcha'])
# Create comment # Create comment
comment = self.create_comment(data) comment = self.create_comment(data)
# Add comment to conversation # Add comment to conversation
conversation = IConversation(self.__parent__) conversation = IConversation(self.__parent__)
if data['in_reply_to']: if data["in_reply_to"]:
# Add a reply to an existing comment # 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) replies = IReplies(conversation_to_reply_to)
comment_id = replies.addComment(comment) comment_id = replies.addComment(comment)
else: else:
@ -279,25 +276,24 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
# shown to the user that his/her comment awaits moderation. If the user # shown to the user that his/her comment awaits moderation. If the user
# has 'review comments' permission, he/she is redirected directly # has 'review comments' permission, he/she is redirected directly
# to the comment. # to the comment.
can_review = getSecurityManager().checkPermission('Review comments', can_review = getSecurityManager().checkPermission("Review comments", context)
context) workflowTool = getToolByName(context, "portal_workflow")
workflowTool = getToolByName(context, 'portal_workflow')
comment_review_state = workflowTool.getInfoFor( comment_review_state = workflowTool.getInfoFor(
comment, comment,
'review_state', "review_state",
None, 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 # Show info message when comment moderation is enabled
IStatusMessage(self.context.REQUEST).addStatusMessage( IStatusMessage(self.context.REQUEST).addStatusMessage(
_('Your comment awaits moderator approval.'), _("Your comment awaits moderator approval."), type="info"
type='info') )
self.request.response.redirect(self.action) self.request.response.redirect(self.action)
else: else:
# Redirect to comment (inside a content object page) # 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): def handleCancel(self, action):
# This method should never be called, it's only there to show # This method should never be called, it's only there to show
# a cancel button that is handled by a jQuery method. # a cancel button that is handled by a jQuery method.
@ -307,15 +303,15 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
class CommentsViewlet(ViewletBase): class CommentsViewlet(ViewletBase):
form = CommentForm form = CommentForm
index = ViewPageTemplateFile('comments.pt') index = ViewPageTemplateFile("comments.pt")
def update(self): def update(self):
super(CommentsViewlet, self).update() super().update()
discussion_allowed = self.is_discussion_allowed() discussion_allowed = self.is_discussion_allowed()
anonymous_allowed_or_can_reply = ( anonymous_allowed_or_can_reply = (
self.is_anonymous() and self.is_anonymous()
self.anonymous_discussion_allowed() or and self.anonymous_discussion_allowed()
self.can_reply() or self.can_reply()
) )
if discussion_allowed and anonymous_allowed_or_can_reply: if discussion_allowed and anonymous_allowed_or_can_reply:
z2.switch_on(self, request_layer=IFormLayer) z2.switch_on(self, request_layer=IFormLayer)
@ -326,10 +322,10 @@ class CommentsViewlet(ViewletBase):
# view methods # view methods
def can_reply(self): def can_reply(self):
"""Returns true if current user has the 'Reply to item' permission. """Returns true if current user has the 'Reply to item' permission."""
""" return getSecurityManager().checkPermission(
return getSecurityManager().checkPermission('Reply to item', "Reply to item", aq_inner(self.context)
aq_inner(self.context)) )
def can_manage(self): def can_manage(self):
"""We keep this method for <= 1.0b9 backward compatibility. Since we do """We keep this method for <= 1.0b9 backward compatibility. Since we do
@ -338,18 +334,17 @@ class CommentsViewlet(ViewletBase):
return self.can_review() return self.can_review()
def can_review(self): def can_review(self):
"""Returns true if current user has the 'Review comments' permission. """Returns true if current user has the 'Review comments' permission."""
""" return getSecurityManager().checkPermission(
return getSecurityManager().checkPermission('Review comments', "Review comments", aq_inner(self.context)
aq_inner(self.context)) )
def can_delete_own(self, comment): def can_delete_own(self, comment):
"""Returns true if the current user can delete the comment. Only """Returns true if the current user can delete the comment. Only
comments without replies can be deleted. comments without replies can be deleted.
""" """
try: try:
return comment.restrictedTraverse( return comment.restrictedTraverse("@@delete-own-comment").can_delete()
'@@delete-own-comment').can_delete()
except Unauthorized: except Unauthorized:
return False return False
@ -358,8 +353,7 @@ class CommentsViewlet(ViewletBase):
no replies. This is used to prepare hidden form buttons for JS. no replies. This is used to prepare hidden form buttons for JS.
""" """
try: try:
return comment.restrictedTraverse( return comment.restrictedTraverse("@@delete-own-comment").could_delete()
'@@delete-own-comment').could_delete()
except Unauthorized: except Unauthorized:
return False return False
@ -367,19 +361,17 @@ class CommentsViewlet(ViewletBase):
"""Returns true if current user has the 'Edit comments' """Returns true if current user has the 'Edit comments'
permission. permission.
""" """
return getSecurityManager().checkPermission('Edit comments', return getSecurityManager().checkPermission("Edit comments", aq_inner(reply))
aq_inner(reply))
def can_delete(self, reply): def can_delete(self, reply):
"""Returns true if current user has the 'Delete comments' """Returns true if current user has the 'Delete comments'
permission. permission.
""" """
return getSecurityManager().checkPermission('Delete comments', return getSecurityManager().checkPermission("Delete comments", aq_inner(reply))
aq_inner(reply))
def is_discussion_allowed(self): def is_discussion_allowed(self):
context = aq_inner(self.context) context = aq_inner(self.context)
return context.restrictedTraverse('@@conversation_view').enabled() return context.restrictedTraverse("@@conversation_view").enabled()
def comment_transform_message(self): def comment_transform_message(self):
"""Returns the description that shows up above the comment text, """Returns the description that shows up above the comment text,
@ -391,34 +383,41 @@ class CommentsViewlet(ViewletBase):
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
# text transform setting # text transform setting
if settings.text_transform == 'text/x-web-intelligent': if settings.text_transform == "text/x-web-intelligent":
message = translate(Message(COMMENT_DESCRIPTION_INTELLIGENT_TEXT), message = translate(
context=self.request) Message(COMMENT_DESCRIPTION_INTELLIGENT_TEXT), context=self.request
elif settings.text_transform == 'text/x-web-markdown': )
message = translate(Message(COMMENT_DESCRIPTION_MARKDOWN), elif settings.text_transform == "text/x-web-markdown":
context=self.request) message = translate(
Message(COMMENT_DESCRIPTION_MARKDOWN), context=self.request
)
else: else:
message = translate(Message(COMMENT_DESCRIPTION_PLAIN_TEXT), message = translate(
context=self.request) Message(COMMENT_DESCRIPTION_PLAIN_TEXT), context=self.request
)
# comment workflow # comment workflow
wftool = getToolByName(context, 'portal_workflow', None) wftool = getToolByName(context, "portal_workflow", None)
workflow_chain = wftool.getChainForPortalType('Discussion Item') workflow_chain = wftool.getChainForPortalType("Discussion Item")
if workflow_chain: if workflow_chain:
comment_workflow = workflow_chain[0] comment_workflow = workflow_chain[0]
comment_workflow = wftool[comment_workflow] comment_workflow = wftool[comment_workflow]
# check if the current workflow implements a pending state. If this # check if the current workflow implements a pending state. If this
# is true comments are moderated # is true comments are moderated
if 'pending' in comment_workflow.states: if "pending" in comment_workflow.states:
message = message + ' ' + \ message = (
translate(Message(COMMENT_DESCRIPTION_MODERATION_ENABLED), message
context=self.request) + " "
+ translate(
Message(COMMENT_DESCRIPTION_MODERATION_ENABLED),
context=self.request,
)
)
return message return message
def has_replies(self, workflow_actions=False): 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: if self.get_replies(workflow_actions) is not None:
try: try:
next(self.get_replies(workflow_actions)) next(self.get_replies(workflow_actions))
@ -442,31 +441,32 @@ class CommentsViewlet(ViewletBase):
if conversation is None: if conversation is None:
return iter([]) return iter([])
wf = getToolByName(context, 'portal_workflow') wf = getToolByName(context, "portal_workflow")
# workflow_actions is only true when user # workflow_actions is only true when user
# has 'Manage portal' permission # has 'Manage portal' permission
def replies_with_workflow_actions(): def replies_with_workflow_actions():
# Generator that returns replies dict with workflow actions # Generator that returns replies dict with workflow actions
for r in conversation.getThreads(): for r in conversation.getThreads():
comment_obj = r['comment'] comment_obj = r["comment"]
# list all possible workflow actions # list all possible workflow actions
actions = [ actions = [
a for a in wf.listActionInfos(object=comment_obj) a
if a['category'] == 'workflow' and a['allowed'] for a in wf.listActionInfos(object=comment_obj)
if a["category"] == "workflow" and a["allowed"]
] ]
r = r.copy() r = r.copy()
r['actions'] = actions r["actions"] = actions
yield r yield r
def published_replies(): def published_replies():
# Generator that returns replies dict with workflow status. # Generator that returns replies dict with workflow status.
for r in conversation.getThreads(): for r in conversation.getThreads():
comment_obj = r['comment'] comment_obj = r["comment"]
workflow_status = wf.getInfoFor(comment_obj, 'review_state') workflow_status = wf.getInfoFor(comment_obj, "review_state")
if workflow_status == 'published': if workflow_status == "published":
r = r.copy() r = r.copy()
r['workflow_status'] = workflow_status r["workflow_status"] = workflow_status
yield r yield r
# Return all direct replies # Return all direct replies
@ -480,20 +480,16 @@ class CommentsViewlet(ViewletBase):
if username is None: if username is None:
return None return None
else: 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): def get_commenter_portrait(self, username=None):
if username is None: if username is None:
# return the default user image if no username is given # return the default user image if no username is given
return 'defaultUser.png' return "defaultUser.png"
else: else:
portal_membership = getToolByName(self.context, portal_membership = getToolByName(self.context, "portal_membership", None)
'portal_membership', return portal_membership.getPersonalPortrait(username).absolute_url()
None)
return portal_membership\
.getPersonalPortrait(username)\
.absolute_url()
def anonymous_discussion_allowed(self): def anonymous_discussion_allowed(self):
# Check if anonymous comments are allowed in the registry # Check if anonymous comments are allowed in the registry
@ -520,20 +516,18 @@ class CommentsViewlet(ViewletBase):
return settings.show_commenter_image return settings.show_commenter_image
def is_anonymous(self): def is_anonymous(self):
portal_membership = getToolByName(self.context, portal_membership = getToolByName(self.context, "portal_membership", None)
'portal_membership',
None)
return portal_membership.isAnonymousUser() return portal_membership.isAnonymousUser()
def login_action(self): def login_action(self):
return '{0}/login_form?came_from={1}'.format( return "{}/login_form?came_from={}".format(
self.navigation_root_url, self.navigation_root_url,
quote(self.request.get('URL', '')), quote(self.request.get("URL", "")),
) )
def format_time(self, time): def format_time(self, time):
# We have to transform Python datetime into Zope DateTime # We have to transform Python datetime into Zope DateTime
# before we can call toLocalizedTime. # before we can call toLocalizedTime.
util = getToolByName(self.context, 'translation_service') util = getToolByName(self.context, "translation_service")
zope_time = DateTime(time.isoformat()) zope_time = DateTime(time.isoformat())
return util.toLocalizedTime(zope_time, long_format=True) return util.toLocalizedTime(zope_time, long_format=True)

View File

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

View File

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*- from ..interfaces import _
from plone.app.discussion.interfaces import _ from ..interfaces import IDiscussionSettings
from plone.app.discussion.interfaces import IDiscussionSettings from ..upgrades import update_registry
from plone.app.discussion.upgrades import update_registry
from plone.app.registry.browser import controlpanel 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 IRecordModifiedEvent
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.interfaces.controlpanel import IMailSchema
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
from Products.statusmessages.interfaces import IStatusMessage from Products.statusmessages.interfaces import IStatusMessage
from z3c.form import button from z3c.form import button
@ -16,87 +16,76 @@ from zope.component import getUtility
from zope.component import queryUtility from zope.component import queryUtility
from zope.component.hooks import getSite 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): class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
"""Discussion settings form. """Discussion settings form."""
"""
schema = IDiscussionSettings schema = IDiscussionSettings
id = 'DiscussionSettingsEditForm' id = "DiscussionSettingsEditForm"
label = _(u'Discussion settings') label = _("Discussion settings")
description = _( description = _(
u'help_discussion_settings_editform', "help_discussion_settings_editform",
default=u'Some discussion related settings are not ' default="Some discussion related settings are not "
u'located in the Discussion Control Panel.\n' "located in the Discussion Control Panel.\n"
u'To enable comments for a specific content type, ' "To enable comments for a specific content type, "
u'go to the Types Control Panel of this type and ' "go to the Types Control Panel of this type and "
u'choose "Allow comments".\n' 'choose "Allow comments".\n'
u'To enable the moderation workflow for comments, ' "To enable the moderation workflow for comments, "
u'go to the Types Control Panel, choose ' "go to the Types Control Panel, choose "
u'"Comment" and set workflow to ' '"Comment" and set workflow to '
u'"Comment Review Workflow".', '"Comment Review Workflow".',
) )
def updateFields(self): def updateFields(self):
super(DiscussionSettingsEditForm, self).updateFields() super().updateFields()
self.fields['globally_enabled'].widgetFactory = \ self.fields["globally_enabled"].widgetFactory = SingleCheckBoxFieldWidget
SingleCheckBoxFieldWidget self.fields["moderation_enabled"].widgetFactory = SingleCheckBoxFieldWidget
self.fields['moderation_enabled'].widgetFactory = \ self.fields["edit_comment_enabled"].widgetFactory = SingleCheckBoxFieldWidget
SingleCheckBoxFieldWidget self.fields[
self.fields['edit_comment_enabled'].widgetFactory = \ "delete_own_comment_enabled"
SingleCheckBoxFieldWidget ].widgetFactory = SingleCheckBoxFieldWidget
self.fields['delete_own_comment_enabled'].widgetFactory = \ self.fields["anonymous_comments"].widgetFactory = SingleCheckBoxFieldWidget
SingleCheckBoxFieldWidget self.fields["show_commenter_image"].widgetFactory = SingleCheckBoxFieldWidget
self.fields['anonymous_comments'].widgetFactory = \ self.fields[
SingleCheckBoxFieldWidget "moderator_notification_enabled"
self.fields['show_commenter_image'].widgetFactory = \ ].widgetFactory = SingleCheckBoxFieldWidget
SingleCheckBoxFieldWidget self.fields[
self.fields['moderator_notification_enabled'].widgetFactory = \ "user_notification_enabled"
SingleCheckBoxFieldWidget ].widgetFactory = SingleCheckBoxFieldWidget
self.fields['user_notification_enabled'].widgetFactory = \
SingleCheckBoxFieldWidget
def updateWidgets(self): def updateWidgets(self):
try: try:
super(DiscussionSettingsEditForm, self).updateWidgets() super().updateWidgets()
except KeyError: except KeyError:
# upgrade profile not visible in prefs_install_products_form # upgrade profile not visible in prefs_install_products_form
# provide auto-upgrade # provide auto-upgrade
update_registry(self.context) update_registry(self.context)
super(DiscussionSettingsEditForm, self).updateWidgets() super().updateWidgets()
self.widgets['globally_enabled'].label = _(u'Enable Comments') self.widgets["globally_enabled"].label = _("Enable Comments")
self.widgets['anonymous_comments'].label = _(u'Anonymous Comments') self.widgets["anonymous_comments"].label = _("Anonymous Comments")
self.widgets['show_commenter_image'].label = _(u'Commenter Image') self.widgets["show_commenter_image"].label = _("Commenter Image")
self.widgets['moderator_notification_enabled'].label = _( self.widgets["moderator_notification_enabled"].label = _(
u'Moderator Email Notification', "Moderator Email Notification",
) )
self.widgets['user_notification_enabled'].label = _( self.widgets["user_notification_enabled"].label = _(
u'User Email Notification', "User Email Notification",
) )
@button.buttonAndHandler(_('Save'), name=None) @button.buttonAndHandler(_("Save"), name=None)
def handleSave(self, action): def handleSave(self, action):
data, errors = self.extractData() data, errors = self.extractData()
if errors: if errors:
self.status = self.formErrorsMessage self.status = self.formErrorsMessage
return return
self.applyChanges(data) self.applyChanges(data)
IStatusMessage(self.request).addStatusMessage(_(u'Changes saved'), IStatusMessage(self.request).addStatusMessage(_("Changes saved"), "info")
'info') self.context.REQUEST.RESPONSE.redirect("@@discussion-controlpanel")
self.context.REQUEST.RESPONSE.redirect('@@discussion-controlpanel')
@button.buttonAndHandler(_('Cancel'), name='cancel') @button.buttonAndHandler(_("Cancel"), name="cancel")
def handleCancel(self, action): def handleCancel(self, action):
IStatusMessage(self.request).addStatusMessage(_(u'Edit cancelled'), IStatusMessage(self.request).addStatusMessage(_("Edit cancelled"), "info")
'info')
self.request.response.redirect( self.request.response.redirect(
'{0}/{1}'.format( "{}/{}".format(
self.context.absolute_url(), self.context.absolute_url(),
self.control_panel_view, self.control_panel_view,
), ),
@ -104,15 +93,15 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper): class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
"""Discussion settings control panel. """Discussion settings control panel."""
"""
form = DiscussionSettingsEditForm form = DiscussionSettingsEditForm
index = ViewPageTemplateFile('controlpanel.pt') index = ViewPageTemplateFile("controlpanel.pt")
def __call__(self): def __call__(self):
self.mailhost_warning() self.mailhost_warning()
self.custom_comment_workflow_warning() self.custom_comment_workflow_warning()
return super(DiscussionSettingsControlPanel, self).__call__() return super().__call__()
@property @property
def site_url(self): def site_url(self):
@ -127,39 +116,40 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
""" """
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
wftool = getToolByName(self.context, 'portal_workflow', None) wftool = getToolByName(self.context, "portal_workflow", None)
workflow_chain = wftool.getChainForPortalType('Discussion Item') workflow_chain = wftool.getChainForPortalType("Discussion Item")
output = [] output = []
# Globally enabled # Globally enabled
if settings.globally_enabled: if settings.globally_enabled:
output.append('globally_enabled') output.append("globally_enabled")
# Comment moderation # Comment moderation
one_state_worklow_disabled = \ one_state_worklow_disabled = "comment_one_state_workflow" not in workflow_chain
'comment_one_state_workflow' not in workflow_chain comment_review_workflow_disabled = (
comment_review_workflow_disabled = \ "comment_review_workflow" not in workflow_chain
'comment_review_workflow' not in workflow_chain )
if one_state_worklow_disabled and comment_review_workflow_disabled: if one_state_worklow_disabled and comment_review_workflow_disabled:
output.append('moderation_custom') output.append("moderation_custom")
elif settings.moderation_enabled: elif settings.moderation_enabled:
output.append('moderation_enabled') output.append("moderation_enabled")
if settings.edit_comment_enabled: if settings.edit_comment_enabled:
output.append('edit_comment_enabled') output.append("edit_comment_enabled")
if settings.delete_own_comment_enabled: if settings.delete_own_comment_enabled:
output.append('delete_own_comment_enabled') output.append("delete_own_comment_enabled")
# Anonymous comments # Anonymous comments
if settings.anonymous_comments: if settings.anonymous_comments:
output.append('anonymous_comments') output.append("anonymous_comments")
# Invalid mail setting # Invalid mail setting
ctrlOverview = getMultiAdapter((self.context, self.request), ctrlOverview = getMultiAdapter(
name='overview-controlpanel') (self.context, self.request), name="overview-controlpanel"
)
if ctrlOverview.mailhost_warning(): if ctrlOverview.mailhost_warning():
output.append('invalid_mail_setup') output.append("invalid_mail_setup")
# Workflow # Workflow
if workflow_chain: if workflow_chain:
@ -167,69 +157,71 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
output.append(discussion_workflow) output.append(discussion_workflow)
# Merge all settings into one string # Merge all settings into one string
return ' '.join(output) return " ".join(output)
def mailhost_warning(self): 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 # Copied from Products.CMFPlone/controlpanel/browser/overview.py
registry = getUtility(IRegistry) registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone') mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mailhost = mail_settings.smtp_host mailhost = mail_settings.smtp_host
email = mail_settings.email_from_address email = mail_settings.email_from_address
if mailhost and email: if mailhost and email:
pass pass
else: else:
message = _(u'discussion_text_no_mailhost_configured', message = _(
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 "discussion_text_no_mailhost_configured",
IStatusMessage(self.request).addStatusMessage(message, 'warning') 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): def custom_comment_workflow_warning(self):
"""Return True if a custom comment workflow is enabled.""" """Return True if a custom comment workflow is enabled."""
wftool = getToolByName(self.context, 'portal_workflow', None) wftool = getToolByName(self.context, "portal_workflow", None)
workflow_chain = wftool.getChainForPortalType('Discussion Item') workflow_chain = wftool.getChainForPortalType("Discussion Item")
one_state_workflow_enabled = \ one_state_workflow_enabled = "comment_one_state_workflow" in workflow_chain
'comment_one_state_workflow' in workflow_chain comment_review_workflow_enabled = "comment_review_workflow" in workflow_chain
comment_review_workflow_enabled = \
'comment_review_workflow' in workflow_chain
if one_state_workflow_enabled or comment_review_workflow_enabled: if one_state_workflow_enabled or comment_review_workflow_enabled:
pass pass
else: else:
message = _(u'discussion_text_custom_comment_workflow', message = _(
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 "discussion_text_custom_comment_workflow",
IStatusMessage(self.request).addStatusMessage(message, 'warning') 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): 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() portal = getSite()
wftool = getToolByName(portal, 'portal_workflow', None) wftool = getToolByName(portal, "portal_workflow", None)
if IRecordModifiedEvent.providedBy(event): if IRecordModifiedEvent.providedBy(event):
# Discussion control panel setting changed # Discussion control panel setting changed
if event.record.fieldName == 'moderation_enabled': if event.record.fieldName == "moderation_enabled":
# Moderation enabled has changed # Moderation enabled has changed
if event.record.value is True: if event.record.value is True:
# Enable moderation workflow # Enable moderation workflow
wftool.setChainForPortalTypes(('Discussion Item',), wftool.setChainForPortalTypes(
'comment_review_workflow') ("Discussion Item",), "comment_review_workflow"
)
else: else:
# Disable moderation workflow # Disable moderation workflow
wftool.setChainForPortalTypes(('Discussion Item',), wftool.setChainForPortalTypes(
'comment_one_state_workflow') ("Discussion Item",), "comment_one_state_workflow"
)
if IConfigurationChangedEvent.providedBy(event): if IConfigurationChangedEvent.providedBy(event):
# Types control panel setting changed # Types control panel setting changed
if 'workflow' in event.data: if "workflow" in event.data:
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
workflow_chain = wftool.getChainForPortalType('Discussion Item') workflow_chain = wftool.getChainForPortalType("Discussion Item")
if workflow_chain: if workflow_chain:
workflow = workflow_chain[0] workflow = workflow_chain[0]
if workflow == 'comment_one_state_workflow': if workflow == "comment_one_state_workflow":
settings.moderation_enabled = False settings.moderation_enabled = False
elif workflow == 'comment_review_workflow': elif workflow == "comment_review_workflow":
settings.moderation_enabled = True settings.moderation_enabled = True
else: else:
# Custom workflow # Custom workflow

View File

@ -1,19 +1,19 @@
# -*- coding: utf-8 -*- from ..interfaces import IDiscussionSettings
from Acquisition import aq_base from Acquisition import aq_base
from Acquisition import aq_chain from Acquisition import aq_chain
from Acquisition import aq_inner 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 plone.registry.interfaces import IRegistry
from Products.CMFCore.interfaces import IFolderish from Products.CMFCore.interfaces import IFolderish
from Products.CMFCore.utils import getToolByName 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 from zope.component import queryUtility
try: try:
from plone.dexterity.interfaces import IDexterityContent from plone.dexterity.interfaces import IDexterityContent
DEXTERITY_INSTALLED = True DEXTERITY_INSTALLED = True
except ImportError: except ImportError:
DEXTERITY_INSTALLED = False DEXTERITY_INSTALLED = False
@ -26,15 +26,14 @@ def traverse_parents(context):
if not IPloneSiteRoot.providedBy(obj): if not IPloneSiteRoot.providedBy(obj):
obj_is_folderish = IFolderish.providedBy(obj) obj_is_folderish = IFolderish.providedBy(obj)
obj_is_stuctural = not INonStructuralFolder.providedBy(obj) obj_is_stuctural = not INonStructuralFolder.providedBy(obj)
if (obj_is_folderish and obj_is_stuctural): if obj_is_folderish and obj_is_stuctural:
flag = getattr(obj, 'allow_discussion', None) flag = getattr(obj, "allow_discussion", None)
if flag is not None: if flag is not None:
return flag return flag
return None return None
class ConversationView(object): class ConversationView:
def enabled(self): def enabled(self):
if DEXTERITY_INSTALLED and IDexterityContent.providedBy(self.context): if DEXTERITY_INSTALLED and IDexterityContent.providedBy(self.context):
return self._enabled_for_dexterity_types() return self._enabled_for_dexterity_types()
@ -42,7 +41,7 @@ class ConversationView(object):
return self._enabled_for_archetypes() return self._enabled_for_archetypes()
def _enabled_for_archetypes(self): 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 This method checks five different settings in order to figure out if
discussion is enabled on a specific content object: discussion is enabled on a specific content object:
@ -82,7 +81,7 @@ class ConversationView(object):
return False return False
# If discussion is disabled for the object, bail out # 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: if obj_flag is False:
return False return False
@ -91,16 +90,16 @@ class ConversationView(object):
folder_allow_discussion = traverse_parents(context) folder_allow_discussion = traverse_parents(context)
if folder_allow_discussion: if folder_allow_discussion:
if not getattr(self, 'allow_discussion', None): if not getattr(self, "allow_discussion", None):
return True return True
else: else:
if obj_flag: if obj_flag:
return True return True
# Check if discussion is allowed on the content type # 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) 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, # If discussion is not allowed on the content type,
# check if 'allow discussion' is overridden on the content object. # check if 'allow discussion' is overridden on the content object.
if not obj_flag: if not obj_flag:
@ -109,7 +108,7 @@ class ConversationView(object):
return True return True
def _enabled_for_dexterity_types(self): 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 This method checks five different settings in order to figure out if
discussion is enable on a specific content object: discussion is enable on a specific content object:
@ -134,11 +133,11 @@ class ConversationView(object):
return False return False
# Check if discussion is allowed on the content object # 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: if context.allow_discussion is not None:
return context.allow_discussion return context.allow_discussion
# Check if discussion is allowed on the content type # 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) 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 getSecurityManager
from AccessControl import Unauthorized from AccessControl import Unauthorized
from Acquisition import aq_inner from Acquisition import aq_inner
from Acquisition import aq_parent 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 CommentPublishedEvent
from plone.app.discussion.events import CommentTransitionEvent 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 _
from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IReplies from plone.app.discussion.interfaces import IReplies
@ -18,21 +17,20 @@ from zope.event import notify
# Translations for generated values in buttons # Translations for generated values in buttons
# States # States
_('comment_pending', default='pending') _("comment_pending", default="pending")
# _('comment_approved', default='published') # _('comment_approved', default='published')
_('comment_published', default='published') _("comment_published", default="published")
_('comment_rejected', default='rejected') _("comment_rejected", default="rejected")
_('comment_spam', default='marked as spam') _("comment_spam", default="marked as spam")
# Transitions # Transitions
_('Recall') _("Recall")
_('Approve') _("Approve")
_('Reject') _("Reject")
_('Spam') _("Spam")
PMF = _ PMF = _
class TranslationHelper(BrowserView): class TranslationHelper(BrowserView):
def translate(self, text=""): def translate(self, text=""):
return _(text) return _(text)
@ -44,22 +42,21 @@ class TranslationHelper(BrowserView):
class View(BrowserView): class View(BrowserView):
"""Show comment moderation view.""" """Show comment moderation view."""
template = ViewPageTemplateFile('moderation.pt') template = ViewPageTemplateFile("moderation.pt")
try: try:
template.id = '@@moderate-comments' template.id = "@@moderate-comments"
except AttributeError: except AttributeError:
# id is not writeable in Zope 2.12 # id is not writeable in Zope 2.12
pass pass
def __init__(self, context, request): def __init__(self, context, request):
super(View, self).__init__(context, request) super().__init__(context, request)
self.workflowTool = getToolByName(self.context, 'portal_workflow') self.workflowTool = getToolByName(self.context, "portal_workflow")
self.transitions = [] self.transitions = []
def __call__(self): def __call__(self):
self.request.set('disable_border', True) self.request.set("disable_border", True)
self.request.set('review_state', self.request.set("review_state", self.request.get("review_state", "pending"))
self.request.get('review_state', 'pending'))
return self.template() return self.template()
def comments(self): def comments(self):
@ -67,15 +64,19 @@ class View(BrowserView):
review_state is string or list of strings. review_state is string or list of strings.
""" """
catalog = getToolByName(self.context, 'portal_catalog') catalog = getToolByName(self.context, "portal_catalog")
if self.request.review_state == 'all': if self.request.review_state == "all":
return catalog(object_provides=IComment.__identifier__, return catalog(
sort_on='created', object_provides=IComment.__identifier__,
sort_order='reverse') sort_on="created",
return catalog(object_provides=IComment.__identifier__, sort_order="reverse",
)
return catalog(
object_provides=IComment.__identifier__,
review_state=self.request.review_state, review_state=self.request.review_state,
sort_on='created', sort_on="created",
sort_order='reverse') sort_order="reverse",
)
def moderation_enabled(self): def moderation_enabled(self):
"""Return true if a review workflow is enabled on 'Discussion Item' """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' A 'review workflow' is characterized by implementing a 'pending'
workflow state. workflow state.
""" """
workflows = self.workflowTool.getChainForPortalType( workflows = self.workflowTool.getChainForPortalType("Discussion Item")
'Discussion Item')
if workflows: if workflows:
comment_workflow = self.workflowTool[workflows[0]] comment_workflow = self.workflowTool[workflows[0]]
if 'pending' in comment_workflow.states: if "pending" in comment_workflow.states:
return True return True
return False return False
@ -100,11 +100,10 @@ class View(BrowserView):
A 'review multipe state workflow' is characterized by implementing A 'review multipe state workflow' is characterized by implementing
a 'rejected' workflow state and a 'spam' workflow state. a 'rejected' workflow state and a 'spam' workflow state.
""" """
workflows = self.workflowTool.getChainForPortalType( workflows = self.workflowTool.getChainForPortalType("Discussion Item")
'Discussion Item')
if workflows: if workflows:
comment_workflow = self.workflowTool[workflows[0]] comment_workflow = self.workflowTool[workflows[0]]
if 'spam' in comment_workflow.states: if "spam" in comment_workflow.states:
return True return True
return False return False
@ -125,27 +124,26 @@ class View(BrowserView):
""" """
if obj: if obj:
transitions = [ transitions = [
a for a in self.workflowTool.listActionInfos(object=obj) a
if a['category'] == 'workflow' and a['allowed'] for a in self.workflowTool.listActionInfos(object=obj)
if a["category"] == "workflow" and a["allowed"]
] ]
return transitions return transitions
class ModerateCommentsEnabled(BrowserView): class ModerateCommentsEnabled(BrowserView):
def __call__(self): def __call__(self):
"""Returns true if a 'review workflow' is enabled on 'Discussion Item' """Returns true if a 'review workflow' is enabled on 'Discussion Item'
content type. A 'review workflow' is characterized by implementing content type. A 'review workflow' is characterized by implementing
a 'pending' workflow state. a 'pending' workflow state.
""" """
context = aq_inner(self.context) context = aq_inner(self.context)
workflowTool = getToolByName(context, 'portal_workflow', None) workflowTool = getToolByName(context, "portal_workflow", None)
comment_workflow = workflowTool.getChainForPortalType( comment_workflow = workflowTool.getChainForPortalType("Discussion Item")
'Discussion Item')
if comment_workflow: if comment_workflow:
comment_workflow = comment_workflow[0] comment_workflow = comment_workflow[0]
comment_workflow = workflowTool[comment_workflow] comment_workflow = workflowTool[comment_workflow]
if 'pending' in comment_workflow.states: if "pending" in comment_workflow.states:
return True return True
return False return False
@ -184,13 +182,15 @@ class DeleteComment(BrowserView):
content_object.reindexObject() content_object.reindexObject()
notify(CommentDeletedEvent(self.context, comment)) notify(CommentDeletedEvent(self.context, comment))
IStatusMessage(self.context.REQUEST).addStatusMessage( IStatusMessage(self.context.REQUEST).addStatusMessage(
_('Comment deleted.'), _("Comment deleted."), type="info"
type='info') )
came_from = self.context.REQUEST.HTTP_REFERER came_from = self.context.REQUEST.HTTP_REFERER
# if the referrer already has a came_from in it, don't redirect back # 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 if (
not getToolByName( len(came_from) == 0
content_object, 'portal_url').isURLInPortal(came_from)): or "came_from=" in came_from
or not getToolByName(content_object, "portal_url").isURLInPortal(came_from)
):
came_from = content_object.absolute_url() came_from = content_object.absolute_url()
return self.context.REQUEST.RESPONSE.redirect(came_from) return self.context.REQUEST.RESPONSE.redirect(came_from)
@ -198,8 +198,7 @@ class DeleteComment(BrowserView):
"""Returns true if current user has the 'Delete comments' """Returns true if current user has the 'Delete comments'
permission. permission.
""" """
return getSecurityManager().checkPermission('Delete comments', return getSecurityManager().checkPermission("Delete comments", aq_inner(reply))
aq_inner(reply))
class DeleteOwnComment(DeleteComment): class DeleteOwnComment(DeleteComment):
@ -213,26 +212,23 @@ class DeleteOwnComment(DeleteComment):
""" """
def could_delete(self, comment=None): 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() sm = getSecurityManager()
comment = comment or aq_inner(self.context) comment = comment or aq_inner(self.context)
userid = sm.getUser().getId() userid = sm.getUser().getId()
return ( return sm.checkPermission(
sm.checkPermission('Delete own comments', comment) and "Delete own comments", comment
'Owner' in comment.get_local_roles_for_userid(userid) ) and "Owner" in comment.get_local_roles_for_userid(userid)
)
def can_delete(self, comment=None): def can_delete(self, comment=None):
comment = comment or self.context comment = comment or self.context
return ( return len(IReplies(aq_inner(comment))) == 0 and self.could_delete(
len(IReplies(aq_inner(comment))) == 0 and comment=comment
self.could_delete(comment=comment)
) )
def __call__(self): def __call__(self):
if self.can_delete(): if self.can_delete():
super(DeleteOwnComment, self).__call__() super().__call__()
else: else:
raise Unauthorized("You're not allowed to delete this comment.") raise Unauthorized("You're not allowed to delete this comment.")
@ -262,33 +258,37 @@ class CommentTransition(BrowserView):
"""Call CommentTransition.""" """Call CommentTransition."""
comment = aq_inner(self.context) comment = aq_inner(self.context)
content_object = aq_parent(aq_parent(comment)) content_object = aq_parent(aq_parent(comment))
workflow_action = self.request.form.get('workflow_action', 'publish') workflow_action = self.request.form.get("workflow_action", "publish")
workflowTool = getToolByName(self.context, 'portal_workflow') workflowTool = getToolByName(self.context, "portal_workflow")
workflowTool.doActionFor(comment, workflow_action) workflowTool.doActionFor(comment, workflow_action)
comment.reindexObject() comment.reindexObject()
content_object.reindexObject(idxs=['total_comments']) content_object.reindexObject(idxs=["total_comments"])
notify(CommentPublishedEvent(self.context, comment)) notify(CommentPublishedEvent(self.context, comment))
# for complexer workflows: # for complexer workflows:
notify(CommentTransitionEvent(self.context, comment)) notify(CommentTransitionEvent(self.context, comment))
comment_state_translated = '' comment_state_translated = ""
if workflowTool.getWorkflowsFor(comment): 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") 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 = _( msgid = _(
"comment_transmitted", "comment_transmitted",
default='Comment ${comment_state_translated}.', default="Comment ${comment_state_translated}.",
mapping={"comment_state_translated": comment_state_translated}) mapping={"comment_state_translated": comment_state_translated},
)
translated = self.context.translate(msgid) 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 came_from = self.context.REQUEST.HTTP_REFERER
# if the referrer already has a came_from in it, don't redirect back # if the referrer already has a came_from in it, don't redirect back
if (len(came_from) == 0 if (
or 'came_from=' in came_from len(came_from) == 0
or not getToolByName( or "came_from=" in came_from
content_object, 'portal_url').isURLInPortal(came_from)): or not getToolByName(content_object, "portal_url").isURLInPortal(came_from)
):
came_from = content_object.absolute_url() came_from = content_object.absolute_url()
return self.context.REQUEST.RESPONSE.redirect(came_from) return self.context.REQUEST.RESPONSE.redirect(came_from)
@ -317,19 +317,19 @@ class BulkActionsView(BrowserView):
""" """
def __init__(self, context, request): def __init__(self, context, request):
super(BulkActionsView, self).__init__(context, request) super().__init__(context, request)
self.workflowTool = getToolByName(context, 'portal_workflow') self.workflowTool = getToolByName(context, "portal_workflow")
def __call__(self): def __call__(self):
"""Call BulkActionsView.""" """Call BulkActionsView."""
if 'form.select.BulkAction' in self.request: if "form.select.BulkAction" in self.request:
bulkaction = self.request.get('form.select.BulkAction') bulkaction = self.request.get("form.select.BulkAction")
self.paths = self.request.get('paths') self.paths = self.request.get("paths")
if self.paths: if self.paths:
if bulkaction == '-1': if bulkaction == "-1":
# no bulk action was selected # no bulk action was selected
pass pass
elif bulkaction == 'delete': elif bulkaction == "delete":
self.delete() self.delete()
else: else:
self.transmit(bulkaction) self.transmit(bulkaction)
@ -346,13 +346,14 @@ class BulkActionsView(BrowserView):
comment = context.restrictedTraverse(path) comment = context.restrictedTraverse(path)
content_object = aq_parent(aq_parent(comment)) content_object = aq_parent(aq_parent(comment))
allowed_transitions = [ allowed_transitions = [
transition['id'] for transition in self.workflowTool.listActionInfos(object=comment) transition["id"]
if transition['category'] == 'workflow' and transition['allowed'] for transition in self.workflowTool.listActionInfos(object=comment)
if transition["category"] == "workflow" and transition["allowed"]
] ]
if action in allowed_transitions: if action in allowed_transitions:
self.workflowTool.doActionFor(comment, action) self.workflowTool.doActionFor(comment, action)
comment.reindexObject() comment.reindexObject()
content_object.reindexObject(idxs=['total_comments']) content_object.reindexObject(idxs=["total_comments"])
notify(CommentPublishedEvent(content_object, comment)) notify(CommentPublishedEvent(content_object, comment))
# for complexer workflows: # for complexer workflows:
notify(CommentTransitionEvent(self.context, comment)) notify(CommentTransitionEvent(self.context, comment))
@ -370,5 +371,5 @@ class BulkActionsView(BrowserView):
conversation = aq_parent(comment) conversation = aq_parent(comment)
content_object = aq_parent(conversation) content_object = aq_parent(conversation)
del conversation[comment.id] del conversation[comment.id]
content_object.reindexObject(idxs=['total_comments']) content_object.reindexObject(idxs=["total_comments"])
notify(CommentDeletedEvent(content_object, comment)) notify(CommentDeletedEvent(content_object, comment))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*-
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
def index_object(obj, event): 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() obj.indexObject()
def unindex_object(obj, event): 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() obj.unindexObject()

View File

@ -72,7 +72,7 @@
<!-- Control panel event subscribers --> <!-- Control panel event subscribers -->
<subscriber <subscriber
for="Products.CMFPlone.interfaces.events.IConfigurationChangedEvent" for="plone.base.interfaces.events.IConfigurationChangedEvent"
handler=".browser.controlpanel.notify_configuration_changed" 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.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.discussion.interfaces import IDiscussionSettings from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.robotframework.testing import REMOTE_LIBRARY_ROBOT_TESTING from plone.app.robotframework.testing import REMOTE_LIBRARY_ROBOT_TESTING
@ -15,40 +14,43 @@ from zope.component import queryUtility
try: try:
import plone.app.collection # noqa import plone.app.collection # noqa
COLLECTION_TYPE = 'Collection'
COLLECTION_TYPE = "Collection"
except ImportError: except ImportError:
COLLECTION_TYPE = 'Topic' COLLECTION_TYPE = "Topic"
class PloneAppDiscussion(PloneSandboxLayer): class PloneAppDiscussion(PloneSandboxLayer):
defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,) defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)
USER_NAME = 'johndoe' USER_NAME = "johndoe"
USER_PASSWORD = 'secret' USER_PASSWORD = "secret"
MEMBER_NAME = 'janedoe' MEMBER_NAME = "janedoe"
MEMBER_PASSWORD = 'secret' MEMBER_PASSWORD = "secret"
USER_WITH_FULLNAME_NAME = 'jim' USER_WITH_FULLNAME_NAME = "jim"
USER_WITH_FULLNAME_FULLNAME = 'Jim Fulton' USER_WITH_FULLNAME_FULLNAME = "Jim Fulton"
USER_WITH_FULLNAME_PASSWORD = 'secret' USER_WITH_FULLNAME_PASSWORD = "secret"
MANAGER_USER_NAME = 'manager' MANAGER_USER_NAME = "manager"
MANAGER_USER_PASSWORD = 'secret' MANAGER_USER_PASSWORD = "secret"
REVIEWER_NAME = 'reviewer' REVIEWER_NAME = "reviewer"
REVIEWER_PASSWORD = 'secret' REVIEWER_PASSWORD = "secret"
def setUpZope(self, app, configurationContext): def setUpZope(self, app, configurationContext):
# Load ZCML # Load ZCML
import plone.app.discussion import plone.app.discussion
self.loadZCML(package=plone.app.discussion,
self.loadZCML(
package=plone.app.discussion,
context=configurationContext, context=configurationContext,
) )
def setUpPloneSite(self, portal): def setUpPloneSite(self, portal):
# Install into Plone site using portal_setup # Install into Plone site using portal_setup
applyProfile(portal, 'plone.app.discussion:default') applyProfile(portal, "plone.app.discussion:default")
# Creates some users # Creates some users
acl_users = getToolByName(portal, 'acl_users') acl_users = getToolByName(portal, "acl_users")
acl_users.userFolderAddUser( acl_users.userFolderAddUser(
self.USER_NAME, self.USER_NAME,
self.USER_PASSWORD, self.USER_PASSWORD,
@ -58,41 +60,42 @@ class PloneAppDiscussion(PloneSandboxLayer):
acl_users.userFolderAddUser( acl_users.userFolderAddUser(
self.MEMBER_NAME, self.MEMBER_NAME,
self.MEMBER_PASSWORD, self.MEMBER_PASSWORD,
['Member'], ["Member"],
[], [],
) )
acl_users.userFolderAddUser( acl_users.userFolderAddUser(
self.USER_WITH_FULLNAME_NAME, self.USER_WITH_FULLNAME_NAME,
self.USER_WITH_FULLNAME_PASSWORD, self.USER_WITH_FULLNAME_PASSWORD,
['Member'], ["Member"],
[], [],
) )
acl_users.userFolderAddUser( acl_users.userFolderAddUser(
self.REVIEWER_NAME, self.REVIEWER_NAME,
self.REVIEWER_PASSWORD, self.REVIEWER_PASSWORD,
['Member'], ["Member"],
[], [],
) )
mtool = getToolByName(portal, 'portal_membership', None) mtool = getToolByName(portal, "portal_membership", None)
gtool = getToolByName(portal, 'portal_groups', None) gtool = getToolByName(portal, "portal_groups", None)
gtool.addPrincipalToGroup(self.REVIEWER_NAME, 'Reviewers') gtool.addPrincipalToGroup(self.REVIEWER_NAME, "Reviewers")
mtool.addMember('jim', 'Jim', ['Member'], []) mtool.addMember("jim", "Jim", ["Member"], [])
mtool.getMemberById('jim').setMemberProperties( mtool.getMemberById("jim").setMemberProperties(
{'fullname': 'Jim Fult\xc3\xb8rn'}) {"fullname": "Jim Fult\xc3\xb8rn"}
)
acl_users.userFolderAddUser( acl_users.userFolderAddUser(
self.MANAGER_USER_NAME, self.MANAGER_USER_NAME,
self.MANAGER_USER_PASSWORD, self.MANAGER_USER_PASSWORD,
['Manager'], ["Manager"],
[], [],
) )
# Add a document # Add a document
setRoles(portal, TEST_USER_ID, ['Manager']) setRoles(portal, TEST_USER_ID, ["Manager"])
portal.invokeFactory( portal.invokeFactory(
id='doc1', id="doc1",
title='Document 1', title="Document 1",
type_name='Document', type_name="Document",
) )
@ -112,12 +115,12 @@ class PloneAppDiscussionRobot(PloneAppDiscussion):
PLONE_APP_DISCUSSION_ROBOT_FIXTURE = PloneAppDiscussionRobot() PLONE_APP_DISCUSSION_ROBOT_FIXTURE = PloneAppDiscussionRobot()
PLONE_APP_DISCUSSION_FIXTURE = PloneAppDiscussion() PLONE_APP_DISCUSSION_FIXTURE = PloneAppDiscussion()
PLONE_APP_DISCUSSION_INTEGRATION_TESTING = IntegrationTesting( PLONE_APP_DISCUSSION_INTEGRATION_TESTING = IntegrationTesting(
bases=(PLONE_APP_DISCUSSION_FIXTURE,), bases=(PLONE_APP_DISCUSSION_FIXTURE,), name="PloneAppDiscussion:Integration"
name='PloneAppDiscussion:Integration') )
PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING = FunctionalTesting( PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING = FunctionalTesting(
bases=(PLONE_APP_DISCUSSION_FIXTURE,), bases=(PLONE_APP_DISCUSSION_FIXTURE,), name="PloneAppDiscussion:Functional"
name='PloneAppDiscussion:Functional') )
PLONE_APP_DISCUSSION_ROBOT_TESTING = FunctionalTesting( PLONE_APP_DISCUSSION_ROBOT_TESTING = FunctionalTesting(
bases=(PLONE_APP_DISCUSSION_ROBOT_FIXTURE,), 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.open(urldoc)
>>> unprivileged_browser.getControl(name='form.widgets.text').value = "This is an anonymous comment" >>> 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.widgets.author_email').value = 'john@acme.com'
>>> unprivileged_browser.getControl(name='form.buttons.comment').click() >>> unprivileged_browser.getControl(name='form.buttons.comment').click()

View File

@ -1,9 +1,10 @@
# -*- coding: utf-8 -*-
"""Test the plone.app.discussion catalog indexes """Test the plone.app.discussion catalog indexes
""" """
from datetime import datetime from datetime import datetime
from plone.app.discussion.interfaces import IConversation 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 setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
@ -21,33 +22,29 @@ class CatalogSetupTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
def test_catalog_installed(self): def test_catalog_installed(self):
self.assertTrue( self.assertTrue(
'total_comments' in "total_comments" in self.portal.portal_catalog.indexes(),
self.portal.portal_catalog.indexes(),
) )
self.assertTrue( self.assertTrue(
'commentators' in "commentators" in self.portal.portal_catalog.indexes(),
self.portal.portal_catalog.indexes(),
) )
self.assertTrue( self.assertTrue(
'total_comments' in "total_comments" in self.portal.portal_catalog.schema(),
self.portal.portal_catalog.schema(),
) )
self.assertTrue( self.assertTrue(
'in_response_to' in "in_response_to" in self.portal.portal_catalog.schema(),
self.portal.portal_catalog.schema(),
) )
def test_collection_criteria_installed(self): def test_collection_criteria_installed(self):
if 'portal_atct' not in self.portal: if "portal_atct" not in self.portal:
return return
try: try:
self.portal.portal_atct.getIndex('commentators') self.portal.portal_atct.getIndex("commentators")
self.portal.portal_atct.getIndex('total_comments') self.portal.portal_atct.getIndex("total_comments")
self.portal.portal_atct.getMetadata('total_comments') self.portal.portal_atct.getMetadata("total_comments")
except AttributeError: except AttributeError:
self.fail() self.fail()
@ -57,19 +54,19 @@ class ConversationCatalogTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
workflow = self.portal.portal_workflow 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) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.title = 'Comment 1' comment1.title = "Comment 1"
comment1.text = 'Comment text' comment1.text = "Comment text"
comment1.creator = 'jim' comment1.creator = "jim"
comment1.author_username = 'Jim' comment1.author_username = "Jim"
comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12) comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12)
comment1.modification_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( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
self.conversation = conversation self.conversation = conversation
@ -91,54 +88,54 @@ class ConversationCatalogTest(unittest.TestCase):
self.new_comment1_id = new_comment1_id self.new_comment1_id = new_comment1_id
def test_total_comments(self): 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) self.assertEqual(self.doc1_brain.total_comments, 1)
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.title = 'Comment 2' comment2.title = "Comment 2"
comment2.text = 'Comment text' comment2.text = "Comment text"
new_comment2_id = self.conversation.addComment(comment2) new_comment2_id = self.conversation.addComment(comment2)
comment2 = self.portal.doc1.restrictedTraverse( comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment2_id), f"++conversation++default/{new_comment2_id}",
) )
comment2.reindexObject() comment2.reindexObject()
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
doc1_brain = brains[0] doc1_brain = brains[0]
self.assertEqual(doc1_brain.total_comments, 2) self.assertEqual(doc1_brain.total_comments, 2)
def test_last_comment_date(self): 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.assertEqual(
self.doc1_brain.last_comment_date, self.doc1_brain.last_comment_date,
datetime(2006, 9, 17, 14, 18, 12), datetime(2006, 9, 17, 14, 18, 12),
) )
# Add another comment and check if last comment date is updated. # Add another comment and check if last comment date is updated.
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.title = 'Comment 2' comment2.title = "Comment 2"
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2.creation_date = datetime(2009, 9, 17, 14, 18, 12) comment2.creation_date = datetime(2009, 9, 17, 14, 18, 12)
comment2.modification_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) new_comment2_id = self.conversation.addComment(comment2)
comment2 = self.portal.doc1.restrictedTraverse( comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment2_id), f"++conversation++default/{new_comment2_id}",
) )
comment2.reindexObject() comment2.reindexObject()
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
doc1_brain = brains[0] doc1_brain = brains[0]
@ -153,9 +150,9 @@ class ConversationCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
doc1_brain = brains[0] doc1_brain = brains[0]
@ -169,44 +166,44 @@ class ConversationCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
doc1_brain = brains[0] doc1_brain = brains[0]
self.assertEqual(doc1_brain.last_comment_date, None) self.assertEqual(doc1_brain.last_comment_date, None)
def test_commentators(self): def test_commentators(self):
self.assertTrue('commentators' in self.doc1_brain) self.assertTrue("commentators" in self.doc1_brain)
self.assertEqual(self.doc1_brain.commentators, ('Jim',)) self.assertEqual(self.doc1_brain.commentators, ("Jim",))
# add another comment with another author # add another comment with another author
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.title = 'Comment 2' comment2.title = "Comment 2"
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2.creator = 'emma' comment2.creator = "emma"
comment2.author_username = 'Emma' comment2.author_username = "Emma"
new_comment2_id = self.conversation.addComment(comment2) new_comment2_id = self.conversation.addComment(comment2)
comment2 = self.portal.doc1.restrictedTraverse( comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment2_id), f"++conversation++default/{new_comment2_id}",
) )
comment2.reindexObject() comment2.reindexObject()
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
doc1_brain = brains[0] doc1_brain = brains[0]
self.assertEqual( self.assertEqual(
sorted(doc1_brain.commentators), sorted(doc1_brain.commentators),
sorted(('Jim', 'Emma')), sorted(("Jim", "Emma")),
) )
# remove one comments # remove one comments
@ -214,22 +211,22 @@ class ConversationCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
doc1_brain = brains[0] doc1_brain = brains[0]
self.assertEqual(doc1_brain.commentators, ('Jim',)) self.assertEqual(doc1_brain.commentators, ("Jim",))
# remove all comments # remove all comments
del self.conversation[self.new_comment1_id] del self.conversation[self.new_comment1_id]
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
doc1_brain = brains[0] doc1_brain = brains[0]
@ -239,9 +236,9 @@ class ConversationCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ 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] comment1_brain = brains[0]
@ -250,14 +247,14 @@ class ConversationCatalogTest(unittest.TestCase):
self.assertEqual(comment1_brain.total_comments, None) self.assertEqual(comment1_brain.total_comments, None)
def test_dont_index_private_commentators(self): 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() self.portal.doc1.reindexObject()
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
doc1_brain = brains[0] doc1_brain = brains[0]
@ -269,70 +266,70 @@ class CommentCatalogTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) 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) conversation = IConversation(self.portal.doc1)
self.conversation = conversation self.conversation = conversation
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
comment1.creator = 'jim' comment1.creator = "jim"
comment1.author_name = 'Jim' comment1.author_name = "Jim"
new_comment1_id = conversation.addComment(comment1) new_comment1_id = conversation.addComment(comment1)
self.comment_id = new_comment1_id self.comment_id = new_comment1_id
# Comment brain # Comment brain
self.comment = self.portal.doc1.restrictedTraverse( self.comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment1_id), f"++conversation++default/{new_comment1_id}",
) )
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.comment.getPhysicalPath()), "query": "/".join(self.comment.getPhysicalPath()),
}, },
), ),
) )
self.comment_brain = brains[0] self.comment_brain = brains[0]
def test_title(self): 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): def test_no_name_title(self):
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
cid = self.conversation.addComment(comment) cid = self.conversation.addComment(comment)
# Comment brain # Comment brain
comment = self.portal.doc1.restrictedTraverse( comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(cid), f"++conversation++default/{cid}",
) )
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(comment.getPhysicalPath()), "query": "/".join(comment.getPhysicalPath()),
}, },
), ),
) )
comment_brain = brains[0] 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): def test_type(self):
self.assertEqual(self.comment_brain.portal_type, 'Discussion Item') self.assertEqual(self.comment_brain.portal_type, "Discussion Item")
self.assertEqual(self.comment_brain.Type, 'Comment') self.assertEqual(self.comment_brain.Type, "Comment")
def test_review_state(self): 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): def test_creator(self):
self.assertEqual(self.comment_brain.Creator, 'jim') self.assertEqual(self.comment_brain.Creator, "jim")
def test_in_response_to(self): def test_in_response_to(self):
"""Make sure in_response_to returns the title or id of the content """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): def test_add_comment(self):
self.assertTrue(self.comment_brain) self.assertTrue(self.comment_brain)
@ -344,7 +341,7 @@ class CommentCatalogTest(unittest.TestCase):
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.comment.getPhysicalPath()), "query": "/".join(self.comment.getPhysicalPath()),
}, },
), ),
) )
@ -352,54 +349,54 @@ class CommentCatalogTest(unittest.TestCase):
def test_reindex_comment(self): def test_reindex_comment(self):
# Make sure a comment is reindexed on the catalog when is modified # 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)) notify(ObjectModifiedEvent(self.comment))
brains = self.catalog.searchResults(SearchableText='Another text') brains = self.catalog.searchResults(SearchableText="Another text")
self.assertEqual(len(brains), 1) self.assertEqual(len(brains), 1)
def test_remove_comments_when_content_object_is_removed(self): def test_remove_comments_when_content_object_is_removed(self):
"""Make sure all comments are removed from the catalog, if the content """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.assertEqual(len(brains), 1)
self.portal.manage_delObjects(['doc1']) self.portal.manage_delObjects(["doc1"])
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'}) brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
self.assertEqual(len(brains), 0) self.assertEqual(len(brains), 0)
def test_move_comments_when_content_object_is_moved(self): def test_move_comments_when_content_object_is_moved(self):
# Create two folders and a content object with a comment # Create two folders and a content object with a comment
self.portal.invokeFactory( self.portal.invokeFactory(
id='folder1', id="folder1",
title='Folder 1', title="Folder 1",
type_name='Folder', type_name="Folder",
) )
self.portal.invokeFactory( self.portal.invokeFactory(
id='folder2', id="folder2",
title='Folder 2', title="Folder 2",
type_name='Folder', type_name="Folder",
) )
self.portal.folder1.invokeFactory( self.portal.folder1.invokeFactory(
id='moveme', id="moveme",
title='Move Me', title="Move Me",
type_name='Document', type_name="Document",
) )
conversation = IConversation(self.portal.folder1.moveme) conversation = IConversation(self.portal.folder1.moveme)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment_id = conversation.addComment(comment) comment_id = conversation.addComment(comment)
# We need to commit here so that _p_jar isn't None and move will work # We need to commit here so that _p_jar isn't None and move will work
transaction.savepoint(optimistic=True) transaction.savepoint(optimistic=True)
# Move moveme from folder1 to folder2 # 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) self.portal.folder2.manage_pasteObjects(cp)
# Make sure no old comment brains are # Make sure no old comment brains are
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
portal_type='Discussion Item', portal_type="Discussion Item",
path={ 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( brains = self.catalog.searchResults(
dict( dict(
portal_type='Discussion Item', portal_type="Discussion Item",
path={ path={
'query': '/'.join(self.portal.folder2.getPhysicalPath()), "query": "/".join(self.portal.folder2.getPhysicalPath()),
}, },
), ),
) )
self.assertEqual(len(brains), 1) self.assertEqual(len(brains), 1)
self.assertEqual( self.assertEqual(
brains[0].getPath(), brains[0].getPath(),
'/plone/folder2/moveme/++conversation++default/' + "/plone/folder2/moveme/++conversation++default/" + str(comment_id),
str(comment_id),
) )
def test_move_upper_level_folder(self): def test_move_upper_level_folder(self):
# create a folder with a nested structure # create a folder with a nested structure
self.portal.invokeFactory( self.portal.invokeFactory(
id='sourcefolder', id="sourcefolder",
title='Source Folder', title="Source Folder",
type_name='Folder', type_name="Folder",
) )
self.portal.sourcefolder.invokeFactory( self.portal.sourcefolder.invokeFactory(
id='moveme', id="moveme",
title='Move Me', title="Move Me",
type_name='Folder', type_name="Folder",
) )
self.portal.sourcefolder.moveme.invokeFactory( self.portal.sourcefolder.moveme.invokeFactory(
id='mydocument', id="mydocument",
title='My Document', title="My Document",
type_name='Folder', type_name="Folder",
) )
self.portal.invokeFactory( self.portal.invokeFactory(
id='targetfolder', id="targetfolder",
title='Target Folder', title="Target Folder",
type_name='Folder', type_name="Folder",
) )
# create comment on my-document # create comment on my-document
conversation = IConversation( conversation = IConversation(
self.portal.sourcefolder.moveme.mydocument, self.portal.sourcefolder.moveme.mydocument,
) )
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment_id = conversation.addComment(comment) comment_id = conversation.addComment(comment)
# We need to commit here so that _p_jar isn't None and move will work # We need to commit here so that _p_jar isn't None and move will work
transaction.savepoint(optimistic=True) transaction.savepoint(optimistic=True)
# Move moveme from folder1 to folder2 # 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) self.portal.targetfolder.manage_pasteObjects(cp)
# Make sure no old comment brains are left # Make sure no old comment brains are left
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
portal_type='Discussion Item', portal_type="Discussion Item",
path={'query': '/plone/sourcefolder/moveme'}, path={"query": "/plone/sourcefolder/moveme"},
), ),
) )
self.assertEqual(len(brains), 0) self.assertEqual(len(brains), 0)
@ -469,49 +465,47 @@ class CommentCatalogTest(unittest.TestCase):
# make sure comments are correctly index on the target # make sure comments are correctly index on the target
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
portal_type='Discussion Item', portal_type="Discussion Item",
path={'query': '/plone/targetfolder/moveme'}, path={"query": "/plone/targetfolder/moveme"},
), ),
) )
self.assertEqual(len(brains), 1) self.assertEqual(len(brains), 1)
self.assertEqual( self.assertEqual(
brains[0].getPath(), brains[0].getPath(),
'/plone/targetfolder/moveme/mydocument/++conversation++default/' + "/plone/targetfolder/moveme/mydocument/++conversation++default/"
str(comment_id), + str(comment_id),
) )
def test_update_comments_when_content_object_is_renamed(self): 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 # We need to commit here so that _p_jar isn't None and move will work
transaction.savepoint(optimistic=True) transaction.savepoint(optimistic=True)
self.portal.manage_renameObject('doc1', 'doc2') self.portal.manage_renameObject("doc1", "doc2")
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
portal_type='Discussion Item', portal_type="Discussion Item",
) )
self.assertEqual(len(brains), 1) self.assertEqual(len(brains), 1)
self.assertEqual( self.assertEqual(
brains[0].getPath(), brains[0].getPath(),
'/plone/doc2/++conversation++default/' + "/plone/doc2/++conversation++default/" + str(self.comment_id),
str(self.comment_id),
) )
def test_clear_and_rebuild_catalog(self): 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) self.assertTrue(brains)
# Clear and rebuild catalog # Clear and rebuild catalog
self.catalog.clearFindAndRebuild() self.catalog.clearFindAndRebuild()
# Check if comment is still there # 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) self.assertTrue(brains)
comment_brain = brains[0] 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( self.assertEqual(
comment_brain.getPath(), comment_brain.getPath(),
'/plone/doc1/++conversation++default/' + "/plone/doc1/++conversation++default/" + str(self.comment_id),
str(self.comment_id),
) )
def test_clear_and_rebuild_catalog_for_nested_comments(self): def test_clear_and_rebuild_catalog_for_nested_comments(self):
@ -526,25 +520,25 @@ class CommentCatalogTest(unittest.TestCase):
# +- Comment 2 # +- Comment 2
# +- Comment 2_1 # +- Comment 2_1
comment1_1 = createObject('plone.Comment') comment1_1 = createObject("plone.Comment")
comment1_1.title = 'Re: Comment 1' comment1_1.title = "Re: Comment 1"
comment1_1.text = 'Comment text' comment1_1.text = "Comment text"
comment1_1_1 = createObject('plone.Comment') comment1_1_1 = createObject("plone.Comment")
comment1_1_1.title = 'Re: Re: Comment 1' comment1_1_1.title = "Re: Re: Comment 1"
comment1_1_1.text = 'Comment text' comment1_1_1.text = "Comment text"
comment1_2 = createObject('plone.Comment') comment1_2 = createObject("plone.Comment")
comment1_2.title = 'Re: Comment 1 (2)' comment1_2.title = "Re: Comment 1 (2)"
comment1_2.text = 'Comment text' comment1_2.text = "Comment text"
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.title = 'Comment 2' comment2.title = "Comment 2"
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2_1 = createObject('plone.Comment') comment2_1 = createObject("plone.Comment")
comment2_1.title = 'Re: Comment 2' comment2_1.title = "Re: Comment 2"
comment2_1.text = 'Comment text' comment2_1.text = "Comment text"
# Create the nested comment structure # Create the nested comment structure
new_id_1 = self.conversation.addComment(self.comment) new_id_1 = self.conversation.addComment(self.comment)
@ -566,7 +560,7 @@ class CommentCatalogTest(unittest.TestCase):
self.catalog.clearFindAndRebuild() self.catalog.clearFindAndRebuild()
# Check if comments are still there # 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.assertTrue(brains)
self.assertEqual(len(brains), 6) self.assertEqual(len(brains), 6)
@ -576,19 +570,19 @@ class NoConversationCatalogTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) 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) conversation = IConversation(self.portal.doc1)
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
dict( dict(
path={ path={
'query': '/'.join(self.portal.doc1.getPhysicalPath()), "query": "/".join(self.portal.doc1.getPhysicalPath()),
}, },
portal_type='Document', portal_type="Document",
), ),
) )
self.conversation = conversation self.conversation = conversation
@ -596,11 +590,10 @@ class NoConversationCatalogTest(unittest.TestCase):
self.doc1_brain = brains[0] self.doc1_brain = brains[0]
def test_total_comments(self): 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) self.assertEqual(self.doc1_brain.total_comments, 0)
# Make sure no conversation has been created # Make sure no conversation has been created
self.assertTrue( self.assertTrue(
'plone.app.discussion:conversation' not in "plone.app.discussion:conversation" not in IAnnotations(self.portal.doc1),
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.browser.comment import View
from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IReplies 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 setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
@ -12,11 +11,10 @@ from zope.component import getMultiAdapter
import datetime import datetime
import logging import logging
import six
import unittest import unittest
logger = logging.getLogger('plone.app.discussion.tests') logger = logging.getLogger("plone.app.discussion.tests")
logger.addHandler(logging.StreamHandler()) logger.addHandler(logging.StreamHandler())
@ -25,30 +23,30 @@ class CommentTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
self.request = self.layer['request'] self.request = self.layer["request"]
workflow = self.portal.portal_workflow workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish') workflow.doActionFor(self.portal.doc1, "publish")
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.catalog = getToolByName(self.portal, 'portal_catalog') self.catalog = getToolByName(self.portal, "portal_catalog")
self.document_brain = self.catalog.searchResults( self.document_brain = self.catalog.searchResults(portal_type="Document")[0]
portal_type='Document')[0]
def test_factory(self): def test_factory(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
self.assertTrue(IComment.providedBy(comment1)) self.assertTrue(IComment.providedBy(comment1))
def test_UTCDates(self): def test_UTCDates(self):
utc_to_local_diff = \ utc_to_local_diff = datetime.datetime.now() - datetime.datetime.utcnow()
datetime.datetime.now() - datetime.datetime.utcnow()
utc_to_local_diff = abs(utc_to_local_diff.seconds) utc_to_local_diff = abs(utc_to_local_diff.seconds)
if utc_to_local_diff < 60: if utc_to_local_diff < 60:
logger.warning('Your computer is living in a timezone where local ' logger.warning(
'time equals utc time. Some potential errors can ' "Your computer is living in a timezone where local "
'get hidden by that') "time equals utc time. Some potential errors can "
comment1 = createObject('plone.Comment') "get hidden by that"
)
comment1 = createObject("plone.Comment")
local_utc = datetime.datetime.utcnow() local_utc = datetime.datetime.utcnow()
for date in (comment1.creation_date, comment1.modification_date): for date in (comment1.creation_date, comment1.modification_date):
difference = abs(date - local_utc) difference = abs(date - local_utc)
@ -58,171 +56,163 @@ class CommentTest(unittest.TestCase):
self.assertFalse(difference // 10) self.assertFalse(difference // 10)
def test_id(self): def test_id(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.comment_id = 123 comment1.comment_id = 123
self.assertEqual('123', comment1.id) self.assertEqual("123", comment1.id)
self.assertEqual('123', comment1.getId()) self.assertEqual("123", comment1.getId())
self.assertEqual(u'123', comment1.__name__) self.assertEqual("123", comment1.__name__)
def test_uid(self): def test_uid(self):
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
conversation.addComment(comment1) conversation.addComment(comment1)
comment_brain = self.catalog.searchResults( comment_brain = self.catalog.searchResults(
portal_type='Discussion Item', portal_type="Discussion Item",
)[0] )[0]
self.assertTrue(comment_brain.UID) self.assertTrue(comment_brain.UID)
def test_uid_is_unique(self): def test_uid_is_unique(self):
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
conversation.addComment(comment1) conversation.addComment(comment1)
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
conversation.addComment(comment2) conversation.addComment(comment2)
brains = self.catalog.searchResults( brains = self.catalog.searchResults(
portal_type='Discussion Item', portal_type="Discussion Item",
) )
self.assertNotEqual(brains[0].UID, brains[1].UID) self.assertNotEqual(brains[0].UID, brains[1].UID)
def test_comment_uid_differs_from_content_uid(self): def test_comment_uid_differs_from_content_uid(self):
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
conversation.addComment(comment1) conversation.addComment(comment1)
comment_brain = self.catalog.searchResults( comment_brain = self.catalog.searchResults(
portal_type='Discussion Item', portal_type="Discussion Item",
)[0] )[0]
self.assertNotEqual(self.document_brain.UID, comment_brain.UID) self.assertNotEqual(self.document_brain.UID, comment_brain.UID)
def test_title(self): def test_title(self):
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.author_name = 'Jim Fulton' comment1.author_name = "Jim Fulton"
conversation.addComment(comment1) 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): def test_no_name_title(self):
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
conversation.addComment(comment1) 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): def test_title_special_characters(self):
self.portal.invokeFactory( self.portal.invokeFactory(
id='doc_sp_chars', id="doc_sp_chars",
title=u'Document äüö', title="Document äüö",
type_name='Document', type_name="Document",
) )
conversation = IConversation(self.portal.doc_sp_chars) conversation = IConversation(self.portal.doc_sp_chars)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.author_name = u'Tarek Ziadé' comment1.author_name = "Tarek Ziadé"
conversation.addComment(comment1) 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): def test_title_special_characters_utf8(self):
self.portal.invokeFactory( self.portal.invokeFactory(
id='doc_sp_chars_utf8', id="doc_sp_chars_utf8",
title='Document ëïû', title="Document ëïû",
type_name='Document', type_name="Document",
) )
conversation = IConversation(self.portal.doc_sp_chars_utf8) conversation = IConversation(self.portal.doc_sp_chars_utf8)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.author_name = 'Hüüb Bôûmä' comment1.author_name = "Hüüb Bôûmä"
conversation.addComment(comment1) 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): def test_creator(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.creator = 'jim' comment1.creator = "jim"
self.assertEqual('jim', comment1.Creator()) self.assertEqual("jim", comment1.Creator())
def test_creator_author_name(self): def test_creator_author_name(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.author_name = 'joey' comment1.author_name = "joey"
self.assertEqual('joey', comment1.Creator()) self.assertEqual("joey", comment1.Creator())
def test_owner(self): def test_owner(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
self.assertEqual((['plone', 'acl_users'], TEST_USER_ID), self.assertEqual(
comment1.getOwnerTuple()) (["plone", "acl_users"], TEST_USER_ID), comment1.getOwnerTuple()
)
def test_type(self): def test_type(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
self.assertEqual(comment1.Type(), 'Comment') self.assertEqual(comment1.Type(), "Comment")
def test_mime_type(self): def test_mime_type(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
self.assertEqual(comment1.mime_type, 'text/plain') self.assertEqual(comment1.mime_type, "text/plain")
def test_getText(self): def test_getText(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'First paragraph\n\nSecond_paragraph' comment1.text = "First paragraph\n\nSecond_paragraph"
self.assertEqual( self.assertEqual(
''.join(comment1.getText().split()), "".join(comment1.getText().split()),
'<p>Firstparagraph<br><br>Second_paragraph</p>', "<p>Firstparagraph<br><br>Second_paragraph</p>",
) )
def test_getText_escapes_HTML(self): def test_getText_escapes_HTML(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = '<b>Got HTML?</b>' comment1.text = "<b>Got HTML?</b>"
self.assertEqual( self.assertEqual(
comment1.getText(), 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): def test_getText_with_non_ascii_characters(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = u'Umlaute sind ä, ö und ü.' comment1.text = "Umlaute sind ä, ö und ü."
out = b'<p>Umlaute sind \xc3\xa4, \xc3\xb6 und \xc3\xbc.</p>' out = b"<p>Umlaute sind \xc3\xa4, \xc3\xb6 und \xc3\xbc.</p>"
if six.PY2: self.assertEqual(comment1.getText(), out.decode("utf8"))
self.assertEqual(
comment1.getText(),
out)
else:
self.assertEqual(
comment1.getText(),
out.decode('utf8'))
def test_getText_doesnt_link(self): def test_getText_doesnt_link(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Go to http://www.plone.org' comment1.text = "Go to http://www.plone.org"
self.assertEqual( self.assertEqual(
comment1.getText(), 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): def test_getText_uses_comment_mime_type(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Go to http://www.plone.org' comment1.text = "Go to http://www.plone.org"
comment1.mime_type = 'text/x-web-intelligent' comment1.mime_type = "text/x-web-intelligent"
self.assertEqual( self.assertEqual(
comment1.getText(), comment1.getText(),
'Go to <a href="http://www.plone.org" ' + 'Go to <a href="http://www.plone.org" '
'rel="nofollow">http://www.plone.org</a>', + 'rel="nofollow">http://www.plone.org</a>',
) )
def test_getText_uses_comment_mime_type_html(self): 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.text = 'Go to <a href="http://www.plone.org">plone.org</a>'
comment1.mime_type = 'text/html' comment1.mime_type = "text/html"
self.assertEqual( self.assertEqual(
comment1.getText(), comment1.getText(),
'Go to <a href="http://www.plone.org">plone.org</a>', 'Go to <a href="http://www.plone.org">plone.org</a>',
) )
def test_getText_w_custom_targetMimetype(self): def test_getText_w_custom_targetMimetype(self):
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'para' comment1.text = "para"
self.assertEqual(comment1.getText(targetMimetype='text/plain'), 'para') self.assertEqual(comment1.getText(targetMimetype="text/plain"), "para")
def test_getText_invalid_transformation_raises_error(self): def test_getText_invalid_transformation_raises_error(self):
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.mime_type = 'text/x-html-safe' comment1.mime_type = "text/x-html-safe"
comment1.text = 'para' comment1.text = "para"
conversation.addComment(comment1) conversation.addComment(comment1)
self.assertEqual( self.assertEqual(comment1.getText(targetMimetype="text/html"), "para")
comment1.getText(targetMimetype='text/html'),
'para')
def test_traversal(self): def test_traversal(self):
# make sure comments are traversable, have an id, absolute_url and # make sure comments are traversable, have an id, absolute_url and
@ -230,26 +220,29 @@ class CommentTest(unittest.TestCase):
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
new_comment1_id = conversation.addComment(comment1) new_comment1_id = conversation.addComment(comment1)
comment = self.portal.doc1.restrictedTraverse( comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment1_id), f"++conversation++default/{new_comment1_id}",
) )
self.assertTrue(IComment.providedBy(comment)) self.assertTrue(IComment.providedBy(comment))
self.assertEqual( self.assertEqual(
( (
'', 'plone', 'doc1', '++conversation++default', "",
"plone",
"doc1",
"++conversation++default",
str(new_comment1_id), str(new_comment1_id),
), ),
comment.getPhysicalPath(), comment.getPhysicalPath(),
) )
self.assertEqual( self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' + "http://nohost/plone/doc1/++conversation++default/" + str(new_comment1_id),
str(new_comment1_id), comment.absolute_url(), comment.absolute_url(),
) )
def test_view_blob_types(self): def test_view_blob_types(self):
@ -258,68 +251,67 @@ class CommentTest(unittest.TestCase):
version of the url with a /view in it. version of the url with a /view in it.
""" """
self.portal.invokeFactory( self.portal.invokeFactory(
id='image1', id="image1",
title='Image', title="Image",
type_name='Image', type_name="Image",
) )
conversation = IConversation(self.portal.image1) conversation = IConversation(self.portal.image1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
new_comment1_id = conversation.addComment(comment1) new_comment1_id = conversation.addComment(comment1)
comment = self.portal.image1.restrictedTraverse( comment = self.portal.image1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment1_id), f"++conversation++default/{new_comment1_id}",
) )
view = View(comment, self.request) view = View(comment, self.request)
View.__call__(view) View.__call__(view)
response = self.request.response response = self.request.response
self.assertIn('/view', response.headers['location']) self.assertIn("/view", response.headers["location"])
def test_workflow(self): def test_workflow(self):
"""Basic test for the 'comment_review_workflow' """Basic test for the 'comment_review_workflow'"""
"""
self.portal.portal_workflow.setChainForPortalTypes( self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",),
('comment_review_workflow,'), ("comment_review_workflow,"),
) )
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
new_comment1_id = conversation.addComment(comment1) new_comment1_id = conversation.addComment(comment1)
comment = conversation[new_comment1_id] comment = conversation[new_comment1_id]
# Make sure comments use the 'comment_review_workflow' # Make sure comments use the 'comment_review_workflow'
chain = self.portal.portal_workflow.getChainFor(comment) 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 # Ensure the initial state was entered and recorded
self.assertEqual( self.assertEqual(
1, 1,
len(comment.workflow_history['comment_review_workflow']), len(comment.workflow_history["comment_review_workflow"]),
) )
self.assertEqual( self.assertEqual(
None, None,
comment.workflow_history['comment_review_workflow'][0]['action'], comment.workflow_history["comment_review_workflow"][0]["action"],
) )
self.assertEqual( self.assertEqual(
'pending', "pending",
self.portal.portal_workflow.getInfoFor(comment, 'review_state'), self.portal.portal_workflow.getInfoFor(comment, "review_state"),
) )
def test_fti(self): def test_fti(self):
# test that we can look up an FTI for Discussion Item # test that we can look up an FTI for Discussion Item
self.assertIn( self.assertIn(
'Discussion Item', "Discussion Item",
self.portal.portal_types.objectIds(), self.portal.portal_types.objectIds(),
) )
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
fti = self.portal.portal_types.getTypeInfo(comment1) 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): def test_view(self):
# make sure that the comment view is there and redirects to the right # 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) conversation = IConversation(self.portal.doc1)
# Create a comment # Create a comment
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
# Add comment to the conversation # Add comment to the conversation
new_comment1_id = conversation.addComment(comment1) new_comment1_id = conversation.addComment(comment1)
comment = self.portal.doc1.restrictedTraverse( comment = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_comment1_id), f"++conversation++default/{new_comment1_id}",
) )
# make sure the view is there # make sure the view is there
self.assertTrue( self.assertTrue(
getMultiAdapter( getMultiAdapter(
(comment, self.request), (comment, self.request),
name='view', name="view",
), ),
) )
@ -362,11 +354,11 @@ class RepliesTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
workflow = self.portal.portal_workflow workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish') workflow.doActionFor(self.portal.doc1, "publish")
def test_add_comment(self): def test_add_comment(self):
# Add comments to a CommentReplies adapter # Add comments to a CommentReplies adapter
@ -378,16 +370,16 @@ class RepliesTest(unittest.TestCase):
# Add a comment to the conversation # Add a comment to the conversation
replies = IReplies(conversation) replies = IReplies(conversation)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
new_id = replies.addComment(comment) new_id = replies.addComment(comment)
comment = self.portal.doc1.restrictedTraverse( 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 # Add a reply to the CommentReplies adapter of the first comment
re_comment = createObject('plone.Comment') re_comment = createObject("plone.Comment")
re_comment.text = 'Comment text' re_comment.text = "Comment text"
replies = IReplies(comment) replies = IReplies(comment)
@ -415,16 +407,16 @@ class RepliesTest(unittest.TestCase):
# Add a comment to the conversation # Add a comment to the conversation
replies = IReplies(conversation) replies = IReplies(conversation)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
new_id = replies.addComment(comment) new_id = replies.addComment(comment)
comment = self.portal.doc1.restrictedTraverse( 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 # Add a reply to the CommentReplies adapter of the first comment
re_comment = createObject('plone.Comment') re_comment = createObject("plone.Comment")
re_comment.text = 'Comment text' re_comment.text = "Comment text"
replies = IReplies(comment) replies = IReplies(comment)
@ -446,83 +438,86 @@ class RepliesTest(unittest.TestCase):
# physical path # physical path
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
conversation.addComment(comment1) conversation.addComment(comment1)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
new_id = conversation.addComment(comment) new_id = conversation.addComment(comment)
comment = self.portal.doc1.restrictedTraverse( 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 # Add a reply to the CommentReplies adapter of the first comment
re_comment = createObject('plone.Comment') re_comment = createObject("plone.Comment")
re_comment.text = 'Comment text' re_comment.text = "Comment text"
replies = IReplies(comment) replies = IReplies(comment)
new_re_id = replies.addComment(re_comment) new_re_id = replies.addComment(re_comment)
re_comment = self.portal.doc1.restrictedTraverse( 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 # Add a reply to the reply
re_re_comment = createObject('plone.Comment') re_re_comment = createObject("plone.Comment")
re_re_comment.text = 'Comment text' re_re_comment.text = "Comment text"
replies = IReplies(re_comment) replies = IReplies(re_comment)
new_re_re_id = replies.addComment(re_re_comment) new_re_re_id = replies.addComment(re_re_comment)
re_re_comment = self.portal.doc1.restrictedTraverse( 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 # Add a reply to the replies reply
re_re_re_comment = createObject('plone.Comment') re_re_re_comment = createObject("plone.Comment")
re_re_re_comment.text = 'Comment text' re_re_re_comment.text = "Comment text"
replies = IReplies(re_re_comment) replies = IReplies(re_re_comment)
new_re_re_re_id = replies.addComment(re_re_re_comment) new_re_re_re_id = replies.addComment(re_re_re_comment)
re_re_re_comment = self.portal.doc1.restrictedTraverse( 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( self.assertEqual(
('', 'plone', 'doc1', '++conversation++default', str(new_id)), ("", "plone", "doc1", "++conversation++default", str(new_id)),
comment.getPhysicalPath(), comment.getPhysicalPath(),
) )
self.assertEqual( self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' + "http://nohost/plone/doc1/++conversation++default/" + str(new_id),
str(new_id), comment.absolute_url(), comment.absolute_url(),
) )
self.assertEqual( self.assertEqual(
('', 'plone', 'doc1', '++conversation++default', str(new_re_id)), ("", "plone", "doc1", "++conversation++default", str(new_re_id)),
re_comment.getPhysicalPath(), re_comment.getPhysicalPath(),
) )
self.assertEqual( self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' + "http://nohost/plone/doc1/++conversation++default/" + str(new_re_id),
str(new_re_id),
re_comment.absolute_url(), re_comment.absolute_url(),
) )
self.assertEqual( self.assertEqual(
( (
'', 'plone', 'doc1', '++conversation++default', "",
"plone",
"doc1",
"++conversation++default",
str(new_re_re_id), str(new_re_re_id),
), ),
re_re_comment.getPhysicalPath(), re_re_comment.getPhysicalPath(),
) )
self.assertEqual( self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' + "http://nohost/plone/doc1/++conversation++default/" + str(new_re_re_id),
str(new_re_re_id),
re_re_comment.absolute_url(), re_re_comment.absolute_url(),
) )
self.assertEqual( self.assertEqual(
( (
'', 'plone', 'doc1', '++conversation++default', "",
"plone",
"doc1",
"++conversation++default",
str(new_re_re_re_id), str(new_re_re_re_id),
), ),
re_re_re_comment.getPhysicalPath(), re_re_re_comment.getPhysicalPath(),
) )
self.assertEqual( self.assertEqual(
'http://nohost/plone/doc1/++conversation++default/' + "http://nohost/plone/doc1/++conversation++default/" + str(new_re_re_re_id),
str(new_re_re_re_id),
re_re_re_comment.absolute_url(), 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 AccessControl import Unauthorized
from datetime import datetime from datetime import datetime
from OFS.Image import Image 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 login
from plone.app.testing import logout from plone.app.testing import logout
from plone.app.testing import setRoles 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.app.testing import TEST_USER_NAME
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from Products.CMFPlone.tests import dummy
from z3c.form.interfaces import IFormLayer from z3c.form.interfaces import IFormLayer
from zope import interface from zope import interface
from zope.annotation.interfaces import IAttributeAnnotatable from zope.annotation.interfaces import IAttributeAnnotatable
@ -28,33 +26,67 @@ from zope.interface import alsoProvides
from zope.interface import Interface from zope.interface import Interface
from zope.publisher.browser import TestRequest from zope.publisher.browser import TestRequest
from zope.publisher.interfaces.browser import IBrowserRequest from zope.publisher.interfaces.browser import IBrowserRequest
from ZPublisher.HTTPRequest import FileUpload
import io
import time import time
import unittest 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): class TestCommentForm(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
self.request = self.layer['request'] self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.portal.invokeFactory('Folder', 'test-folder') self.portal.invokeFactory("Folder", "test-folder")
self.folder = self.portal['test-folder'] self.folder = self.portal["test-folder"]
interface.alsoProvides( interface.alsoProvides(
self.portal.REQUEST, self.portal.REQUEST,
interfaces.IDiscussionLayer, interfaces.IDiscussionLayer,
) )
wftool = getToolByName(self.portal, 'portal_workflow') wftool = getToolByName(self.portal, "portal_workflow")
wftool.doActionFor(self.portal.doc1, action='publish') wftool.doActionFor(self.portal.doc1, action="publish")
self.portal.doc1.allow_discussion = True 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.memberdata = self.portal.portal_memberdata
self.context = getattr(self.portal, 'doc1') self.context = getattr(self.portal, "doc1")
# Allow discussion # Allow discussion
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
@ -62,8 +94,7 @@ class TestCommentForm(unittest.TestCase):
settings.globally_enabled = True settings.globally_enabled = True
def test_add_comment(self): def test_add_comment(self):
"""Post a comment as logged-in user. """Post a comment as logged-in user."""
"""
# Allow discussion # Allow discussion
self.portal.doc1.allow_discussion = True self.portal.doc1.allow_discussion = True
@ -80,7 +111,7 @@ class TestCommentForm(unittest.TestCase):
adapts=(Interface, IBrowserRequest), adapts=(Interface, IBrowserRequest),
provides=Interface, provides=Interface,
factory=CommentForm, factory=CommentForm,
name=u'comment-form', name="comment-form",
) )
# The form should return an error if the comment text field is empty # The form should return an error if the comment text field is empty
@ -88,46 +119,45 @@ class TestCommentForm(unittest.TestCase):
commentForm = getMultiAdapter( commentForm = getMultiAdapter(
(self.context, request), (self.context, request),
name=u'comment-form', name="comment-form",
) )
commentForm.update() commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612 data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 1) 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 # The form is submitted successfully, if the required text field is
# filled out # filled out
request = make_request(form={'form.widgets.text': u'bar'}) request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter( commentForm = getMultiAdapter(
(self.context, request), (self.context, request),
name=u'comment-form', name="comment-form",
) )
commentForm.update() commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612 data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0) self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'foo')) self.assertFalse(commentForm.handleComment(commentForm, "foo"))
comments = IConversation(commentForm.context).getComments() comments = IConversation(commentForm.context).getComments()
comments = [comment for comment in comments] # consume iterator comments = [comment for comment in comments] # consume iterator
self.assertEqual(len(comments), 1) self.assertEqual(len(comments), 1)
for comment in comments: for comment in comments:
self.assertEqual(comment.text, u'bar') self.assertEqual(comment.text, "bar")
self.assertEqual(comment.creator, 'test_user_1_') 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() local_roles = comment.get_local_roles()
self.assertEqual(len(local_roles), 1) self.assertEqual(len(local_roles), 1)
userid, roles = local_roles[0] userid, roles = local_roles[0]
self.assertEqual(userid, 'test_user_1_') self.assertEqual(userid, "test_user_1_")
self.assertEqual(len(roles), 1) self.assertEqual(len(roles), 1)
self.assertEqual(roles[0], 'Owner') self.assertEqual(roles[0], "Owner")
def test_edit_comment(self): def test_edit_comment(self):
"""Edit a comment as logged-in user. """Edit a comment as logged-in user."""
"""
# Allow discussion # Allow discussion
self.portal.doc1.allow_discussion = True self.portal.doc1.allow_discussion = True
@ -144,65 +174,64 @@ class TestCommentForm(unittest.TestCase):
adapts=(Interface, IBrowserRequest), adapts=(Interface, IBrowserRequest),
provides=Interface, provides=Interface,
factory=CommentForm, factory=CommentForm,
name=u'comment-form', name="comment-form",
) )
provideAdapter( provideAdapter(
adapts=(Interface, IBrowserRequest), adapts=(Interface, IBrowserRequest),
provides=Interface, provides=Interface,
factory=EditCommentForm, factory=EditCommentForm,
name=u'edit-comment-form', name="edit-comment-form",
) )
# The form is submitted successfully, if the required text field is # The form is submitted successfully, if the required text field is
# filled out # filled out
request = make_request(form={'form.widgets.text': u'bar'}) request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter( commentForm = getMultiAdapter(
(self.context, request), (self.context, request),
name=u'comment-form', name="comment-form",
) )
commentForm.update() commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612 data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0) self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'foo')) self.assertFalse(commentForm.handleComment(commentForm, "foo"))
# Edit the last comment # Edit the last comment
conversation = IConversation(self.context) conversation = IConversation(self.context)
comment = [x for x in conversation.getComments()][-1] 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( editForm = getMultiAdapter(
(comment, request), (comment, request),
name=u'edit-comment-form', name="edit-comment-form",
) )
editForm.update() editForm.update()
data, errors = editForm.extractData() # pylint: disable-msg=W0612 data, errors = editForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0) 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] 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 = IConversation(commentForm.context).getComments()
comments = [c for c in comments] # consume iterator comments = [c for c in comments] # consume iterator
self.assertEqual(len(comments), 1) self.assertEqual(len(comments), 1)
for comment in comments: for comment in comments:
self.assertEqual(comment.text, u'foobar') self.assertEqual(comment.text, "foobar")
self.assertEqual(comment.creator, 'test_user_1_') 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() local_roles = comment.get_local_roles()
self.assertEqual(len(local_roles), 1) self.assertEqual(len(local_roles), 1)
userid, roles = local_roles[0] userid, roles = local_roles[0]
self.assertEqual(userid, 'test_user_1_') self.assertEqual(userid, "test_user_1_")
self.assertEqual(len(roles), 1) self.assertEqual(len(roles), 1)
self.assertEqual(roles[0], 'Owner') self.assertEqual(roles[0], "Owner")
def test_delete_comment(self): def test_delete_comment(self):
"""Delete a comment as logged-in user. """Delete a comment as logged-in user."""
"""
# Allow discussion # Allow discussion
self.portal.doc1.allow_discussion = True self.portal.doc1.allow_discussion = True
@ -219,48 +248,47 @@ class TestCommentForm(unittest.TestCase):
adapts=(Interface, IBrowserRequest), adapts=(Interface, IBrowserRequest),
provides=Interface, provides=Interface,
factory=CommentForm, factory=CommentForm,
name=u'comment-form', name="comment-form",
) )
# The form is submitted successfully, if the required text field is # The form is submitted successfully, if the required text field is
# filled out # filled out
form_request = make_request(form={'form.widgets.text': u'bar'}) form_request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter( commentForm = getMultiAdapter(
(self.context, form_request), (self.context, form_request),
name=u'comment-form', name="comment-form",
) )
commentForm.update() commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612 data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0) self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'foo')) self.assertFalse(commentForm.handleComment(commentForm, "foo"))
# Delete the last comment # Delete the last comment
conversation = IConversation(self.context) conversation = IConversation(self.context)
comment = [x for x in conversation.getComments()][-1] comment = [x for x in conversation.getComments()][-1]
deleteView = getMultiAdapter( deleteView = getMultiAdapter(
(comment, self.request), (comment, self.request),
name=u'moderate-delete-comment', name="moderate-delete-comment",
) )
# try to delete last comment without 'Delete comments' permission # 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( self.assertRaises(
Unauthorized, Unauthorized,
comment.restrictedTraverse, comment.restrictedTraverse,
'@@moderate-delete-comment', "@@moderate-delete-comment",
) )
deleteView() deleteView()
self.assertEqual(1, len([x for x in conversation.getComments()])) self.assertEqual(1, len([x for x in conversation.getComments()]))
# try to delete last comment with 'Delete comments' permission # try to delete last comment with 'Delete comments' permission
setRoles(self.portal, TEST_USER_ID, ['Reviewer']) setRoles(self.portal, TEST_USER_ID, ["Reviewer"])
deleteView() deleteView()
self.assertEqual(0, len([x for x in conversation.getComments()])) 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): def test_delete_own_comment(self):
"""Delete own comment as logged-in user. """Delete own comment as logged-in user."""
"""
# Allow discussion # Allow discussion
self.portal.doc1.allow_discussion = True self.portal.doc1.allow_discussion = True
@ -277,42 +305,42 @@ class TestCommentForm(unittest.TestCase):
adapts=(Interface, IBrowserRequest), adapts=(Interface, IBrowserRequest),
provides=Interface, provides=Interface,
factory=CommentForm, factory=CommentForm,
name=u'comment-form', name="comment-form",
) )
# The form is submitted successfully, if the required text field is # The form is submitted successfully, if the required text field is
# filled out # filled out
form_request = make_request(form={'form.widgets.text': u'bar'}) form_request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter( commentForm = getMultiAdapter(
(self.context, form_request), (self.context, form_request),
name=u'comment-form', name="comment-form",
) )
commentForm.update() commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612 data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0) self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'foo')) self.assertFalse(commentForm.handleComment(commentForm, "foo"))
# Delete the last comment # Delete the last comment
conversation = IConversation(self.context) conversation = IConversation(self.context)
comment = [x for x in conversation.getComments()][-1] comment = [x for x in conversation.getComments()][-1]
deleteView = getMultiAdapter( deleteView = getMultiAdapter(
(comment, self.request), (comment, self.request),
name=u'delete-own-comment', name="delete-own-comment",
) )
# try to delete last comment with johndoe # try to delete last comment with johndoe
setRoles(self.portal, 'johndoe', ['Member']) setRoles(self.portal, "johndoe", ["Member"])
login(self.portal, 'johndoe') login(self.portal, "johndoe")
self.assertRaises( self.assertRaises(
Unauthorized, Unauthorized,
comment.restrictedTraverse, comment.restrictedTraverse,
'@@delete-own-comment', "@@delete-own-comment",
) )
self.assertEqual(1, len([x for x in conversation.getComments()])) self.assertEqual(1, len([x for x in conversation.getComments()]))
# try to delete last comment with the same user that created it # try to delete last comment with the same user that created it
login(self.portal, TEST_USER_NAME) login(self.portal, TEST_USER_NAME)
setRoles(self.portal, TEST_USER_ID, ['Member']) setRoles(self.portal, TEST_USER_ID, ["Member"])
deleteView() deleteView()
self.assertEqual(0, len([x for x in conversation.getComments()])) self.assertEqual(0, len([x for x in conversation.getComments()]))
@ -335,40 +363,43 @@ class TestCommentForm(unittest.TestCase):
alsoProvides(request, IAttributeAnnotatable) alsoProvides(request, IAttributeAnnotatable)
return request return request
provideAdapter(adapts=(Interface, IBrowserRequest), provideAdapter(
adapts=(Interface, IBrowserRequest),
provides=Interface, provides=Interface,
factory=CommentForm, factory=CommentForm,
name=u'comment-form') name="comment-form",
)
# Post an anonymous comment and provide a name # Post an anonymous comment and provide a name
request = make_request(form={ request = make_request(
'form.widgets.name': u'john doe', form={
'form.widgets.text': u'bar', "form.widgets.name": "john doe",
}) "form.widgets.text": "bar",
}
)
commentForm = getMultiAdapter( commentForm = getMultiAdapter(
(self.context, request), (self.context, request),
name=u'comment-form', name="comment-form",
) )
commentForm.update() commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612 data, errors = commentForm.extractData() # pylint: disable-msg=W0612
self.assertEqual(len(errors), 0) self.assertEqual(len(errors), 0)
self.assertFalse(commentForm.handleComment(commentForm, 'action')) self.assertFalse(commentForm.handleComment(commentForm, "action"))
comments = IConversation(commentForm.context).getComments() comments = IConversation(commentForm.context).getComments()
comments = [comment for comment in comments] # consume itertor comments = [comment for comment in comments] # consume itertor
self.assertEqual(len(comments), 1) self.assertEqual(len(comments), 1)
for comment in IConversation(commentForm.context).getComments(): for comment in IConversation(commentForm.context).getComments():
self.assertEqual(comment.text, u'bar') self.assertEqual(comment.text, "bar")
self.assertIsNone(comment.creator) self.assertIsNone(comment.creator)
roles = comment.get_local_roles() roles = comment.get_local_roles()
self.assertEqual(len(roles), 0) self.assertEqual(len(roles), 0)
def test_can_not_add_comments_if_discussion_is_not_allowed(self): 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 # Disable discussion
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
@ -382,16 +413,18 @@ class TestCommentForm(unittest.TestCase):
alsoProvides(request, IAttributeAnnotatable) alsoProvides(request, IAttributeAnnotatable)
return request return request
provideAdapter(adapts=(Interface, IBrowserRequest), provideAdapter(
adapts=(Interface, IBrowserRequest),
provides=Interface, provides=Interface,
factory=CommentForm, factory=CommentForm,
name=u'comment-form') name="comment-form",
)
request = make_request(form={'form.widgets.text': u'bar'}) request = make_request(form={"form.widgets.text": "bar"})
commentForm = getMultiAdapter( commentForm = getMultiAdapter(
(self.context, request), (self.context, request),
name=u'comment-form', name="comment-form",
) )
commentForm.update() commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612 data, errors = commentForm.extractData() # pylint: disable-msg=W0612
@ -400,10 +433,7 @@ class TestCommentForm(unittest.TestCase):
# allowed # allowed
self.assertEqual(len(errors), 0) self.assertEqual(len(errors), 0)
self.assertRaises(Unauthorized, self.assertRaises(Unauthorized, commentForm.handleComment, commentForm, "foo")
commentForm.handleComment,
commentForm,
'foo')
def test_anonymous_can_not_add_comments_if_discussion_is_not_allowed(self): def test_anonymous_can_not_add_comments_if_discussion_is_not_allowed(self):
"""Make sure that anonymous users can't post comments if anonymous """Make sure that anonymous users can't post comments if anonymous
@ -421,15 +451,16 @@ class TestCommentForm(unittest.TestCase):
alsoProvides(request, IAttributeAnnotatable) alsoProvides(request, IAttributeAnnotatable)
return request return request
provideAdapter(adapts=(Interface, IBrowserRequest), provideAdapter(
adapts=(Interface, IBrowserRequest),
provides=Interface, provides=Interface,
factory=CommentForm, factory=CommentForm,
name=u'comment-form') 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), commentForm = getMultiAdapter((self.context, request), name="comment-form")
name=u'comment-form')
commentForm.update() commentForm.update()
data, errors = commentForm.extractData() # pylint: disable-msg=W0612 data, errors = commentForm.extractData() # pylint: disable-msg=W0612
@ -438,7 +469,7 @@ class TestCommentForm(unittest.TestCase):
Unauthorized, Unauthorized,
commentForm.handleComment, commentForm.handleComment,
commentForm, commentForm,
'foo', "foo",
) )
@ -447,22 +478,22 @@ class TestCommentsViewlet(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
self.request = self.layer['request'] self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.portal.invokeFactory('Folder', 'test-folder') self.portal.invokeFactory("Folder", "test-folder")
self.folder = self.portal['test-folder'] self.folder = self.portal["test-folder"]
interface.alsoProvides( interface.alsoProvides(
self.request, self.request,
interfaces.IDiscussionLayer, interfaces.IDiscussionLayer,
) )
self.workflowTool = getToolByName(self.portal, 'portal_workflow') self.workflowTool = getToolByName(self.portal, "portal_workflow")
self.workflowTool.setDefaultChain('comment_one_state_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 self.memberdata = self.portal.portal_memberdata
context = getattr(self.portal, 'doc1') context = getattr(self.portal, "doc1")
self.viewlet = CommentsViewlet(context, self.request, None, None) self.viewlet = CommentsViewlet(context, self.request, None, None)
# Allow discussion # Allow discussion
@ -484,9 +515,8 @@ class TestCommentsViewlet(unittest.TestCase):
# Anonymous has no 'can review' permission # Anonymous has no 'can review' permission
self.assertFalse(self.viewlet.can_review()) self.assertFalse(self.viewlet.can_review())
# The reviewer role has the 'Review comments' permission # The reviewer role has the 'Review comments' permission
self.portal.acl_users._doAddUser( self.portal.acl_users._doAddUser("reviewer", "secret", ["Reviewer"], [])
'reviewer', 'secret', ['Reviewer'], []) login(self.portal, "reviewer")
login(self.portal, 'reviewer')
self.assertTrue(self.viewlet.can_review()) self.assertTrue(self.viewlet.can_review())
def test_can_manage(self): def test_can_manage(self):
@ -500,9 +530,8 @@ class TestCommentsViewlet(unittest.TestCase):
# Anonymous has no 'can review' permission # Anonymous has no 'can review' permission
self.assertFalse(self.viewlet.can_manage()) self.assertFalse(self.viewlet.can_manage())
# The reviewer role has the 'Review comments' permission # The reviewer role has the 'Review comments' permission
self.portal.acl_users._doAddUser( self.portal.acl_users._doAddUser("reviewer", "secret", ["Reviewer"], [])
'reviewer', 'secret', ['Reviewer'], []) login(self.portal, "reviewer")
login(self.portal, 'reviewer')
self.assertTrue(self.viewlet.can_manage()) self.assertTrue(self.viewlet.can_manage())
def test_is_discussion_allowed(self): def test_is_discussion_allowed(self):
@ -519,46 +548,48 @@ class TestCommentsViewlet(unittest.TestCase):
self.assertTrue(self.viewlet.comment_transform_message()) self.assertTrue(self.viewlet.comment_transform_message())
self.assertEqual( self.assertEqual(
self.viewlet.comment_transform_message(), self.viewlet.comment_transform_message(),
'You can add a comment by filling out the form below. Plain ' + "You can add a comment by filling out the form below. Plain "
'text formatting.') + "text formatting.",
)
# Set text transform to intelligent text # Set text transform to intelligent text
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False) 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 # Make sure the comment description changes accordingly
self.assertEqual( self.assertEqual(
self.viewlet.comment_transform_message(), self.viewlet.comment_transform_message(),
'You can add a comment by filling out the form below. ' + "You can add a comment by filling out the form below. "
'Plain text formatting. Web and email addresses are transformed ' + + "Plain text formatting. Web and email addresses are transformed "
'into clickable links.', + "into clickable links.",
) )
# Enable moderation workflow # Enable moderation workflow
self.workflowTool.setChainForPortalTypes( self.workflowTool.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",), ("comment_review_workflow,")
('comment_review_workflow,')) )
# Make sure the comment description shows that comments are moderated # Make sure the comment description shows that comments are moderated
self.assertEqual( self.assertEqual(
self.viewlet.comment_transform_message(), self.viewlet.comment_transform_message(),
'You can add a comment by filling out the form below. ' + "You can add a comment by filling out the form below. "
'Plain text formatting. Web and email addresses are transformed ' + + "Plain text formatting. Web and email addresses are transformed "
'into clickable links. Comments are moderated.') + "into clickable links. Comments are moderated.",
)
def test_has_replies(self): def test_has_replies(self):
self.assertEqual(self.viewlet.has_replies(), False) self.assertEqual(self.viewlet.has_replies(), False)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
conversation.addComment(comment) conversation.addComment(comment)
self.assertEqual(self.viewlet.has_replies(), True) self.assertEqual(self.viewlet.has_replies(), True)
def test_get_replies(self): def test_get_replies(self):
self.assertFalse(self.viewlet.get_replies()) self.assertFalse(self.viewlet.get_replies())
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
conversation.addComment(comment) conversation.addComment(comment)
conversation.addComment(comment) conversation.addComment(comment)
@ -581,8 +612,8 @@ class TestCommentsViewlet(unittest.TestCase):
def test_get_replies_with_workflow_actions(self): def test_get_replies_with_workflow_actions(self):
self.assertFalse(self.viewlet.get_replies(workflow_actions=True)) self.assertFalse(self.viewlet.get_replies(workflow_actions=True))
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
c1 = conversation.addComment(comment) c1 = conversation.addComment(comment)
self.assertEqual( self.assertEqual(
@ -591,32 +622,34 @@ class TestCommentsViewlet(unittest.TestCase):
) )
# Enable moderation workflow # Enable moderation workflow
self.workflowTool.setChainForPortalTypes( self.workflowTool.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",),
('comment_review_workflow,'), ("comment_review_workflow,"),
) )
# Check if workflow actions are available # Check if workflow actions are available
reply = next(self.viewlet.get_replies(workflow_actions=True)) reply = next(self.viewlet.get_replies(workflow_actions=True))
self.assertTrue('actions' in reply) self.assertTrue("actions" in reply)
self.assertEqual( self.assertEqual(
reply['actions'][0]['id'], reply["actions"][0]["id"],
'mark_as_spam', "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( self.assertEqual(
reply['actions'][0]['url'], reply["actions"][0]["url"],
expected_url.format(int(c1)), expected_url.format(int(c1)),
) )
def test_get_commenter_home_url(self): def test_get_commenter_home_url(self):
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
IConversation(self.portal.doc1) IConversation(self.portal.doc1)
portal_membership = getToolByName(self.portal, 'portal_membership') portal_membership = getToolByName(self.portal, "portal_membership")
m = portal_membership.getAuthenticatedMember() m = portal_membership.getAuthenticatedMember()
self.assertEqual( self.assertEqual(
self.viewlet.get_commenter_home_url(m.getUserName()), 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): def test_get_commenter_home_url_is_none(self):
@ -625,72 +658,77 @@ class TestCommentsViewlet(unittest.TestCase):
def test_get_commenter_portrait(self): def test_get_commenter_portrait(self):
# Add a user with a member image # Add a user with a member image
self.membershipTool.addMember('jim', 'Jim', ['Member'], []) self.membershipTool.addMember("jim", "Jim", ["Member"], [])
self.memberdata._setPortrait(Image( self.memberdata._setPortrait(
id='jim', Image(
file=dummy.File(), id="jim",
title='', file=DummyFile(),
), 'jim') title="",
self.assertEqual( ),
self.memberdata._getPortrait('jim').getId(), "jim",
'jim',
) )
self.assertEqual( self.assertEqual(
self.memberdata._getPortrait('jim').meta_type, self.memberdata._getPortrait("jim").getId(),
'Image', "jim",
)
self.assertEqual(
self.memberdata._getPortrait("jim").meta_type,
"Image",
) )
# Add a conversation with a comment # Add a conversation with a comment
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment.Creator = 'Jim' comment.Creator = "Jim"
comment.author_username = 'jim' comment.author_username = "jim"
conversation.addComment(comment) conversation.addComment(comment)
# Call get_commenter_portrait method of the viewlet # Call get_commenter_portrait method of the viewlet
self.viewlet.update() 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 # Check if the correct member image URL is returned
self.assertEqual( self.assertEqual(
portrait_url, portrait_url,
'http://nohost/plone/portal_memberdata/portraits/jim', "http://nohost/plone/portal_memberdata/portraits/jim",
) )
def test_get_commenter_portrait_is_none(self): def test_get_commenter_portrait_is_none(self):
self.assertTrue( self.assertTrue(
self.viewlet.get_commenter_portrait() in ( self.viewlet.get_commenter_portrait()
'defaultUser.png', in (
'defaultUser.gif', "defaultUser.png",
"defaultUser.gif",
), ),
) )
def test_get_commenter_portrait_without_userimage(self): def test_get_commenter_portrait_without_userimage(self):
# Create a user without a user image # 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 # Add a conversation with a comment
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment.Creator = 'Jim' comment.Creator = "Jim"
comment.author_username = 'jim' comment.author_username = "jim"
conversation.addComment(comment) conversation.addComment(comment)
# Call get_commenter_portrait method of the viewlet # Call get_commenter_portrait method of the viewlet
self.viewlet.update() 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. # Check if the correct default member image URL is returned.
# Note that Products.PlonePAS 4.0.5 and later have .png and # Note that Products.PlonePAS 4.0.5 and later have .png and
# earlier versions have .gif. # earlier versions have .gif.
self.assertTrue( self.assertTrue(
portrait_url in ( portrait_url
'http://nohost/plone/defaultUser.png', in (
'http://nohost/plone/defaultUser.gif', "http://nohost/plone/defaultUser.png",
"http://nohost/plone/defaultUser.gif",
), ),
) )
@ -700,8 +738,8 @@ class TestCommentsViewlet(unittest.TestCase):
# Allow anonymous discussion # Allow anonymous discussion
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
registry[ registry[
'plone.app.discussion.interfaces.IDiscussionSettings.' + "plone.app.discussion.interfaces.IDiscussionSettings."
'anonymous_comments' + "anonymous_comments"
] = True ] = True
# Test if anonymous discussion is allowed for the viewlet # Test if anonymous discussion is allowed for the viewlet
self.assertTrue(self.viewlet.anonymous_discussion_allowed()) self.assertTrue(self.viewlet.anonymous_discussion_allowed())
@ -710,8 +748,8 @@ class TestCommentsViewlet(unittest.TestCase):
self.assertTrue(self.viewlet.show_commenter_image()) self.assertTrue(self.viewlet.show_commenter_image())
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
registry[ registry[
'plone.app.discussion.interfaces.IDiscussionSettings.' + "plone.app.discussion.interfaces.IDiscussionSettings."
'show_commenter_image' + "show_commenter_image"
] = False ] = False
self.assertFalse(self.viewlet.show_commenter_image()) self.assertFalse(self.viewlet.show_commenter_image())
@ -724,7 +762,7 @@ class TestCommentsViewlet(unittest.TestCase):
self.viewlet.update() self.viewlet.update()
self.assertEqual( self.assertEqual(
self.viewlet.login_action(), 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): 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 # 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 # time of the local time given above. That way, the time for the
# example below is correct within each time zone, independent of DST # example below is correct within each time zone, independent of DST
python_time = datetime( python_time = datetime(*time.gmtime(time.mktime(python_time.timetuple()))[:7])
*time.gmtime(time.mktime(python_time.timetuple()))[:7])
localized_time = self.viewlet.format_time(python_time) localized_time = self.viewlet.format_time(python_time)
self.assertTrue( 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 ICommentAddedEvent
from plone.app.discussion.interfaces import ICommentRemovedEvent from plone.app.discussion.interfaces import ICommentRemovedEvent
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IReplies from plone.app.discussion.interfaces import IReplies
from plone.app.discussion.interfaces import IReplyAddedEvent from plone.app.discussion.interfaces import IReplyAddedEvent
from plone.app.discussion.interfaces import IReplyRemovedEvent 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 setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from plone.contentrules.rule.interfaces import IRuleEventType from plone.contentrules.rule.interfaces import IRuleEventType
@ -17,31 +18,33 @@ import unittest
class CommentContentRulesTest(unittest.TestCase): class CommentContentRulesTest(unittest.TestCase):
""" Test custom comments events """Test custom comments events"""
"""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
# Setup sandbox # Setup sandbox
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
self.request = self.layer['request'] self.request = self.layer["request"]
# Setup current user properties # Setup current user properties
member = self.portal.portal_membership.getMemberById(TEST_USER_ID) member = self.portal.portal_membership.getMemberById(TEST_USER_ID)
member.setMemberProperties({ member.setMemberProperties(
'fullname': 'X Manager', {
'email': 'xmanager@example.com', "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 = createObject("plone.Comment")
comment.text = 'This is a comment' comment.text = "This is a comment"
comment.author_username = 'jim' comment.author_username = "jim"
comment.author_name = 'Jim' comment.author_name = "Jim"
comment.author_email = 'jim@example.com' comment.author_email = "jim@example.com"
conversation = IConversation(self.document) conversation = IConversation(self.document)
conversation.addComment(comment) conversation.addComment(comment)
@ -52,58 +55,61 @@ class CommentContentRulesTest(unittest.TestCase):
self.assertTrue(IRuleEventType.providedBy(IReplyRemovedEvent)) self.assertTrue(IRuleEventType.providedBy(IReplyRemovedEvent))
def testCommentIdStringSubstitution(self): def testCommentIdStringSubstitution(self):
comment_id = getAdapter(self.document, IStringSubstitution, comment_id = getAdapter(self.document, IStringSubstitution, name="comment_id")
name=u'comment_id')
self.assertIsInstance(comment_id(), int) self.assertIsInstance(comment_id(), int)
def testCommentTextStringSubstitution(self): def testCommentTextStringSubstitution(self):
comment_text = getAdapter(self.document, IStringSubstitution, comment_text = getAdapter(
name=u'comment_text') self.document, IStringSubstitution, name="comment_text"
self.assertEqual(comment_text(), u'This is a comment') )
self.assertEqual(comment_text(), "This is a comment")
def testCommentUserIdStringSubstitution(self): def testCommentUserIdStringSubstitution(self):
comment_user_id = getAdapter(self.document, IStringSubstitution, comment_user_id = getAdapter(
name=u'comment_user_id') self.document, IStringSubstitution, name="comment_user_id"
self.assertEqual(comment_user_id(), u'jim') )
self.assertEqual(comment_user_id(), "jim")
def testCommentUserFullNameStringSubstitution(self): def testCommentUserFullNameStringSubstitution(self):
comment_user_fullname = getAdapter(self.document, IStringSubstitution, comment_user_fullname = getAdapter(
name=u'comment_user_fullname') self.document, IStringSubstitution, name="comment_user_fullname"
self.assertEqual(comment_user_fullname(), u'Jim') )
self.assertEqual(comment_user_fullname(), "Jim")
def testCommentUserEmailStringSubstitution(self): def testCommentUserEmailStringSubstitution(self):
comment_user_email = getAdapter(self.document, IStringSubstitution, comment_user_email = getAdapter(
name=u'comment_user_email') self.document, IStringSubstitution, name="comment_user_email"
self.assertEqual(comment_user_email(), u'jim@example.com') )
self.assertEqual(comment_user_email(), "jim@example.com")
class ReplyContentRulesTest(unittest.TestCase): class ReplyContentRulesTest(unittest.TestCase):
""" Test custom comments events """Test custom comments events"""
"""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
# Setup sandbox # Setup sandbox
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
self.request = self.layer['request'] self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.document = self.portal['doc1'] self.document = self.portal["doc1"]
conversation = IConversation(self.document) conversation = IConversation(self.document)
replies = IReplies(conversation) replies = IReplies(conversation)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'This is a comment' comment.text = "This is a comment"
new_id = replies.addComment(comment) new_id = replies.addComment(comment)
comment = self.document.restrictedTraverse( comment = self.document.restrictedTraverse(
'++conversation++default/{0}'.format(new_id), f"++conversation++default/{new_id}",
) )
re_comment = createObject('plone.Comment') re_comment = createObject("plone.Comment")
re_comment.text = 'This is a reply' re_comment.text = "This is a reply"
re_comment.author_username = 'julia' re_comment.author_username = "julia"
re_comment.author_name = 'Juliana' re_comment.author_name = "Juliana"
re_comment.author_email = 'julia@example.com' re_comment.author_email = "julia@example.com"
replies = IReplies(comment) replies = IReplies(comment)
replies.addComment(re_comment) replies.addComment(re_comment)
@ -112,7 +118,7 @@ class ReplyContentRulesTest(unittest.TestCase):
reply_id = getAdapter( reply_id = getAdapter(
self.document, self.document,
IStringSubstitution, IStringSubstitution,
name=u'comment_id', name="comment_id",
) )
self.assertIsInstance(reply_id(), int) self.assertIsInstance(reply_id(), int)
@ -120,30 +126,30 @@ class ReplyContentRulesTest(unittest.TestCase):
reply_text = getAdapter( reply_text = getAdapter(
self.document, self.document,
IStringSubstitution, 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): def testReplyUserIdStringSubstitution(self):
reply_user_id = getAdapter( reply_user_id = getAdapter(
self.document, self.document,
IStringSubstitution, 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): def testReplyUserFullNameStringSubstitution(self):
reply_user_fullname = getAdapter( reply_user_fullname = getAdapter(
self.document, self.document,
IStringSubstitution, 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): def testReplyUserEmailStringSubstitution(self):
reply_user_email = getAdapter( reply_user_email = getAdapter(
self.document, self.document,
IStringSubstitution, 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.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 setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from plone.registry import Registry from plone.registry import Registry
@ -17,8 +18,8 @@ class RegistryTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.registry = Registry() self.registry = Registry()
self.registry.registerInterface(IDiscussionSettings) self.registry.registerInterface(IDiscussionSettings)
@ -29,99 +30,100 @@ class RegistryTest(unittest.TestCase):
def test_discussion_controlpanel_view(self): def test_discussion_controlpanel_view(self):
view = getMultiAdapter( view = getMultiAdapter(
(self.portal, self.portal.REQUEST), (self.portal, self.portal.REQUEST),
name='discussion-controlpanel', name="discussion-controlpanel",
) )
self.assertTrue(view()) self.assertTrue(view())
def test_discussion_in_controlpanel(self): def test_discussion_in_controlpanel(self):
# Check if discussion is in the control panel # Check if discussion is in the control panel
self.controlpanel = getToolByName(self.portal, 'portal_controlpanel') self.controlpanel = getToolByName(self.portal, "portal_controlpanel")
self.assertTrue( self.assertTrue(
'discussion' in [ "discussion"
a.getAction(self)['id'] in [a.getAction(self)["id"] for a in self.controlpanel.listActions()],
for a in self.controlpanel.listActions()
],
) )
def test_globally_enabled(self): def test_globally_enabled(self):
# Check globally_enabled record # Check globally_enabled record
self.assertTrue('globally_enabled' in IDiscussionSettings) self.assertTrue("globally_enabled" in IDiscussionSettings)
self.assertEqual( self.assertEqual(
self.registry[ self.registry[
'plone.app.discussion.interfaces.' + "plone.app.discussion.interfaces."
'IDiscussionSettings.globally_enabled' + "IDiscussionSettings.globally_enabled"
], ],
False, False,
) )
def test_anonymous_comments(self): def test_anonymous_comments(self):
# Check anonymous_comments record # Check anonymous_comments record
self.assertTrue('anonymous_comments' in IDiscussionSettings) self.assertTrue("anonymous_comments" in IDiscussionSettings)
self.assertEqual( self.assertEqual(
self.registry[ self.registry[
'plone.app.discussion.interfaces.' + "plone.app.discussion.interfaces."
'IDiscussionSettings.anonymous_comments' + "IDiscussionSettings.anonymous_comments"
], ],
False, False,
) )
def test_moderation_enabled(self): def test_moderation_enabled(self):
# Check globally_enabled record # Check globally_enabled record
self.assertTrue('moderation_enabled' in IDiscussionSettings) self.assertTrue("moderation_enabled" in IDiscussionSettings)
self.assertEqual( self.assertEqual(
self.registry[ self.registry[
'plone.app.discussion.interfaces.' + "plone.app.discussion.interfaces."
'IDiscussionSettings.moderation_enabled' + "IDiscussionSettings.moderation_enabled"
], ],
False, False,
) )
def test_edit_comment_enabled(self): def test_edit_comment_enabled(self):
# Check edit_comment_enabled record # Check edit_comment_enabled record
self.assertTrue('edit_comment_enabled' in IDiscussionSettings) self.assertTrue("edit_comment_enabled" in IDiscussionSettings)
self.assertEqual( self.assertEqual(
self.registry['plone.app.discussion.interfaces.' + self.registry[
'IDiscussionSettings.edit_comment_enabled'], "plone.app.discussion.interfaces."
+ "IDiscussionSettings.edit_comment_enabled"
],
False, False,
) )
def test_delete_own_comment_enabled(self): def test_delete_own_comment_enabled(self):
# Check delete_own_comment_enabled record # 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.assertEqual(
self.registry['plone.app.discussion.interfaces.' + self.registry[
'IDiscussionSettings.delete_own_comment_enabled'], "plone.app.discussion.interfaces."
+ "IDiscussionSettings.delete_own_comment_enabled"
],
False, False,
) )
def test_text_transform(self): def test_text_transform(self):
self.assertTrue('text_transform' in IDiscussionSettings) self.assertTrue("text_transform" in IDiscussionSettings)
self.assertEqual( self.assertEqual(
self.registry[ self.registry[
'plone.app.discussion.interfaces.' + "plone.app.discussion.interfaces."
'IDiscussionSettings.text_transform' + "IDiscussionSettings.text_transform"
], ],
'text/plain', "text/plain",
) )
def test_captcha(self): def test_captcha(self):
# Check globally_enabled record # Check globally_enabled record
self.assertTrue('captcha' in IDiscussionSettings) self.assertTrue("captcha" in IDiscussionSettings)
self.assertEqual( self.assertEqual(
self.registry[ self.registry[
'plone.app.discussion.interfaces.' + "plone.app.discussion.interfaces." + "IDiscussionSettings.captcha"
'IDiscussionSettings.captcha'
], ],
'disabled', "disabled",
) )
def test_show_commenter_image(self): def test_show_commenter_image(self):
# Check show_commenter_image record # Check show_commenter_image record
self.assertTrue('show_commenter_image' in IDiscussionSettings) self.assertTrue("show_commenter_image" in IDiscussionSettings)
self.assertEqual( self.assertEqual(
self.registry[ self.registry[
'plone.app.discussion.interfaces.' + "plone.app.discussion.interfaces."
'IDiscussionSettings.show_commenter_image' + "IDiscussionSettings.show_commenter_image"
], ],
True, True,
) )
@ -129,12 +131,12 @@ class RegistryTest(unittest.TestCase):
def test_moderator_notification_enabled(self): def test_moderator_notification_enabled(self):
# Check show_commenter_image record # Check show_commenter_image record
self.assertTrue( self.assertTrue(
'moderator_notification_enabled' in IDiscussionSettings, "moderator_notification_enabled" in IDiscussionSettings,
) )
self.assertEqual( self.assertEqual(
self.registry[ self.registry[
'plone.app.discussion.interfaces.' + "plone.app.discussion.interfaces."
'IDiscussionSettings.moderator_notification_enabled' + "IDiscussionSettings.moderator_notification_enabled"
], ],
False, False,
) )
@ -154,8 +156,8 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
self.settings = registry.forInterface(IDiscussionSettings, check=False) self.settings = registry.forInterface(IDiscussionSettings, check=False)
@ -167,9 +169,9 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
# By default the comment_one_state_workflow without moderation is # By default the comment_one_state_workflow without moderation is
# enabled # enabled
self.assertEqual( self.assertEqual(
('comment_one_state_workflow',), ("comment_one_state_workflow",),
self.portal.portal_workflow.getChainForPortalType( self.portal.portal_workflow.getChainForPortalType(
'Discussion Item', "Discussion Item",
), ),
) )
@ -179,17 +181,17 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
# Make sure the comment_review_workflow with moderation enabled is # Make sure the comment_review_workflow with moderation enabled is
# enabled # enabled
self.assertEqual( self.assertEqual(
('comment_review_workflow',), ("comment_review_workflow",),
self.portal.portal_workflow.getChainForPortalType( self.portal.portal_workflow.getChainForPortalType(
'Discussion Item', "Discussion Item",
), ),
) )
# And back # And back
self.settings.moderation_enabled = False self.settings.moderation_enabled = False
self.assertEqual( self.assertEqual(
('comment_one_state_workflow',), ("comment_one_state_workflow",),
self.portal.portal_workflow.getChainForPortalType( self.portal.portal_workflow.getChainForPortalType(
'Discussion Item', "Discussion Item",
), ),
) )
@ -203,8 +205,8 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
# Enable the 'comment_review_workflow' with moderation enabled # Enable the 'comment_review_workflow' with moderation enabled
self.portal.portal_workflow.setChainForPortalTypes( self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",),
('comment_review_workflow',), ("comment_review_workflow",),
) )
# Make sure the moderation_enabled settings has changed # Make sure the moderation_enabled settings has changed
@ -212,15 +214,15 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
# Enable the 'comment_review_workflow' with moderation enabled # Enable the 'comment_review_workflow' with moderation enabled
self.portal.portal_workflow.setChainForPortalTypes( self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",),
('comment_one_state_workflow',), ("comment_one_state_workflow",),
) )
self.settings.moderation_enabled = True self.settings.moderation_enabled = True
# Enable a 'custom' discussion workflow # Enable a 'custom' discussion workflow
self.portal.portal_workflow.setChainForPortalTypes( self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",),
('intranet_workflow',), ("intranet_workflow",),
) )
# Setting has not changed. A Custom workflow disables the # 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_base
from Acquisition import aq_parent from Acquisition import aq_parent
from datetime import datetime from datetime import datetime
from datetime import timedelta 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 setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from plone.app.vocabularies.types import BAD_TYPES from plone.app.vocabularies.types import BAD_TYPES
from plone.dexterity.interfaces import IDexterityContent
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from zope import interface from zope import interface
@ -19,31 +19,22 @@ from zope.annotation.interfaces import IAnnotations
from zope.component import createObject from zope.component import createObject
from zope.component import queryUtility from zope.component import queryUtility
import six
import unittest import unittest
try:
from plone.dexterity.interfaces import IDexterityContent
DEXTERITY = True
except ImportError:
DEXTERITY = False
class ConversationTest(unittest.TestCase): class ConversationTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
interface.alsoProvides( interface.alsoProvides(self.portal.REQUEST, IDiscussionLayer)
self.portal.REQUEST, interfaces.IDiscussionLayer)
self.typetool = self.portal.portal_types self.typetool = self.portal.portal_types
self.portal_discussion = getToolByName( self.portal_discussion = getToolByName(
self.portal, self.portal,
'portal_discussion', "portal_discussion",
None, None,
) )
# Allow discussion # Allow discussion
@ -52,7 +43,7 @@ class ConversationTest(unittest.TestCase):
settings.globally_enabled = True settings.globally_enabled = True
workflow = self.portal.portal_workflow workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish') workflow.doActionFor(self.portal.doc1, "publish")
def test_add_comment(self): def test_add_comment(self):
# Create a conversation. In this case we doesn't assign it to an # 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 # Add a comment. Note: in real life, we always create comments via the
# factory to allow different factories to be swapped in # factory to allow different factories to be swapped in
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
new_id = conversation.addComment(comment) new_id = conversation.addComment(comment)
@ -79,20 +70,19 @@ class ConversationTest(unittest.TestCase):
self.assertEqual(len(tuple(conversation.getThreads())), 1) self.assertEqual(len(tuple(conversation.getThreads())), 1)
self.assertEqual(conversation.total_comments(), 1) self.assertEqual(conversation.total_comments(), 1)
self.assertTrue( self.assertTrue(
conversation.last_comment_date - datetime.utcnow() < conversation.last_comment_date - datetime.utcnow() < timedelta(seconds=1),
timedelta(seconds=1),
) )
def test_private_comment(self): def test_private_comment(self):
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.author_username = 'nobody' comment.author_username = "nobody"
conversation.addComment(comment) conversation.addComment(comment)
comment.manage_permission('View', roles=tuple()) comment.manage_permission("View", roles=tuple())
self.assertEqual(0, conversation.total_comments()) self.assertEqual(0, conversation.total_comments())
self.assertEqual(None, conversation.last_comment_date) 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)) self.assertEqual([], list(conversation.public_commentators))
def test_delete_comment(self): 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 # Add a comment. Note: in real life, we always create comments via the
# factory to allow different factories to be swapped in # factory to allow different factories to be swapped in
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
new_id = conversation.addComment(comment) new_id = conversation.addComment(comment)
@ -139,23 +129,23 @@ class ConversationTest(unittest.TestCase):
# +- Comment 2_1 # +- Comment 2_1
# Create all comments # Create all comments
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
comment1_1 = createObject('plone.Comment') comment1_1 = createObject("plone.Comment")
comment1_1.text = 'Comment text' comment1_1.text = "Comment text"
comment1_1_1 = createObject('plone.Comment') comment1_1_1 = createObject("plone.Comment")
comment1_1_1.text = 'Comment text' comment1_1_1.text = "Comment text"
comment1_2 = createObject('plone.Comment') comment1_2 = createObject("plone.Comment")
comment1_2.text = 'Comment text' comment1_2.text = "Comment text"
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2_1 = createObject('plone.Comment') comment2_1 = createObject("plone.Comment")
comment2_1.text = 'Comment text' comment2_1.text = "Comment text"
# Create the nested comment structure # Create the nested comment structure
new_id_1 = conversation.addComment(comment1) new_id_1 = conversation.addComment(comment1)
@ -175,21 +165,24 @@ class ConversationTest(unittest.TestCase):
del conversation[new_id_1] del conversation[new_id_1]
self.assertEqual([ self.assertEqual(
{'comment': comment2, 'depth': 0, 'id': new_id_2}, [
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1}, {"comment": comment2, "depth": 0, "id": new_id_2},
], list(conversation.getThreads())) {"comment": comment2_1, "depth": 1, "id": new_id_2_1},
],
list(conversation.getThreads()),
)
def test_delete_comment_when_content_object_is_deleted(self): def test_delete_comment_when_content_object_is_deleted(self):
# Make sure all comments of a content object are deleted when the # Make sure all comments of a content object are deleted when the
# object itself is deleted. # object itself is deleted.
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
conversation.addComment(comment) conversation.addComment(comment)
# Delete the content object # Delete the content object
self.portal.manage_delObjects(['doc1']) self.portal.manage_delObjects(["doc1"])
# Make sure the comment has been deleted as well # Make sure the comment has been deleted as well
self.assertEqual(len(list(conversation.getComments())), 0) self.assertEqual(len(list(conversation.getComments())), 0)
@ -198,8 +191,8 @@ class ConversationTest(unittest.TestCase):
def test_comments_enabled_on_doc_in_subfolder(self): def test_comments_enabled_on_doc_in_subfolder(self):
typetool = self.portal.portal_types typetool = self.portal.portal_types
typetool.constructContent('Folder', self.portal, 'folder1') typetool.constructContent("Folder", self.portal, "folder1")
typetool.constructContent('Document', self.portal.folder1, 'doc2') typetool.constructContent("Document", self.portal.folder1, "doc2")
folder = self.portal.folder1 folder = self.portal.folder1
@ -209,13 +202,13 @@ class ConversationTest(unittest.TestCase):
self.assertFalse(aq_base(folder).allow_discussion) self.assertFalse(aq_base(folder).allow_discussion)
doc = self.portal.folder1.doc2 doc = self.portal.folder1.doc2
conversation = doc.restrictedTraverse('@@conversation_view') conversation = doc.restrictedTraverse("@@conversation_view")
self.assertEqual(conversation.enabled(), False) self.assertEqual(conversation.enabled(), False)
# We have to allow discussion on Document content type, since # We have to allow discussion on Document content type, since
# otherwise allow_discussion will always return False # otherwise allow_discussion will always return False
portal_types = getToolByName(self.portal, 'portal_types') portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, 'Document') document_fti = getattr(portal_types, "Document")
document_fti.manage_changeProperties(allow_discussion=True) document_fti.manage_changeProperties(allow_discussion=True)
self.assertEqual(conversation.enabled(), True) self.assertEqual(conversation.enabled(), True)
@ -223,13 +216,12 @@ class ConversationTest(unittest.TestCase):
def test_disable_commenting_globally(self): def test_disable_commenting_globally(self):
# Create a conversation. # Create a conversation.
conversation = self.portal.doc1.restrictedTraverse( conversation = self.portal.doc1.restrictedTraverse("@@conversation_view")
'@@conversation_view')
# We have to allow discussion on Document content type, since # We have to allow discussion on Document content type, since
# otherwise allow_discussion will always return False # otherwise allow_discussion will always return False
portal_types = getToolByName(self.portal, 'portal_types') portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, 'Document') document_fti = getattr(portal_types, "Document")
document_fti.manage_changeProperties(allow_discussion=True) document_fti.manage_changeProperties(allow_discussion=True)
# Check if conversation is enabled now # Check if conversation is enabled now
@ -249,14 +241,14 @@ class ConversationTest(unittest.TestCase):
def test_allow_discussion_for_news_items(self): 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 newsitem = self.portal.newsitem
conversation = newsitem.restrictedTraverse('@@conversation_view') conversation = newsitem.restrictedTraverse("@@conversation_view")
# We have to allow discussion on Document content type, since # We have to allow discussion on Document content type, since
# otherwise allow_discussion will always return False # otherwise allow_discussion will always return False
portal_types = getToolByName(self.portal, 'portal_types') portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, 'News Item') document_fti = getattr(portal_types, "News Item")
document_fti.manage_changeProperties(allow_discussion=True) document_fti.manage_changeProperties(allow_discussion=True)
# Check if conversation is enabled now # Check if conversation is enabled now
@ -278,23 +270,23 @@ class ConversationTest(unittest.TestCase):
# Create a conversation. # Create a conversation.
conversation = self.portal.doc1.restrictedTraverse( conversation = self.portal.doc1.restrictedTraverse(
'@@conversation_view', "@@conversation_view",
) )
# The Document content type is disabled by default # The Document content type is disabled by default
self.assertEqual(conversation.enabled(), False) self.assertEqual(conversation.enabled(), False)
# Allow discussion on Document content type # Allow discussion on Document content type
portal_types = getToolByName(self.portal, 'portal_types') portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, 'Document') document_fti = getattr(portal_types, "Document")
document_fti.manage_changeProperties(allow_discussion=True) document_fti.manage_changeProperties(allow_discussion=True)
# Check if conversation is enabled now # Check if conversation is enabled now
self.assertEqual(conversation.enabled(), True) self.assertEqual(conversation.enabled(), True)
# Disallow discussion on Document content type # Disallow discussion on Document content type
portal_types = getToolByName(self.portal, 'portal_types') portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, 'Document') document_fti = getattr(portal_types, "Document")
document_fti.manage_changeProperties(allow_discussion=False) document_fti.manage_changeProperties(allow_discussion=False)
# Check if conversation is enabled now # Check if conversation is enabled now
@ -306,17 +298,17 @@ class ConversationTest(unittest.TestCase):
# plone.app.contenttypes does not have this restriction any longer. # plone.app.contenttypes does not have this restriction any longer.
# Create a folder # 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 # 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 # Allow discussion for the folder
self.portal.f1.allow_discussion = True self.portal.f1.allow_discussion = True
# Allow discussion on Folder content type # Allow discussion on Folder content type
portal_types = getToolByName(self.portal, 'portal_types') portal_types = getToolByName(self.portal, "portal_types")
document_fti = getattr(portal_types, 'Folder') document_fti = getattr(portal_types, "Folder")
document_fti.manage_changeProperties(allow_discussion=True) document_fti.manage_changeProperties(allow_discussion=True)
self.assertTrue(conversation.enabled()) self.assertTrue(conversation.enabled())
@ -326,7 +318,7 @@ class ConversationTest(unittest.TestCase):
# Create a conversation. # Create a conversation.
conversation = self.portal.doc1.restrictedTraverse( conversation = self.portal.doc1.restrictedTraverse(
'@@conversation_view', "@@conversation_view",
) )
# Discussion is disallowed by default # 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 # Add a comment. Note: in real life, we always create comments via the
# factory to allow different factories to be swapped in # factory to allow different factories to be swapped in
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
new_id1 = conversation.addComment(comment1) new_id1 = conversation.addComment(comment1)
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.text = 'Comment text' comment2.text = "Comment text"
new_id2 = conversation.addComment(comment2) new_id2 = conversation.addComment(comment2)
@ -384,17 +376,17 @@ class ConversationTest(unittest.TestCase):
self.assertTrue(comment2 in conversation.values()) self.assertTrue(comment2 in conversation.values())
# check if comment ids are in iterkeys # check if comment ids are in iterkeys
self.assertTrue(new_id1 in six.iterkeys(conversation)) self.assertTrue(new_id1 in conversation.keys())
self.assertTrue(new_id2 in six.iterkeys(conversation)) self.assertTrue(new_id2 in conversation.keys())
self.assertFalse(123 in six.iterkeys(conversation)) self.assertFalse(123 in conversation.keys())
# check if comment objects are in itervalues # check if comment objects are in itervalues
self.assertTrue(comment1 in six.itervalues(conversation)) self.assertTrue(comment1 in conversation.values())
self.assertTrue(comment2 in six.itervalues(conversation)) self.assertTrue(comment2 in conversation.values())
# check if iteritems returns (key, comment object) pairs # check if iteritems returns (key, comment object) pairs
self.assertTrue((new_id1, comment1) in six.iteritems(conversation)) self.assertTrue((new_id1, comment1) in conversation.items())
self.assertTrue((new_id2, comment2) in six.iteritems(conversation)) self.assertTrue((new_id2, comment2) in conversation.items())
# TODO test acquisition wrapping # noqa T000 # TODO test acquisition wrapping # noqa T000
# self.assertTrue(aq_base(aq_parent(comment1)) is conversation) # 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 # comments via the factory to allow different factories to be
# swapped in # swapped in
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.text = 'Comment text' comment2.text = "Comment text"
comment3 = createObject('plone.Comment') comment3 = createObject("plone.Comment")
comment3.text = 'Comment text' comment3.text = "Comment text"
conversation.addComment(comment1) conversation.addComment(comment1)
conversation.addComment(comment2) conversation.addComment(comment2)
@ -437,49 +429,49 @@ class ConversationTest(unittest.TestCase):
# Note: in real life, we always create # Note: in real life, we always create
# comments via the factory to allow different factories to be # comments via the factory to allow different factories to be
# swapped in # swapped in
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
comment1.author_username = 'Jim' comment1.author_username = "Jim"
conversation.addComment(comment1) conversation.addComment(comment1)
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2.author_username = 'Joe' comment2.author_username = "Joe"
conversation.addComment(comment2) conversation.addComment(comment2)
comment3 = createObject('plone.Comment') comment3 = createObject("plone.Comment")
comment3.text = 'Comment text' comment3.text = "Comment text"
comment3.author_username = 'Jack' comment3.author_username = "Jack"
new_comment3_id = conversation.addComment(comment3) new_comment3_id = conversation.addComment(comment3)
comment4 = createObject('plone.Comment') comment4 = createObject("plone.Comment")
comment4.text = 'Comment text' comment4.text = "Comment text"
comment4.author_username = 'Jack' comment4.author_username = "Jack"
new_comment4_id = conversation.addComment(comment4) new_comment4_id = conversation.addComment(comment4)
# check if all commentators are in the commentators list # check if all commentators are in the commentators list
self.assertEqual(conversation.total_comments(), 4) self.assertEqual(conversation.total_comments(), 4)
self.assertTrue('Jim' in conversation.commentators) self.assertTrue("Jim" in conversation.commentators)
self.assertTrue('Joe' in conversation.commentators) self.assertTrue("Joe" in conversation.commentators)
self.assertTrue('Jack' in conversation.commentators) self.assertTrue("Jack" in conversation.commentators)
# remove the comment from Jack # remove the comment from Jack
del conversation[new_comment3_id] del conversation[new_comment3_id]
# check if Jack is still in the commentators list (since # check if Jack is still in the commentators list (since
# he had added two comments) # he had added two comments)
self.assertTrue('Jim' in conversation.commentators) self.assertTrue("Jim" in conversation.commentators)
self.assertTrue('Joe' in conversation.commentators) self.assertTrue("Joe" in conversation.commentators)
self.assertTrue('Jack' in conversation.commentators) self.assertTrue("Jack" in conversation.commentators)
self.assertEqual(conversation.total_comments(), 3) self.assertEqual(conversation.total_comments(), 3)
# remove the second comment from Jack # remove the second comment from Jack
del conversation[new_comment4_id] del conversation[new_comment4_id]
# check if Jack has been removed from the commentators list # check if Jack has been removed from the commentators list
self.assertTrue('Jim' in conversation.commentators) self.assertTrue("Jim" in conversation.commentators)
self.assertTrue('Joe' in conversation.commentators) self.assertTrue("Joe" in conversation.commentators)
self.assertFalse('Jack' in conversation.commentators) self.assertFalse("Jack" in conversation.commentators)
self.assertEqual(conversation.total_comments(), 2) self.assertEqual(conversation.total_comments(), 2)
def test_last_comment_date(self): def test_last_comment_date(self):
@ -494,29 +486,29 @@ class ConversationTest(unittest.TestCase):
# Note: in real life, we always create # Note: in real life, we always create
# comments via the factory to allow different factories to be # comments via the factory to allow different factories to be
# swapped in # swapped in
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
comment1.creation_date = datetime.utcnow() - timedelta(4) comment1.creation_date = datetime.utcnow() - timedelta(4)
conversation.addComment(comment1) conversation.addComment(comment1)
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2.creation_date = datetime.utcnow() - timedelta(2) comment2.creation_date = datetime.utcnow() - timedelta(2)
new_comment2_id = conversation.addComment(comment2) new_comment2_id = conversation.addComment(comment2)
comment3 = createObject('plone.Comment') comment3 = createObject("plone.Comment")
comment3.text = 'Comment text' comment3.text = "Comment text"
comment3.creation_date = datetime.utcnow() - timedelta(1) comment3.creation_date = datetime.utcnow() - timedelta(1)
new_comment3_id = conversation.addComment(comment3) new_comment3_id = conversation.addComment(comment3)
# check if the latest comment is exactly one day old # check if the latest comment is exactly one day old
self.assertTrue( self.assertTrue(
conversation.last_comment_date < datetime.utcnow() - conversation.last_comment_date
timedelta(hours=23, minutes=59, seconds=59), < datetime.utcnow() - timedelta(hours=23, minutes=59, seconds=59),
) )
self.assertTrue( self.assertTrue(
conversation.last_comment_date > conversation.last_comment_date
datetime.utcnow() - timedelta(days=1, seconds=1), > datetime.utcnow() - timedelta(days=1, seconds=1),
) )
# remove the latest comment # remove the latest comment
@ -525,12 +517,12 @@ class ConversationTest(unittest.TestCase):
# check if the latest comment has been updated # check if the latest comment has been updated
# the latest comment should be exactly two days old # the latest comment should be exactly two days old
self.assertTrue( self.assertTrue(
conversation.last_comment_date < datetime.utcnow() - conversation.last_comment_date
timedelta(days=1, hours=23, minutes=59, seconds=59), < datetime.utcnow() - timedelta(days=1, hours=23, minutes=59, seconds=59),
) )
self.assertTrue( self.assertTrue(
conversation.last_comment_date > datetime.utcnow() - conversation.last_comment_date
timedelta(days=2, seconds=1), > datetime.utcnow() - timedelta(days=2, seconds=1),
) )
# remove the latest comment again # remove the latest comment again
@ -539,12 +531,12 @@ class ConversationTest(unittest.TestCase):
# check if the latest comment has been updated # check if the latest comment has been updated
# the latest comment should be exactly four days old # the latest comment should be exactly four days old
self.assertTrue( self.assertTrue(
conversation.last_comment_date < datetime.utcnow() - conversation.last_comment_date
timedelta(days=3, hours=23, minutes=59, seconds=59), < datetime.utcnow() - timedelta(days=3, hours=23, minutes=59, seconds=59),
) )
self.assertTrue( self.assertTrue(
conversation.last_comment_date > datetime.utcnow() - conversation.last_comment_date
timedelta(days=4, seconds=2), > datetime.utcnow() - timedelta(days=4, seconds=2),
) )
def test_get_comments_full(self): def test_get_comments_full(self):
@ -572,23 +564,23 @@ class ConversationTest(unittest.TestCase):
# +- Comment 2_1 # +- Comment 2_1
# Create all comments # Create all comments
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
comment1_1 = createObject('plone.Comment') comment1_1 = createObject("plone.Comment")
comment1_1.text = 'Comment text' comment1_1.text = "Comment text"
comment1_1_1 = createObject('plone.Comment') comment1_1_1 = createObject("plone.Comment")
comment1_1_1.text = 'Comment text' comment1_1_1.text = "Comment text"
comment1_2 = createObject('plone.Comment') comment1_2 = createObject("plone.Comment")
comment1_2.text = 'Comment text' comment1_2.text = "Comment text"
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2_1 = createObject('plone.Comment') comment2_1 = createObject("plone.Comment")
comment2_1.text = 'Comment text' comment2_1.text = "Comment text"
# Create the nested comment structure # Create the nested comment structure
new_id_1 = conversation.addComment(comment1) new_id_1 = conversation.addComment(comment1)
@ -608,14 +600,17 @@ class ConversationTest(unittest.TestCase):
# Get threads # Get threads
self.assertEqual([ self.assertEqual(
{'comment': comment1, 'depth': 0, 'id': new_id_1}, [
{'comment': comment1_1, 'depth': 1, 'id': new_id_1_1}, {"comment": comment1, "depth": 0, "id": new_id_1},
{'comment': comment1_1_1, 'depth': 2, 'id': new_id_1_1_1}, {"comment": comment1_1, "depth": 1, "id": new_id_1_1},
{'comment': comment1_2, 'depth': 1, 'id': new_id_1_2}, {"comment": comment1_1_1, "depth": 2, "id": new_id_1_1_1},
{'comment': comment2, 'depth': 0, 'id': new_id_2}, {"comment": comment1_2, "depth": 1, "id": new_id_1_2},
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1}, {"comment": comment2, "depth": 0, "id": new_id_2},
], list(conversation.getThreads())) {"comment": comment2_1, "depth": 1, "id": new_id_2_1},
],
list(conversation.getThreads()),
)
def test_get_threads_batched(self): def test_get_threads_batched(self):
# TODO: test start, size, root and depth arguments to getThreads() # noqa T000 # 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 # make sure we can traverse to conversations and get a URL and path
conversation = self.portal.doc1.restrictedTraverse( conversation = self.portal.doc1.restrictedTraverse(
'++conversation++default', "++conversation++default",
) )
self.assertTrue(IConversation.providedBy(conversation)) self.assertTrue(IConversation.providedBy(conversation))
self.assertEqual( self.assertEqual(
('', 'plone', 'doc1', '++conversation++default'), ("", "plone", "doc1", "++conversation++default"),
conversation.getPhysicalPath(), conversation.getPhysicalPath(),
) )
self.assertEqual( self.assertEqual(
'http://nohost/plone/doc1/++conversation++default', "http://nohost/plone/doc1/++conversation++default",
conversation.absolute_url(), conversation.absolute_url(),
) )
@ -644,7 +639,7 @@ class ConversationTest(unittest.TestCase):
# can't be converted to int # can't be converted to int
conversation = self.portal.doc1.restrictedTraverse( conversation = self.portal.doc1.restrictedTraverse(
'++conversation++default/ThisCantBeRight', "++conversation++default/ThisCantBeRight",
) )
self.assertEqual(conversation, None) self.assertEqual(conversation, None)
@ -658,17 +653,16 @@ class ConversationTest(unittest.TestCase):
self.assertTrue(conversation.__parent__) self.assertTrue(conversation.__parent__)
self.assertTrue(aq_parent(conversation)) 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): 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): def test_no_comment(self):
IConversation(self.portal.doc1) IConversation(self.portal.doc1)
# Make sure no conversation has been created # Make sure no conversation has been created
self.assertTrue( self.assertTrue(
'plone.app.discussion:conversation' not in "plone.app.discussion:conversation" not in IAnnotations(self.portal.doc1),
IAnnotations(self.portal.doc1),
) )
@ -677,21 +671,20 @@ class ConversationEnabledForDexterityTypesTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
interface.alsoProvides( interface.alsoProvides(
self.portal.REQUEST, self.portal.REQUEST,
interfaces.IDiscussionLayer, IDiscussionLayer,
) )
if DEXTERITY:
interface.alsoProvides( interface.alsoProvides(
self.portal.doc1, self.portal.doc1,
IDexterityContent, IDexterityContent,
) )
def _makeOne(self, *args, **kw): 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): def _globally_enable_discussion(self, value):
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
@ -699,40 +692,35 @@ class ConversationEnabledForDexterityTypesTest(unittest.TestCase):
settings.globally_enabled = value settings.globally_enabled = value
def _enable_discussion_on_portal_type(self, portal_type, allow_discussion): 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 = getattr(portal_types, portal_type)
document_fti.manage_changeProperties(allow_discussion=allow_discussion) document_fti.manage_changeProperties(allow_discussion=allow_discussion)
def test_conversation_is_not_enabled_by_default(self): def test_conversation_is_not_enabled_by_default(self):
if DEXTERITY:
conversation = self._makeOne(self.portal.doc1) conversation = self._makeOne(self.portal.doc1)
self.assertFalse(conversation.enabled()) self.assertFalse(conversation.enabled())
def test_conversation_is_not_enabled_by_default_on_portal_type(self): def test_conversation_is_not_enabled_by_default_on_portal_type(self):
if DEXTERITY:
self._globally_enable_discussion(True) self._globally_enable_discussion(True)
conversation = self._makeOne(self.portal.doc1) conversation = self._makeOne(self.portal.doc1)
self.assertFalse(conversation.enabled()) self.assertFalse(conversation.enabled())
def test_conversation_needs_to_be_enabled_globally_and_for_type(self): def test_conversation_needs_to_be_enabled_globally_and_for_type(self):
if DEXTERITY:
self._globally_enable_discussion(True) self._globally_enable_discussion(True)
self._enable_discussion_on_portal_type('Document', True) self._enable_discussion_on_portal_type("Document", True)
conversation = self._makeOne(self.portal.doc1) conversation = self._makeOne(self.portal.doc1)
self.assertTrue(conversation.enabled()) self.assertTrue(conversation.enabled())
def test_disable_discussion(self): def test_disable_discussion(self):
if DEXTERITY:
self._globally_enable_discussion(True) self._globally_enable_discussion(True)
self._enable_discussion_on_portal_type('Document', True) self._enable_discussion_on_portal_type("Document", True)
self.portal.doc1.allow_discussion = False self.portal.doc1.allow_discussion = False
conversation = self._makeOne(self.portal.doc1) conversation = self._makeOne(self.portal.doc1)
self.assertFalse(conversation.enabled()) self.assertFalse(conversation.enabled())
def test_enable_discussion(self): def test_enable_discussion(self):
if DEXTERITY:
self._globally_enable_discussion(True) self._globally_enable_discussion(True)
self._enable_discussion_on_portal_type('Document', True) self._enable_discussion_on_portal_type("Document", True)
self.portal.doc1.allow_discussion = True self.portal.doc1.allow_discussion = True
conversation = self._makeOne(self.portal.doc1) conversation = self._makeOne(self.portal.doc1)
self.assertTrue(conversation.enabled()) self.assertTrue(conversation.enabled())
@ -745,11 +733,11 @@ class RepliesTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
workflow = self.portal.portal_workflow workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish') workflow.doActionFor(self.portal.doc1, "publish")
def test_add_comment(self): def test_add_comment(self):
# Add comments to a ConversationReplies adapter # Add comments to a ConversationReplies adapter
@ -760,8 +748,8 @@ class RepliesTest(unittest.TestCase):
replies = IReplies(conversation) replies = IReplies(conversation)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
new_id = replies.addComment(comment) new_id = replies.addComment(comment)
@ -787,8 +775,8 @@ class RepliesTest(unittest.TestCase):
replies = IReplies(conversation) replies = IReplies(conversation)
# Add a comment. # Add a comment.
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
new_id = replies.addComment(comment) new_id = replies.addComment(comment)
@ -826,39 +814,39 @@ class RepliesTest(unittest.TestCase):
# +- Comment 2_1 # +- Comment 2_1
# Create all comments # Create all comments
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment text' comment1.text = "Comment text"
comment1_1 = createObject('plone.Comment') comment1_1 = createObject("plone.Comment")
comment1_1.text = 'Comment text' comment1_1.text = "Comment text"
comment1_1_1 = createObject('plone.Comment') comment1_1_1 = createObject("plone.Comment")
comment1_1_1.text = 'Comment text' comment1_1_1.text = "Comment text"
comment1_2 = createObject('plone.Comment') comment1_2 = createObject("plone.Comment")
comment1_2.text = 'Comment text' comment1_2.text = "Comment text"
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2_1 = createObject('plone.Comment') comment2_1 = createObject("plone.Comment")
comment2_1.text = 'Comment text' comment2_1.text = "Comment text"
# Create the nested comment structure # Create the nested comment structure
new_id_1 = replies.addComment(comment1) new_id_1 = replies.addComment(comment1)
comment1 = self.portal.doc1.restrictedTraverse( comment1 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_1), f"++conversation++default/{new_id_1}",
) )
replies_to_comment1 = IReplies(comment1) replies_to_comment1 = IReplies(comment1)
new_id_2 = replies.addComment(comment2) new_id_2 = replies.addComment(comment2)
comment2 = self.portal.doc1.restrictedTraverse( comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_2), f"++conversation++default/{new_id_2}",
) )
replies_to_comment2 = IReplies(comment2) replies_to_comment2 = IReplies(comment2)
new_id_1_1 = replies_to_comment1.addComment(comment1_1) new_id_1_1 = replies_to_comment1.addComment(comment1_1)
comment1_1 = self.portal.doc1.restrictedTraverse( 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 = IReplies(comment1_1)
replies_to_comment1_1.addComment(comment1_1_1) replies_to_comment1_1.addComment(comment1_1_1)

View File

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

View File

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

View File

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*-
"""Test for the plone.app.discussion indexers """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 datetime import datetime
from plone.app.discussion import catalog 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.testing import setRoles from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from plone.indexer.delegate import DelegatingIndexerFactory from plone.indexer.delegate import DelegatingIndexerFactory
@ -26,41 +25,40 @@ sed diam voluptua. At [...]"""
class ConversationIndexersTest(unittest.TestCase): class ConversationIndexersTest(unittest.TestCase):
"""Conversation Indexer Tests """Conversation Indexer Tests"""
"""
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
workflow = self.portal.portal_workflow workflow = self.portal.portal_workflow
workflow.doActionFor(self.portal.doc1, 'publish') workflow.doActionFor(self.portal.doc1, "publish")
# Create a conversation. # Create a conversation.
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.text = 'Comment Text' comment1.text = "Comment Text"
comment1.creator = 'jim' comment1.creator = "jim"
comment1.author_username = 'Jim' comment1.author_username = "Jim"
comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12) comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12)
comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12) comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12)
self.new_id1 = conversation.addComment(comment1) self.new_id1 = conversation.addComment(comment1)
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.text = 'Comment Text' comment2.text = "Comment Text"
comment2.creator = 'emma' comment2.creator = "emma"
comment2.author_username = 'Emma' comment2.author_username = "Emma"
comment2.creation_date = datetime(2007, 12, 13, 4, 18, 12) comment2.creation_date = datetime(2007, 12, 13, 4, 18, 12)
comment2.modification_date = datetime(2007, 12, 13, 4, 18, 12) comment2.modification_date = datetime(2007, 12, 13, 4, 18, 12)
self.new_id2 = conversation.addComment(comment2) self.new_id2 = conversation.addComment(comment2)
comment3 = createObject('plone.Comment') comment3 = createObject("plone.Comment")
comment3.text = 'Comment Text' comment3.text = "Comment Text"
comment3.creator = 'lukas' comment3.creator = "lukas"
comment3.author_username = 'Lukas' comment3.author_username = "Lukas"
comment3.creation_date = datetime(2009, 4, 12, 11, 12, 12) comment3.creation_date = datetime(2009, 4, 12, 11, 12, 12)
comment3.modification_date = datetime(2009, 4, 12, 11, 12, 12) comment3.modification_date = datetime(2009, 4, 12, 11, 12, 12)
self.new_id3 = conversation.addComment(comment3) self.new_id3 = conversation.addComment(comment3)
@ -68,10 +66,12 @@ class ConversationIndexersTest(unittest.TestCase):
self.conversation = conversation self.conversation = conversation
def test_conversation_total_comments(self): def test_conversation_total_comments(self):
self.assertTrue(isinstance( self.assertTrue(
isinstance(
catalog.total_comments, catalog.total_comments,
DelegatingIndexerFactory, DelegatingIndexerFactory,
)) )
)
self.assertEqual(catalog.total_comments(self.portal.doc1)(), 3) self.assertEqual(catalog.total_comments(self.portal.doc1)(), 3)
del self.conversation[self.new_id1] del self.conversation[self.new_id1]
self.assertEqual(catalog.total_comments(self.portal.doc1)(), 2) 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) self.assertEqual(catalog.total_comments(self.portal.doc1)(), 0)
def test_conversation_last_comment_date(self): def test_conversation_last_comment_date(self):
self.assertTrue(isinstance( self.assertTrue(
isinstance(
catalog.last_comment_date, catalog.last_comment_date,
DelegatingIndexerFactory, DelegatingIndexerFactory,
)) )
)
self.assertEqual( self.assertEqual(
catalog.last_comment_date(self.portal.doc1)(), catalog.last_comment_date(self.portal.doc1)(),
datetime(2009, 4, 12, 11, 12, 12), datetime(2009, 4, 12, 11, 12, 12),
@ -110,8 +112,8 @@ class CommentIndexersTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
# Create a conversation. In this case we doesn't assign it to an # Create a conversation. In this case we doesn't assign it to an
# object, as we just want to check the Conversation object API. # 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 # Add a comment. Note: in real life, we always create comments via the
# factory to allow different factories to be swapped in # factory to allow different factories to be swapped in
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Lorem ipsum dolor sit amet.' comment.text = "Lorem ipsum dolor sit amet."
comment.creator = 'jim' comment.creator = "jim"
comment.author_name = 'Jim' comment.author_name = "Jim"
comment.creation_date = datetime(2006, 9, 17, 14, 18, 12) comment.creation_date = datetime(2006, 9, 17, 14, 18, 12)
comment.modification_date = datetime(2008, 3, 12, 7, 32, 52) comment.modification_date = datetime(2008, 3, 12, 7, 32, 52)
@ -132,60 +134,61 @@ class CommentIndexersTest(unittest.TestCase):
self.conversation = conversation self.conversation = conversation
def test_title(self): 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)) self.assertTrue(isinstance(catalog.title, DelegatingIndexerFactory))
def test_description(self): def test_description(self):
self.assertEqual( self.assertEqual(
catalog.description(self.comment)(), catalog.description(self.comment)(),
'Lorem ipsum dolor sit amet.', "Lorem ipsum dolor sit amet.",
) )
self.assertTrue( self.assertTrue(isinstance(catalog.description, DelegatingIndexerFactory))
isinstance(catalog.description, DelegatingIndexerFactory))
def test_description_long(self): def test_description_long(self):
# Create a 50 word comment and make sure the description returns # Create a 50 word comment and make sure the description returns
# only the first 25 words # only the first 25 words
comment_long = createObject('plone.Comment') comment_long = createObject("plone.Comment")
comment_long.title = 'Long Comment' comment_long.title = "Long Comment"
comment_long.text = LONG_TEXT comment_long.text = LONG_TEXT
self.conversation.addComment(comment_long) self.conversation.addComment(comment_long)
self.assertEqual( self.assertEqual(
catalog.description(comment_long)(), catalog.description(comment_long)(),
LONG_TEXT_CUT.replace('\n', ' '), LONG_TEXT_CUT.replace("\n", " "),
) )
def test_dates(self): def test_dates(self):
# Test if created, modified, effective etc. are set correctly # Test if created, modified, effective etc. are set correctly
self.assertEqual( self.assertEqual(
catalog.created(self.comment)(), catalog.created(self.comment)(),
DateTime(2006, 9, 17, 14, 18, 12, 'GMT'), DateTime(2006, 9, 17, 14, 18, 12, "GMT"),
) )
self.assertEqual( self.assertEqual(
catalog.effective(self.comment)(), catalog.effective(self.comment)(),
DateTime(2006, 9, 17, 14, 18, 12, 'GMT'), DateTime(2006, 9, 17, 14, 18, 12, "GMT"),
) )
self.assertEqual( self.assertEqual(
catalog.modified(self.comment)(), 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): def test_searchable_text(self):
# Test if searchable text is a concatenation of title and comment text # Test if searchable text is a concatenation of title and comment text
self.assertEqual( self.assertEqual(
catalog.searchable_text(self.comment)(), catalog.searchable_text(self.comment)(),
('Lorem ipsum dolor sit amet.'), ("Lorem ipsum dolor sit amet."),
) )
self.assertTrue(isinstance( self.assertTrue(
isinstance(
catalog.searchable_text, catalog.searchable_text,
DelegatingIndexerFactory, DelegatingIndexerFactory,
)) )
)
def test_creator(self): def test_creator(self):
self.assertEqual(catalog.creator(self.comment)(), ('jim')) self.assertEqual(catalog.creator(self.comment)(), ("jim"))
def test_in_response_to(self): def test_in_response_to(self):
# make sure in_response_to returns the title or id of the content # 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(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 ..browser.moderation import BulkActionsView
from plone.app.discussion.browser.moderation import BulkActionsView from ..browser.moderation import CommentTransition
from plone.app.discussion.browser.moderation import DeleteComment from ..browser.moderation import DeleteComment
from plone.app.discussion.browser.moderation import CommentTransition from ..browser.moderation import View
from plone.app.discussion.browser.moderation import View from ..interfaces import IConversation
from plone.app.discussion.interfaces import IConversation from ..interfaces import IDiscussionSettings
from plone.app.discussion.interfaces import IDiscussionSettings from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.testing import setRoles from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
@ -21,59 +20,57 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.app = self.layer['app'] self.app = self.layer["app"]
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
self.request = self.layer['request'] self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.wf = getToolByName(self.portal, self.wf = getToolByName(self.portal, "portal_workflow", None)
'portal_workflow',
None)
self.context = self.portal self.context = self.portal
self.portal.portal_workflow.setChainForPortalTypes( self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",),
'comment_review_workflow', "comment_review_workflow",
) )
self.wf_tool = self.portal.portal_workflow self.wf_tool = self.portal.portal_workflow
# Add a conversation with three comments # Add a conversation with three comments
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.title = 'Comment 1' comment1.title = "Comment 1"
comment1.text = 'Comment text' comment1.text = "Comment text"
comment1.Creator = 'Jim' comment1.Creator = "Jim"
new_id_1 = conversation.addComment(comment1) new_id_1 = conversation.addComment(comment1)
self.comment1 = self.portal.doc1.restrictedTraverse( self.comment1 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_1), f"++conversation++default/{new_id_1}",
) )
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.title = 'Comment 2' comment2.title = "Comment 2"
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2.Creator = 'Joe' comment2.Creator = "Joe"
new_id_2 = conversation.addComment(comment2) new_id_2 = conversation.addComment(comment2)
self.comment2 = self.portal.doc1.restrictedTraverse( self.comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_2), f"++conversation++default/{new_id_2}",
) )
comment3 = createObject('plone.Comment') comment3 = createObject("plone.Comment")
comment3.title = 'Comment 3' comment3.title = "Comment 3"
comment3.text = 'Comment text' comment3.text = "Comment text"
comment3.Creator = 'Emma' comment3.Creator = "Emma"
new_id_3 = conversation.addComment(comment3) new_id_3 = conversation.addComment(comment3)
self.comment3 = self.portal.doc1.restrictedTraverse( self.comment3 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_3), f"++conversation++default/{new_id_3}",
) )
self.conversation = conversation self.conversation = conversation
def test_default_bulkaction(self): def test_default_bulkaction(self):
# Make sure no error is raised when no bulk actions has been supplied # Make sure no error is raised when no bulk actions has been supplied
self.request.set('form.select.BulkAction', '-1') self.request.set("form.select.BulkAction", "-1")
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())]) self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request) view = BulkActionsView(self.portal, self.request)
self.assertFalse(view()) self.assertFalse(view())
def test_publish(self): def test_publish(self):
self.request.set('form.select.BulkAction', 'publish') self.request.set("form.select.BulkAction", "publish")
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())]) self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request) view = BulkActionsView(self.portal, self.request)
view() view()
@ -81,16 +78,16 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Count published comments # Count published comments
published_comments = 0 published_comments = 0
for r in self.conversation.getThreads(): for r in self.conversation.getThreads():
comment_obj = r['comment'] comment_obj = r["comment"]
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state') workflow_status = self.wf.getInfoFor(comment_obj, "review_state")
if workflow_status == 'published': if workflow_status == "published":
published_comments += 1 published_comments += 1
# Make sure the comment has been published # Make sure the comment has been published
self.assertEqual(published_comments, 1) self.assertEqual(published_comments, 1)
def test_mark_as_spam(self): def test_mark_as_spam(self):
self.request.set('form.select.BulkAction', 'mark_as_spam') self.request.set("form.select.BulkAction", "mark_as_spam")
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())]) self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request) view = BulkActionsView(self.portal, self.request)
@ -99,9 +96,9 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Count spam comments # Count spam comments
spam_comments = 0 spam_comments = 0
for r in self.conversation.getThreads(): for r in self.conversation.getThreads():
comment_obj = r['comment'] comment_obj = r["comment"]
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state') workflow_status = self.wf.getInfoFor(comment_obj, "review_state")
if workflow_status == 'spam': if workflow_status == "spam":
spam_comments += 1 spam_comments += 1
# Make sure the comment has been marked as spam # Make sure the comment has been marked as spam
self.assertEqual(spam_comments, 1) self.assertEqual(spam_comments, 1)
@ -110,9 +107,14 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Initially we have three comments # Initially we have three comments
self.assertEqual(len(self.conversation.objectIds()), 3) self.assertEqual(len(self.conversation.objectIds()), 3)
# Delete two comments with bulk actions # Delete two comments with bulk actions
self.request.set('form.select.BulkAction', 'delete') self.request.set("form.select.BulkAction", "delete")
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath()), self.request.set(
'/'.join(self.comment3.getPhysicalPath())]) "paths",
[
"/".join(self.comment1.getPhysicalPath()),
"/".join(self.comment3.getPhysicalPath()),
],
)
view = BulkActionsView(self.app, self.request) view = BulkActionsView(self.app, self.request)
view() view()

View File

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*- from ..browser.moderation import BulkActionsView
from plone.app.discussion.browser.moderation import BulkActionsView from ..browser.moderation import CommentTransition
from plone.app.discussion.browser.moderation import DeleteComment from ..browser.moderation import DeleteComment
from plone.app.discussion.browser.moderation import CommentTransition from ..browser.moderation import View
from plone.app.discussion.browser.moderation import View from ..interfaces import IConversation
from plone.app.discussion.interfaces import IConversation from ..interfaces import IDiscussionSettings
from plone.app.discussion.interfaces import IDiscussionSettings from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.testing import setRoles from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
@ -21,21 +20,19 @@ class ModerationViewTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.app = self.layer['app'] self.app = self.layer["app"]
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
self.request = self.layer['request'] self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.portal_discussion = getToolByName(self.portal, self.portal_discussion = getToolByName(self.portal, "portal_discussion", None)
'portal_discussion', self.membership_tool = getToolByName(self.portal, "portal_membership")
None)
self.membership_tool = getToolByName(self.portal,
'portal_membership')
self.memberdata = self.portal.portal_memberdata self.memberdata = self.portal.portal_memberdata
request = self.app.REQUEST request = self.app.REQUEST
context = getattr(self.portal, 'doc1') context = getattr(self.portal, "doc1")
self.view = View(context, request) self.view = View(context, request)
self.portal.portal_workflow.setChainForPortalTypes( self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), 'comment_review_workflow') ("Discussion Item",), "comment_review_workflow"
)
self.wf_tool = self.portal.portal_workflow self.wf_tool = self.portal.portal_workflow
def test_moderation_enabled(self): def test_moderation_enabled(self):
@ -43,15 +40,17 @@ class ModerationViewTest(unittest.TestCase):
workflow implements a 'pending' state. workflow implements a 'pending' state.
""" """
# If workflow is not set, enabled must return False # 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) self.assertEqual(self.view.moderation_enabled(), False)
# The comment_one_state_workflow does not have a 'pending' state # The comment_one_state_workflow does not have a 'pending' state
self.wf_tool.setChainForPortalTypes(('Discussion Item',), self.wf_tool.setChainForPortalTypes(
('comment_one_state_workflow,')) ("Discussion Item",), ("comment_one_state_workflow,")
)
self.assertEqual(self.view.moderation_enabled(), False) self.assertEqual(self.view.moderation_enabled(), False)
# The comment_review_workflow does have a 'pending' state # The comment_review_workflow does have a 'pending' state
self.wf_tool.setChainForPortalTypes(('Discussion Item',), self.wf_tool.setChainForPortalTypes(
('comment_review_workflow,')) ("Discussion Item",), ("comment_review_workflow,")
)
self.assertEqual(self.view.moderation_enabled(), True) self.assertEqual(self.view.moderation_enabled(), True)
@ -60,59 +59,57 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.app = self.layer['app'] self.app = self.layer["app"]
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
self.request = self.layer['request'] self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
self.wf = getToolByName(self.portal, self.wf = getToolByName(self.portal, "portal_workflow", None)
'portal_workflow',
None)
self.context = self.portal self.context = self.portal
self.portal.portal_workflow.setChainForPortalTypes( self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",),
'comment_review_workflow', "comment_review_workflow",
) )
self.wf_tool = self.portal.portal_workflow self.wf_tool = self.portal.portal_workflow
# Add a conversation with three comments # Add a conversation with three comments
conversation = IConversation(self.portal.doc1) conversation = IConversation(self.portal.doc1)
comment1 = createObject('plone.Comment') comment1 = createObject("plone.Comment")
comment1.title = 'Comment 1' comment1.title = "Comment 1"
comment1.text = 'Comment text' comment1.text = "Comment text"
comment1.Creator = 'Jim' comment1.Creator = "Jim"
new_id_1 = conversation.addComment(comment1) new_id_1 = conversation.addComment(comment1)
self.comment1 = self.portal.doc1.restrictedTraverse( self.comment1 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_1), f"++conversation++default/{new_id_1}",
) )
comment2 = createObject('plone.Comment') comment2 = createObject("plone.Comment")
comment2.title = 'Comment 2' comment2.title = "Comment 2"
comment2.text = 'Comment text' comment2.text = "Comment text"
comment2.Creator = 'Joe' comment2.Creator = "Joe"
new_id_2 = conversation.addComment(comment2) new_id_2 = conversation.addComment(comment2)
self.comment2 = self.portal.doc1.restrictedTraverse( self.comment2 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_2), f"++conversation++default/{new_id_2}",
) )
comment3 = createObject('plone.Comment') comment3 = createObject("plone.Comment")
comment3.title = 'Comment 3' comment3.title = "Comment 3"
comment3.text = 'Comment text' comment3.text = "Comment text"
comment3.Creator = 'Emma' comment3.Creator = "Emma"
new_id_3 = conversation.addComment(comment3) new_id_3 = conversation.addComment(comment3)
self.comment3 = self.portal.doc1.restrictedTraverse( self.comment3 = self.portal.doc1.restrictedTraverse(
'++conversation++default/{0}'.format(new_id_3), f"++conversation++default/{new_id_3}",
) )
self.conversation = conversation self.conversation = conversation
def test_default_bulkaction(self): def test_default_bulkaction(self):
# Make sure no error is raised when no bulk actions has been supplied # Make sure no error is raised when no bulk actions has been supplied
self.request.set('form.select.BulkAction', '-1') self.request.set("form.select.BulkAction", "-1")
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())]) self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request) view = BulkActionsView(self.portal, self.request)
self.assertFalse(view()) self.assertFalse(view())
def test_publish(self): def test_publish(self):
self.request.set('form.select.BulkAction', 'publish') self.request.set("form.select.BulkAction", "publish")
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())]) self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
view = BulkActionsView(self.portal, self.request) view = BulkActionsView(self.portal, self.request)
view() view()
@ -120,9 +117,9 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Count published comments # Count published comments
published_comments = 0 published_comments = 0
for r in self.conversation.getThreads(): for r in self.conversation.getThreads():
comment_obj = r['comment'] comment_obj = r["comment"]
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state') workflow_status = self.wf.getInfoFor(comment_obj, "review_state")
if workflow_status == 'published': if workflow_status == "published":
published_comments += 1 published_comments += 1
# Make sure the comment has been published # Make sure the comment has been published
self.assertEqual(published_comments, 1) self.assertEqual(published_comments, 1)
@ -131,9 +128,14 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
# Initially we have three comments # Initially we have three comments
self.assertEqual(len(self.conversation.objectIds()), 3) self.assertEqual(len(self.conversation.objectIds()), 3)
# Delete two comments with bulk actions # Delete two comments with bulk actions
self.request.set('form.select.BulkAction', 'delete') self.request.set("form.select.BulkAction", "delete")
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath()), self.request.set(
'/'.join(self.comment3.getPhysicalPath())]) "paths",
[
"/".join(self.comment1.getPhysicalPath()),
"/".join(self.comment3.getPhysicalPath()),
],
)
view = BulkActionsView(self.app, self.request) view = BulkActionsView(self.app, self.request)
view() view()
@ -151,41 +153,41 @@ class RedirectionTest(unittest.TestCase):
def setUp(self): def setUp(self):
# Update settings. # Update settings.
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
self.request = self.layer['request'] self.request = self.layer["request"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
# applyProfile(self.portal, 'plone.app.discussion:default') # applyProfile(self.portal, 'plone.app.discussion:default')
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings) settings = registry.forInterface(IDiscussionSettings)
settings.globally_enabled = True settings.globally_enabled = True
self.portal.portal_workflow.setChainForPortalTypes( self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",),
('comment_review_workflow',), ("comment_review_workflow",),
) )
# Create page plus comment. # Create page plus comment.
self.portal.invokeFactory( self.portal.invokeFactory(
id='page', id="page",
title='Page 1', title="Page 1",
type_name='Document', type_name="Document",
) )
self.page = self.portal.page self.page = self.portal.page
self.conversation = IConversation(self.page) self.conversation = IConversation(self.page)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
self.comment_id = self.conversation.addComment(comment) self.comment_id = self.conversation.addComment(comment)
self.comment = list(self.conversation.getComments())[0] self.comment = list(self.conversation.getComments())[0]
def test_regression(self): def test_regression(self):
page_url = self.page.absolute_url() page_url = self.page.absolute_url()
self.request['HTTP_REFERER'] = page_url self.request["HTTP_REFERER"] = page_url
for Klass in (DeleteComment, CommentTransition): for Klass in (DeleteComment, CommentTransition):
view = Klass(self.comment, self.request) view = Klass(self.comment, self.request)
view.__parent__ = self.comment view.__parent__ = self.comment
self.assertEqual(page_url, view()) self.assertEqual(page_url, view())
def test_valid_next_url(self): 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): for Klass in (DeleteComment, CommentTransition):
view = Klass(self.comment, self.request) view = Klass(self.comment, self.request)
view.__parent__ = self.comment 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 Acquisition import aq_base
from plone.app.discussion.interfaces import IConversation from persistent.list import PersistentList
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
from plone.app.testing import setRoles from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from plone.base.interfaces import IMailSchema
from plone.registry.interfaces import IRegistry 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.interfaces import IMailHost
from Products.MailHost.MailHost import _mungeHeaders
from Products.MailHost.MailHost import MailBase
from zope.component import createObject from zope.component import createObject
from zope.component import getSiteManager from zope.component import getSiteManager
from zope.component import getUtility from zope.component import getUtility
@ -16,50 +17,85 @@ from zope.component import queryUtility
import unittest 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): class TestUserNotificationUnit(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
# Set up a mock mailhost # Set up a mock mailhost
self.portal._original_MailHost = self.portal.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 = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost) sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(mailhost, provided=IMailHost) sm.registerUtility(mailhost, provided=IMailHost)
# We need to fake a valid mail setup # We need to fake a valid mail setup
registry = getUtility(IRegistry) registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone') mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mail_settings.email_from_address = 'portal@plone.test' mail_settings.email_from_address = "portal@plone.test"
self.mailhost = self.portal.MailHost self.mailhost = self.portal.MailHost
# Enable user notification setting # Enable user notification setting
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
registry['plone.app.discussion.interfaces.IDiscussionSettings' + registry[
'.user_notification_enabled'] = True "plone.app.discussion.interfaces.IDiscussionSettings"
# Archetypes content types store data as utf-8 encoded strings + ".user_notification_enabled"
# The missing u in front of a string is therefor not missing ] = True
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) self.conversation = IConversation(self.portal.doc1)
def beforeTearDown(self): def beforeTearDown(self):
self.portal.MailHost = self.portal._original_MailHost self.portal.MailHost = self.portal._original_MailHost
sm = getSiteManager(context=self.portal) sm = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost) sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(aq_base(self.portal._original_MailHost), sm.registerUtility(aq_base(self.portal._original_MailHost), provided=IMailHost)
provided=IMailHost)
def test_notify_user(self): def test_notify_user(self):
# Add a comment with user notification enabled. Add another comment # Add a comment with user notification enabled. Add another comment
# and make sure an email is send to the user of the first comment. # and make sure an email is send to the user of the first comment.
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment.user_notification = True comment.user_notification = True
comment.author_email = 'john@plone.test' comment.author_email = "john@plone.test"
self.conversation.addComment(comment) self.conversation.addComment(comment)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment_id = self.conversation.addComment(comment) comment_id = self.conversation.addComment(comment)
@ -67,52 +103,46 @@ class TestUserNotificationUnit(unittest.TestCase):
self.assertTrue(self.mailhost.messages[0]) self.assertTrue(self.mailhost.messages[0])
msg = self.mailhost.messages[0] msg = self.mailhost.messages[0]
msg = msg.decode("utf-8") msg = msg.decode("utf-8")
self.assertIn('To: john@plone.test', msg) self.assertIn("To: john@plone.test", msg)
self.assertIn('From: portal@plone.test', msg) self.assertIn("From: portal@plone.test", msg)
# We expect the headers to be properly header encoded (7-bit): # We expect the headers to be properly header encoded (7-bit):
self.assertIn( self.assertIn("Subject: =?utf-8?q?A_comment_has_been_posted=2E?=", msg)
'Subject: =?utf-8?q?A_comment_has_been_posted=2E?=',
msg)
# The output should be encoded in a reasonable manner # The output should be encoded in a reasonable manner
# (in this case quoted-printable). # (in this case quoted-printable).
# Depending on which Python version and which Products.MailHost version, # Depending on which Python version and which Products.MailHost version,
# you may get lines separated by '\n' or '\r\n' in here. # you may get lines separated by '\n' or '\r\n' in here.
msg = msg.replace('\r\n', '\n') msg = msg.replace("\r\n", "\n")
self.assertIn( self.assertIn('A comment on "K=C3=B6lle Alaaf" has been posted here:', msg)
'A comment on "K=C3=B6lle Alaaf" has been posted here:', self.assertIn(f"http://nohost/plone/d=\noc1/view#{comment_id}", msg)
msg) self.assertIn("Comment text", msg)
self.assertIn( self.assertNotIn("Approve comment", msg)
'http://nohost/plone/d=\noc1/view#{0}'.format(comment_id), self.assertNotIn("Delete comment", msg)
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): def test_do_not_notify_user_when_notification_is_disabled(self):
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
registry[ registry[
'plone.app.discussion.interfaces.IDiscussionSettings.' + "plone.app.discussion.interfaces.IDiscussionSettings."
'user_notification_enabled' + "user_notification_enabled"
] = False ] = False
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment.user_notification = True comment.user_notification = True
comment.author_email = 'john@plone.test' comment.author_email = "john@plone.test"
self.conversation.addComment(comment) self.conversation.addComment(comment)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
self.conversation.addComment(comment) self.conversation.addComment(comment)
self.assertEqual(len(self.mailhost.messages), 0) self.assertEqual(len(self.mailhost.messages), 0)
def test_do_not_notify_user_when_email_address_is_given(self): def test_do_not_notify_user_when_email_address_is_given(self):
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment.user_notification = True comment.user_notification = True
self.conversation.addComment(comment) self.conversation.addComment(comment)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
self.conversation.addComment(comment) 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 # Set sender mail address to none and make sure no email is send to
# the moderator. # the moderator.
registry = getUtility(IRegistry) registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone') mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mail_settings.email_from_address = None mail_settings.email_from_address = None
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment.user_notification = True comment.user_notification = True
comment.author_email = 'john@plone.test' comment.author_email = "john@plone.test"
self.conversation.addComment(comment) self.conversation.addComment(comment)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
self.conversation.addComment(comment) self.conversation.addComment(comment)
self.assertEqual(len(self.mailhost.messages), 0) 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 # When a user has added two comments in a conversation and has
# both times requested email notification, do not send him two # both times requested email notification, do not send him two
# emails when another comment has been added. # emails when another comment has been added.
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment.user_notification = True comment.user_notification = True
comment.author_email = 'john@plone.test' comment.author_email = "john@plone.test"
self.conversation.addComment(comment) self.conversation.addComment(comment)
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment.user_notification = True comment.user_notification = True
comment.author_email = 'john@plone.test' comment.author_email = "john@plone.test"
self.conversation.addComment(comment) self.conversation.addComment(comment)
@ -163,48 +193,45 @@ class TestModeratorNotificationUnit(unittest.TestCase):
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
def setUp(self): def setUp(self):
self.portal = self.layer['portal'] self.portal = self.layer["portal"]
setRoles(self.portal, TEST_USER_ID, ['Manager']) setRoles(self.portal, TEST_USER_ID, ["Manager"])
# Set up a mock mailhost # Set up a mock mailhost
self.portal._original_MailHost = self.portal.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 = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost) sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(mailhost, provided=IMailHost) sm.registerUtility(mailhost, provided=IMailHost)
# We need to fake a valid mail setup # We need to fake a valid mail setup
registry = getUtility(IRegistry) registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone') mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mail_settings.email_from_address = 'portal@plone.test' mail_settings.email_from_address = "portal@plone.test"
self.mailhost = self.portal.MailHost self.mailhost = self.portal.MailHost
# Enable comment moderation # Enable comment moderation
self.portal.portal_types['Document'].allow_discussion = True self.portal.portal_types["Document"].allow_discussion = True
self.portal.portal_workflow.setChainForPortalTypes( self.portal.portal_workflow.setChainForPortalTypes(
('Discussion Item',), ("Discussion Item",),
('comment_review_workflow',), ("comment_review_workflow",),
) )
# Enable moderator notification setting # Enable moderator notification setting
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
registry[ registry[
'plone.app.discussion.interfaces.IDiscussionSettings.' + "plone.app.discussion.interfaces.IDiscussionSettings."
'moderator_notification_enabled' + "moderator_notification_enabled"
] = True ] = True
# Archetypes content types store data as utf-8 encoded strings self.portal.doc1.title = "Kölle Alaaf" # What is 'Fasching'?
# The missing u in front of a string is therefor not missing
self.portal.doc1.title = 'Kölle Alaaf' # What is 'Fasching'?
self.conversation = IConversation(self.portal.doc1) self.conversation = IConversation(self.portal.doc1)
def beforeTearDown(self): def beforeTearDown(self):
self.portal.MailHost = self.portal._original_MailHost self.portal.MailHost = self.portal._original_MailHost
sm = getSiteManager(context=self.portal) sm = getSiteManager(context=self.portal)
sm.unregisterUtility(provided=IMailHost) sm.unregisterUtility(provided=IMailHost)
sm.registerUtility(aq_base(self.portal._original_MailHost), sm.registerUtility(aq_base(self.portal._original_MailHost), provided=IMailHost)
provided=IMailHost)
def test_notify_moderator(self): def test_notify_moderator(self):
"""Add a comment and make sure an email is send to the moderator.""" """Add a comment and make sure an email is send to the moderator."""
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
comment.author_email = 'john@plone.test' comment.author_email = "john@plone.test"
comment_id = self.conversation.addComment(comment) comment_id = self.conversation.addComment(comment)
@ -212,54 +239,41 @@ class TestModeratorNotificationUnit(unittest.TestCase):
self.assertTrue(self.mailhost.messages[0]) self.assertTrue(self.mailhost.messages[0])
msg = self.mailhost.messages[0] msg = self.mailhost.messages[0]
msg = msg.decode("utf-8") msg = msg.decode("utf-8")
self.assertTrue('To: portal@plone.test' in msg) self.assertTrue("To: portal@plone.test" in msg)
self.assertTrue('From: portal@plone.test' in msg) self.assertTrue("From: portal@plone.test" in msg)
# We expect the headers to be properly header encoded (7-bit): # We expect the headers to be properly header encoded (7-bit):
self.assertTrue( self.assertTrue("Subject: =?utf-8?q?A_comment_has_been_posted=2E?=" in msg)
'Subject: =?utf-8?q?A_comment_has_been_posted=2E?='
in msg)
# The output should be encoded in a reasonable manner # The output should be encoded in a reasonable manner
# (in this case quoted-printable): # (in this case quoted-printable):
self.assertTrue( self.assertTrue('A comment on "K=C3=B6lle Alaaf" has been posted' in msg)
'A comment on "K=C3=B6lle Alaaf" has been posted' self.assertIn(f"http://nohost/plone/doc1/view#{comment_id}", msg)
in msg self.assertIn(comment.author_email, msg)
) self.assertIn(comment.text, msg)
self.assertIn(
'http://nohost/plone/doc1/view#{0}'.format(comment_id),
msg
)
self.assertIn(
comment.author_email,
msg
)
self.assertIn(
comment.text,
msg
)
def test_notify_moderator_specific_address(self): def test_notify_moderator_specific_address(self):
# A moderator email address can be specified in the control panel. # A moderator email address can be specified in the control panel.
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
registry['plone.app.discussion.interfaces.IDiscussionSettings' + registry[
'.moderator_email'] = 'test@example.com' "plone.app.discussion.interfaces.IDiscussionSettings" + ".moderator_email"
comment = createObject('plone.Comment') ] = "test@example.com"
comment.text = 'Comment text' comment = createObject("plone.Comment")
comment.text = "Comment text"
self.conversation.addComment(comment) self.conversation.addComment(comment)
self.assertEqual(len(self.mailhost.messages), 1) self.assertEqual(len(self.mailhost.messages), 1)
msg = self.mailhost.messages[0] msg = self.mailhost.messages[0]
msg = msg.decode("utf-8") 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): 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 # Set sender mail address to nonw and make sure no email is send to the
# moderator. # moderator.
registry = getUtility(IRegistry) registry = getUtility(IRegistry)
mail_settings = registry.forInterface(IMailSchema, prefix='plone') mail_settings = registry.forInterface(IMailSchema, prefix="plone")
mail_settings.email_from_address = None mail_settings.email_from_address = None
comment = createObject('plone.Comment') comment = createObject("plone.Comment")
comment.text = 'Comment text' comment.text = "Comment text"
self.conversation.addComment(comment) self.conversation.addComment(comment)
@ -269,10 +283,12 @@ class TestModeratorNotificationUnit(unittest.TestCase):
# Disable moderator notification setting and make sure no email is send # Disable moderator notification setting and make sure no email is send
# to the moderator. # to the moderator.
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
registry['plone.app.discussion.interfaces.IDiscussionSettings.' + registry[
'moderator_notification_enabled'] = False "plone.app.discussion.interfaces.IDiscussionSettings."
comment = createObject('plone.Comment') + "moderator_notification_enabled"
comment.text = 'Comment text' ] = False
comment = createObject("plone.Comment")
comment.text = "Comment text"
self.conversation.addComment(comment) self.conversation.addComment(comment)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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