Merge pull request #195 from plone/plone-base-overhaul
Plone base overhaul
This commit is contained in:
commit
fe10c7448f
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# plone.app.discussion documentation build configuration file, created by
|
||||
# sphinx-quickstart on Thu Mar 18 10:17:15 2010.
|
||||
@ -24,184 +23,190 @@ import sys
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.coverage',
|
||||
'repoze.sphinx.autointerface'
|
||||
]
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.doctest",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.coverage",
|
||||
"repoze.sphinx.autointerface",
|
||||
]
|
||||
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.txt'
|
||||
source_suffix = ".txt"
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8'
|
||||
# source_encoding = 'utf-8'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = u'plone.app.discussion'
|
||||
copyright = u'2010, Timo Stollenwerk - Plone Foundation'
|
||||
project = "plone.app.discussion"
|
||||
copyright = "2010, Timo Stollenwerk - Plone Foundation"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '2.0'
|
||||
version = "2.0"
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '2.0'
|
||||
release = "2.0"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
# language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of documents that shouldn't be included in the build.
|
||||
#unused_docs = []
|
||||
# unused_docs = []
|
||||
|
||||
# List of directories, relative to source directory, that shouldn't be searched
|
||||
# for source files.
|
||||
exclude_trees = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
# modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
#html_theme = 'plone'
|
||||
# html_theme = 'plone'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
html_theme_path = ['_themes']
|
||||
html_theme_path = ["_themes"]
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_use_modindex = True
|
||||
# html_use_modindex = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = ''
|
||||
# html_file_suffix = ''
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'ploneappdiscussiondoc'
|
||||
htmlhelp_basename = "ploneappdiscussiondoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------
|
||||
|
||||
# The paper size ('letter' or 'a4').
|
||||
#latex_paper_size = 'letter'
|
||||
# latex_paper_size = 'letter'
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#latex_font_size = '10pt'
|
||||
# latex_font_size = '10pt'
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'ploneappdiscussion.tex', u'plone.app.discussion Documentation',
|
||||
u'Timo Stollenwerk', 'manual'),
|
||||
(
|
||||
"index",
|
||||
"ploneappdiscussion.tex",
|
||||
"plone.app.discussion Documentation",
|
||||
"Timo Stollenwerk",
|
||||
"manual",
|
||||
),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
# latex_use_parts = False
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#latex_preamble = ''
|
||||
# latex_preamble = ''
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_use_modindex = True
|
||||
# latex_use_modindex = True
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'http://docs.python.org/': None}
|
||||
intersphinx_mapping = {"http://docs.python.org/": None}
|
||||
|
@ -58,13 +58,13 @@ comment form with the "website" field::
|
||||
|
||||
# Interface to define the fields we want to add to the comment form.
|
||||
class ICommentExtenderFields(Interface):
|
||||
website = schema.TextLine(title=u"Website", required=False)
|
||||
website = schema.TextLine(title="Website", required=False)
|
||||
|
||||
# Persistent class that implements the ICommentExtenderFields interface
|
||||
@adapter(Comment)
|
||||
class CommentExtenderFields(Persistent):
|
||||
interface.implements(ICommentExtenderFields)
|
||||
website = u""
|
||||
website = ""
|
||||
|
||||
# CommentExtenderFields factory
|
||||
CommentExtenderFactory = factory(CommentExtenderFields)
|
||||
|
@ -20,8 +20,8 @@ configure.zcml::
|
||||
Define an interface IMyDexterityContentType groked schema, I added::
|
||||
|
||||
allowDiscussion = schema.Bool(
|
||||
title=_(u"Allow Users to Comment"),
|
||||
description=_(u"Allow users to commemt on you. Comments
|
||||
title=_("Allow Users to Comment"),
|
||||
description=_("Allow users to comment on you. Comments
|
||||
are shown at the end of each page"),
|
||||
required=True,
|
||||
default=True,
|
||||
|
2
news/195.breaking
Normal file
2
news/195.breaking
Normal file
@ -0,0 +1,2 @@
|
||||
Code style black & isort. Remove six usage. Use plone.base and move annotation key over to here.
|
||||
[jensens]
|
@ -1,2 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__import__('pkg_resources').declare_namespace(__name__)
|
||||
__import__("pkg_resources").declare_namespace(__name__)
|
||||
|
@ -1,2 +1 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
__import__('pkg_resources').declare_namespace(__name__)
|
||||
__import__("pkg_resources").declare_namespace(__name__)
|
||||
|
@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from zope.i18nmessageid import MessageFactory
|
||||
|
||||
|
||||
_ = MessageFactory('plone')
|
||||
_ = MessageFactory("plone")
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Captcha validator, see captcha.txt for design notes.
|
||||
from persistent import Persistent
|
||||
from plone.app.discussion.browser.comments import CommentForm
|
||||
@ -21,9 +20,9 @@ from zope.publisher.interfaces.browser import IDefaultBrowserLayer
|
||||
@adapter(Comment)
|
||||
@interface.implementer(ICaptcha)
|
||||
class Captcha(Persistent):
|
||||
"""Captcha input field.
|
||||
"""
|
||||
captcha = u''
|
||||
"""Captcha input field."""
|
||||
|
||||
captcha = ""
|
||||
|
||||
|
||||
Captcha = factory(Captcha)
|
||||
@ -47,22 +46,24 @@ class CaptchaExtender(extensible.FormExtender):
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
self.captcha = settings.captcha
|
||||
portal_membership = getToolByName(self.context, 'portal_membership')
|
||||
portal_membership = getToolByName(self.context, "portal_membership")
|
||||
self.isAnon = portal_membership.isAnonymousUser()
|
||||
|
||||
def update(self):
|
||||
if self.captcha != 'disabled' and self.isAnon:
|
||||
if self.captcha != "disabled" and self.isAnon:
|
||||
# Add a captcha field if captcha is enabled in the registry
|
||||
self.add(ICaptcha, prefix='')
|
||||
if self.captcha == 'captcha':
|
||||
self.add(ICaptcha, prefix="")
|
||||
if self.captcha == "captcha":
|
||||
from plone.formwidget.captcha import CaptchaFieldWidget
|
||||
self.form.fields['captcha'].widgetFactory = CaptchaFieldWidget
|
||||
elif self.captcha == 'recaptcha':
|
||||
|
||||
self.form.fields["captcha"].widgetFactory = CaptchaFieldWidget
|
||||
elif self.captcha == "recaptcha":
|
||||
from plone.formwidget.recaptcha import ReCaptchaFieldWidget
|
||||
self.form.fields['captcha'].widgetFactory = \
|
||||
ReCaptchaFieldWidget
|
||||
elif self.captcha == 'norobots':
|
||||
|
||||
self.form.fields["captcha"].widgetFactory = ReCaptchaFieldWidget
|
||||
elif self.captcha == "norobots":
|
||||
from collective.z3cform.norobots import NorobotsFieldWidget
|
||||
self.form.fields['captcha'].widgetFactory = NorobotsFieldWidget
|
||||
|
||||
self.form.fields["captcha"].widgetFactory = NorobotsFieldWidget
|
||||
else:
|
||||
self.form.fields['captcha'].mode = interfaces.HIDDEN_MODE
|
||||
self.form.fields["captcha"].mode = interfaces.HIDDEN_MODE
|
||||
|
@ -1,4 +1,3 @@
|
||||
# coding: utf-8
|
||||
from .comments import CommentForm
|
||||
from AccessControl import getSecurityManager
|
||||
from Acquisition import aq_inner
|
||||
@ -14,7 +13,6 @@ from zope.component import getMultiAdapter
|
||||
from zope.component import getUtility
|
||||
from zope.event import notify
|
||||
from zope.lifecycleevent import ObjectModifiedEvent
|
||||
from .comments import CommentForm
|
||||
|
||||
|
||||
class View(BrowserView):
|
||||
@ -38,8 +36,7 @@ class View(BrowserView):
|
||||
context = aq_inner(self.context)
|
||||
|
||||
registry = getUtility(IRegistry)
|
||||
view_action_types = registry.get(
|
||||
'plone.types_use_view_action_in_listings', [])
|
||||
view_action_types = registry.get("plone.types_use_view_action_in_listings", [])
|
||||
|
||||
obj = aq_parent(aq_parent(context))
|
||||
url = obj.absolute_url()
|
||||
@ -50,33 +47,34 @@ class View(BrowserView):
|
||||
will redirect right to the binary object, bypassing comments.
|
||||
"""
|
||||
if obj.portal_type in view_action_types:
|
||||
url = '{0}/view'.format(url)
|
||||
url = f"{url}/view"
|
||||
|
||||
self.request.response.redirect('{0}#{1}'.format(url, context.id))
|
||||
self.request.response.redirect(f"{url}#{context.id}")
|
||||
|
||||
|
||||
class EditCommentForm(CommentForm):
|
||||
"""Form to edit an existing comment."""
|
||||
|
||||
ignoreContext = True
|
||||
id = 'edit-comment-form'
|
||||
label = _(u'edit_comment_form_title', default=u'Edit comment')
|
||||
id = "edit-comment-form"
|
||||
label = _("edit_comment_form_title", default="Edit comment")
|
||||
|
||||
def updateWidgets(self):
|
||||
super(EditCommentForm, self).updateWidgets()
|
||||
self.widgets['text'].value = self.context.text
|
||||
super().updateWidgets()
|
||||
self.widgets["text"].value = self.context.text
|
||||
# We have to rename the id, otherwise TinyMCE can't initialize
|
||||
# because there are two textareas with the same id.
|
||||
self.widgets['text'].id = 'overlay-comment-text'
|
||||
self.widgets["text"].id = "overlay-comment-text"
|
||||
|
||||
def _redirect(self, target=''):
|
||||
def _redirect(self, target=""):
|
||||
if not target:
|
||||
portal_state = getMultiAdapter((self.context, self.request),
|
||||
name=u'plone_portal_state')
|
||||
portal_state = getMultiAdapter(
|
||||
(self.context, self.request), name="plone_portal_state"
|
||||
)
|
||||
target = portal_state.portal_url()
|
||||
self.request.response.redirect(target)
|
||||
|
||||
@button.buttonAndHandler(_(u'label_save',
|
||||
default=u'Save'), name='comment')
|
||||
@button.buttonAndHandler(_("label_save", default="Save"), name="comment")
|
||||
def handleComment(self, action):
|
||||
|
||||
# Validate form
|
||||
@ -85,32 +83,28 @@ class EditCommentForm(CommentForm):
|
||||
return
|
||||
|
||||
# Check permissions
|
||||
can_edit = getSecurityManager().checkPermission(
|
||||
'Edit comments',
|
||||
self.context)
|
||||
mtool = getToolByName(self.context, 'portal_membership')
|
||||
can_edit = getSecurityManager().checkPermission("Edit comments", self.context)
|
||||
mtool = getToolByName(self.context, "portal_membership")
|
||||
if mtool.isAnonymousUser() or not can_edit:
|
||||
return
|
||||
|
||||
# Update text
|
||||
self.context.text = data['text']
|
||||
self.context.text = data["text"]
|
||||
# Notify that the object has been modified
|
||||
notify(ObjectModifiedEvent(self.context))
|
||||
|
||||
# Redirect to comment
|
||||
IStatusMessage(self.request).add(_(u'comment_edit_notification',
|
||||
default='Comment was edited'),
|
||||
type='info')
|
||||
return self._redirect(
|
||||
target=self.action.replace('@@edit-comment', '@@view'))
|
||||
IStatusMessage(self.request).add(
|
||||
_("comment_edit_notification", default="Comment was edited"), type="info"
|
||||
)
|
||||
return self._redirect(target=self.action.replace("@@edit-comment", "@@view"))
|
||||
|
||||
@button.buttonAndHandler(_(u'cancel_form_button',
|
||||
default=u'Cancel'), name='cancel')
|
||||
@button.buttonAndHandler(_("cancel_form_button", default="Cancel"), name="cancel")
|
||||
def handle_cancel(self, action):
|
||||
IStatusMessage(self.request).add(
|
||||
_(u'comment_edit_cancel_notification',
|
||||
default=u'Edit comment cancelled'),
|
||||
type='info')
|
||||
_("comment_edit_cancel_notification", default="Edit comment cancelled"),
|
||||
type="info",
|
||||
)
|
||||
return self._redirect(target=self.context.absolute_url())
|
||||
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from AccessControl import getSecurityManager
|
||||
from AccessControl import Unauthorized
|
||||
from Acquisition import aq_inner
|
||||
@ -12,15 +11,15 @@ from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.app.discussion.interfaces import IReplies
|
||||
from plone.app.layout.viewlets.common import ViewletBase
|
||||
from plone.base.utils import safe_text
|
||||
from plone.registry.interfaces import IRegistry
|
||||
from plone.z3cform import z2
|
||||
from plone.z3cform.fieldsets import extensible
|
||||
from plone.z3cform.interfaces import IWrappedForm
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
from Products.CMFPlone.utils import safe_unicode
|
||||
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
|
||||
from Products.statusmessages.interfaces import IStatusMessage
|
||||
from six.moves.urllib.parse import quote
|
||||
from urllib.parse import quote
|
||||
from z3c.form import button
|
||||
from z3c.form import field
|
||||
from z3c.form import form
|
||||
@ -35,28 +34,28 @@ from zope.interface import alsoProvides
|
||||
|
||||
|
||||
COMMENT_DESCRIPTION_PLAIN_TEXT = _(
|
||||
u'comment_description_plain_text',
|
||||
default=u'You can add a comment by filling out the form below. '
|
||||
u'Plain text formatting.',
|
||||
"comment_description_plain_text",
|
||||
default="You can add a comment by filling out the form below. "
|
||||
"Plain text formatting.",
|
||||
)
|
||||
|
||||
COMMENT_DESCRIPTION_MARKDOWN = _(
|
||||
u'comment_description_markdown',
|
||||
default=u'You can add a comment by filling out the form below. '
|
||||
u'Plain text formatting. You can use the Markdown syntax for '
|
||||
u'links and images.',
|
||||
"comment_description_markdown",
|
||||
default="You can add a comment by filling out the form below. "
|
||||
"Plain text formatting. You can use the Markdown syntax for "
|
||||
"links and images.",
|
||||
)
|
||||
|
||||
COMMENT_DESCRIPTION_INTELLIGENT_TEXT = _(
|
||||
u'comment_description_intelligent_text',
|
||||
default=u'You can add a comment by filling out the form below. '
|
||||
u'Plain text formatting. Web and email addresses are '
|
||||
u'transformed into clickable links.',
|
||||
"comment_description_intelligent_text",
|
||||
default="You can add a comment by filling out the form below. "
|
||||
"Plain text formatting. Web and email addresses are "
|
||||
"transformed into clickable links.",
|
||||
)
|
||||
|
||||
COMMENT_DESCRIPTION_MODERATION_ENABLED = _(
|
||||
u'comment_description_moderation_enabled',
|
||||
default=u'Comments are moderated.',
|
||||
"comment_description_moderation_enabled",
|
||||
default="Comments are moderated.",
|
||||
)
|
||||
|
||||
|
||||
@ -64,30 +63,31 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
|
||||
ignoreContext = True # don't use context to get widget data
|
||||
id = None
|
||||
label = _(u'Add a comment')
|
||||
fields = field.Fields(IComment).omit('portal_type',
|
||||
'__parent__',
|
||||
'__name__',
|
||||
'comment_id',
|
||||
'mime_type',
|
||||
'creator',
|
||||
'creation_date',
|
||||
'modification_date',
|
||||
'author_username',
|
||||
'title')
|
||||
label = _("Add a comment")
|
||||
fields = field.Fields(IComment).omit(
|
||||
"portal_type",
|
||||
"__parent__",
|
||||
"__name__",
|
||||
"comment_id",
|
||||
"mime_type",
|
||||
"creator",
|
||||
"creation_date",
|
||||
"modification_date",
|
||||
"author_username",
|
||||
"title",
|
||||
)
|
||||
|
||||
def updateFields(self):
|
||||
super(CommentForm, self).updateFields()
|
||||
self.fields['user_notification'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
super().updateFields()
|
||||
self.fields["user_notification"].widgetFactory = SingleCheckBoxFieldWidget
|
||||
|
||||
def updateWidgets(self):
|
||||
super(CommentForm, self).updateWidgets()
|
||||
super().updateWidgets()
|
||||
|
||||
# Widgets
|
||||
self.widgets['in_reply_to'].mode = interfaces.HIDDEN_MODE
|
||||
self.widgets['text'].addClass('autoresize')
|
||||
self.widgets['user_notification'].label = _(u'')
|
||||
self.widgets["in_reply_to"].mode = interfaces.HIDDEN_MODE
|
||||
self.widgets["text"].addClass("autoresize")
|
||||
self.widgets["user_notification"].label = _("")
|
||||
# Reset widget field settings to their defaults, which may be changed
|
||||
# further on. Otherwise, the email field might get set to required
|
||||
# when an anonymous user visits, and then remain required when an
|
||||
@ -97,19 +97,19 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
# would have no effect until the instance was restarted. Note that the
|
||||
# widget is new each time, but the field is the same item in memory as
|
||||
# the previous time.
|
||||
self.widgets['author_email'].field.required = False
|
||||
self.widgets["author_email"].field.required = False
|
||||
# The widget is new, but its 'required' setting is based on the
|
||||
# previous value on the field, so we need to reset it here. Changing
|
||||
# the field in updateFields does not help.
|
||||
self.widgets['author_email'].required = False
|
||||
self.widgets["author_email"].required = False
|
||||
|
||||
# Rename the id of the text widgets because there can be css-id
|
||||
# clashes with the text field of documents when using and overlay
|
||||
# with TinyMCE.
|
||||
self.widgets['text'].id = 'form-widgets-comment-text'
|
||||
self.widgets["text"].id = "form-widgets-comment-text"
|
||||
|
||||
# Anonymous / Logged-in
|
||||
mtool = getToolByName(self.context, 'portal_membership')
|
||||
mtool = getToolByName(self.context, "portal_membership")
|
||||
anon = mtool.isAnonymousUser()
|
||||
|
||||
registry = queryUtility(IRegistry)
|
||||
@ -119,56 +119,55 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
if settings.anonymous_email_enabled:
|
||||
# according to IDiscussionSettings.anonymous_email_enabled:
|
||||
# 'If selected, anonymous user will have to give their email.'
|
||||
self.widgets['author_email'].field.required = True
|
||||
self.widgets['author_email'].required = True
|
||||
self.widgets["author_email"].field.required = True
|
||||
self.widgets["author_email"].required = True
|
||||
else:
|
||||
self.widgets['author_email'].mode = interfaces.HIDDEN_MODE
|
||||
self.widgets["author_email"].mode = interfaces.HIDDEN_MODE
|
||||
else:
|
||||
self.widgets['author_name'].mode = interfaces.HIDDEN_MODE
|
||||
self.widgets['author_email'].mode = interfaces.HIDDEN_MODE
|
||||
self.widgets["author_name"].mode = interfaces.HIDDEN_MODE
|
||||
self.widgets["author_email"].mode = interfaces.HIDDEN_MODE
|
||||
|
||||
member = mtool.getAuthenticatedMember()
|
||||
member_email = member.getProperty('email')
|
||||
member_email = member.getProperty("email")
|
||||
|
||||
# Hide the user_notification checkbox if user notification is disabled
|
||||
# or the user is not logged in. Also check if the user has a valid
|
||||
# email address
|
||||
member_email_is_empty = member_email == ''
|
||||
member_email_is_empty = member_email == ""
|
||||
user_notification_disabled = not settings.user_notification_enabled
|
||||
if member_email_is_empty or user_notification_disabled or anon:
|
||||
self.widgets['user_notification'].mode = interfaces.HIDDEN_MODE
|
||||
self.widgets["user_notification"].mode = interfaces.HIDDEN_MODE
|
||||
|
||||
def updateActions(self):
|
||||
super(CommentForm, self).updateActions()
|
||||
self.actions['cancel'].addClass('btn btn-secondary')
|
||||
self.actions['cancel'].addClass('hide')
|
||||
self.actions['comment'].addClass('btn btn-primary')
|
||||
super().updateActions()
|
||||
self.actions["cancel"].addClass("btn btn-secondary")
|
||||
self.actions["cancel"].addClass("hide")
|
||||
self.actions["comment"].addClass("btn btn-primary")
|
||||
|
||||
def get_author(self, data):
|
||||
context = aq_inner(self.context)
|
||||
# some attributes are not always set
|
||||
author_name = u''
|
||||
author_name = ""
|
||||
|
||||
# Make sure author_name/ author_email is properly encoded
|
||||
if 'author_name' in data:
|
||||
author_name = safe_unicode(data['author_name'])
|
||||
if 'author_email' in data:
|
||||
author_email = safe_unicode(data['author_email'])
|
||||
if "author_name" in data:
|
||||
author_name = safe_text(data["author_name"])
|
||||
if "author_email" in data:
|
||||
author_email = safe_text(data["author_email"])
|
||||
|
||||
# Set comment author properties for anonymous users or members
|
||||
portal_membership = getToolByName(context, 'portal_membership')
|
||||
portal_membership = getToolByName(context, "portal_membership")
|
||||
anon = portal_membership.isAnonymousUser()
|
||||
if not anon and getSecurityManager().checkPermission(
|
||||
'Reply to item', context):
|
||||
if not anon and getSecurityManager().checkPermission("Reply to item", context):
|
||||
# Member
|
||||
member = portal_membership.getAuthenticatedMember()
|
||||
email = safe_unicode(member.getProperty('email'))
|
||||
fullname = member.getProperty('fullname')
|
||||
if not fullname or fullname == '':
|
||||
email = safe_text(member.getProperty("email"))
|
||||
fullname = member.getProperty("fullname")
|
||||
if not fullname or fullname == "":
|
||||
fullname = member.getUserName()
|
||||
fullname = safe_unicode(fullname)
|
||||
fullname = safe_text(fullname)
|
||||
author_name = fullname
|
||||
email = safe_unicode(email)
|
||||
email = safe_text(email)
|
||||
# XXX: according to IComment interface author_email must not be # noqa T000
|
||||
# set for logged in users, cite:
|
||||
# 'for anonymous comments only, set to None for logged in comments'
|
||||
@ -179,7 +178,7 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
|
||||
def create_comment(self, data):
|
||||
context = aq_inner(self.context)
|
||||
comment = createObject('plone.Comment')
|
||||
comment = createObject("plone.Comment")
|
||||
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
@ -200,42 +199,42 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
comment.author_name, comment.author_email = self.get_author(data)
|
||||
|
||||
# Set comment author properties for anonymous users or members
|
||||
portal_membership = getToolByName(context, 'portal_membership')
|
||||
portal_membership = getToolByName(context, "portal_membership")
|
||||
anon = portal_membership.isAnonymousUser()
|
||||
if anon and anonymous_comments:
|
||||
# Anonymous Users
|
||||
comment.user_notification = None
|
||||
elif not anon and getSecurityManager().checkPermission(
|
||||
'Reply to item', context):
|
||||
"Reply to item", context
|
||||
):
|
||||
# Member
|
||||
member = portal_membership.getAuthenticatedMember()
|
||||
memberid = member.getId()
|
||||
user = member.getUser()
|
||||
comment.changeOwnership(user, recursive=False)
|
||||
comment.manage_setLocalRoles(memberid, ['Owner'])
|
||||
comment.manage_setLocalRoles(memberid, ["Owner"])
|
||||
comment.creator = memberid
|
||||
comment.author_username = memberid
|
||||
|
||||
else: # pragma: no cover
|
||||
raise Unauthorized(
|
||||
u'Anonymous user tries to post a comment, but anonymous '
|
||||
u'commenting is disabled. Or user does not have the '
|
||||
u"'reply to item' permission.",
|
||||
"Anonymous user tries to post a comment, but anonymous "
|
||||
"commenting is disabled. Or user does not have the "
|
||||
"'reply to item' permission.",
|
||||
)
|
||||
|
||||
return comment
|
||||
|
||||
@button.buttonAndHandler(_(u'add_comment_button', default=u'Comment'),
|
||||
name='comment')
|
||||
@button.buttonAndHandler(_("add_comment_button", default="Comment"), name="comment")
|
||||
def handleComment(self, action):
|
||||
context = aq_inner(self.context)
|
||||
|
||||
# Check if conversation is enabled on this content object
|
||||
if not self.__parent__.restrictedTraverse(
|
||||
'@@conversation_view',
|
||||
"@@conversation_view",
|
||||
).enabled():
|
||||
raise Unauthorized(
|
||||
'Discussion is not enabled for this content object.',
|
||||
"Discussion is not enabled for this content object.",
|
||||
)
|
||||
|
||||
# Validation form
|
||||
@ -246,28 +245,26 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
# Validate Captcha
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
portal_membership = getToolByName(self.context, 'portal_membership')
|
||||
captcha_enabled = settings.captcha != 'disabled'
|
||||
portal_membership = getToolByName(self.context, "portal_membership")
|
||||
captcha_enabled = settings.captcha != "disabled"
|
||||
anonymous_comments = settings.anonymous_comments
|
||||
anon = portal_membership.isAnonymousUser()
|
||||
if captcha_enabled and anonymous_comments and anon:
|
||||
if 'captcha' not in data:
|
||||
data['captcha'] = u''
|
||||
captcha = CaptchaValidator(self.context,
|
||||
self.request,
|
||||
None,
|
||||
ICaptcha['captcha'],
|
||||
None)
|
||||
captcha.validate(data['captcha'])
|
||||
if "captcha" not in data:
|
||||
data["captcha"] = ""
|
||||
captcha = CaptchaValidator(
|
||||
self.context, self.request, None, ICaptcha["captcha"], None
|
||||
)
|
||||
captcha.validate(data["captcha"])
|
||||
|
||||
# Create comment
|
||||
comment = self.create_comment(data)
|
||||
|
||||
# Add comment to conversation
|
||||
conversation = IConversation(self.__parent__)
|
||||
if data['in_reply_to']:
|
||||
if data["in_reply_to"]:
|
||||
# Add a reply to an existing comment
|
||||
conversation_to_reply_to = conversation.get(data['in_reply_to'])
|
||||
conversation_to_reply_to = conversation.get(data["in_reply_to"])
|
||||
replies = IReplies(conversation_to_reply_to)
|
||||
comment_id = replies.addComment(comment)
|
||||
else:
|
||||
@ -279,25 +276,24 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
# shown to the user that his/her comment awaits moderation. If the user
|
||||
# has 'review comments' permission, he/she is redirected directly
|
||||
# to the comment.
|
||||
can_review = getSecurityManager().checkPermission('Review comments',
|
||||
context)
|
||||
workflowTool = getToolByName(context, 'portal_workflow')
|
||||
can_review = getSecurityManager().checkPermission("Review comments", context)
|
||||
workflowTool = getToolByName(context, "portal_workflow")
|
||||
comment_review_state = workflowTool.getInfoFor(
|
||||
comment,
|
||||
'review_state',
|
||||
"review_state",
|
||||
None,
|
||||
)
|
||||
if comment_review_state == 'pending' and not can_review:
|
||||
if comment_review_state == "pending" and not can_review:
|
||||
# Show info message when comment moderation is enabled
|
||||
IStatusMessage(self.context.REQUEST).addStatusMessage(
|
||||
_('Your comment awaits moderator approval.'),
|
||||
type='info')
|
||||
_("Your comment awaits moderator approval."), type="info"
|
||||
)
|
||||
self.request.response.redirect(self.action)
|
||||
else:
|
||||
# Redirect to comment (inside a content object page)
|
||||
self.request.response.redirect(self.action + '#' + str(comment_id))
|
||||
self.request.response.redirect(self.action + "#" + str(comment_id))
|
||||
|
||||
@button.buttonAndHandler(_(u'Cancel'))
|
||||
@button.buttonAndHandler(_("Cancel"))
|
||||
def handleCancel(self, action):
|
||||
# This method should never be called, it's only there to show
|
||||
# a cancel button that is handled by a jQuery method.
|
||||
@ -307,15 +303,15 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
|
||||
class CommentsViewlet(ViewletBase):
|
||||
|
||||
form = CommentForm
|
||||
index = ViewPageTemplateFile('comments.pt')
|
||||
index = ViewPageTemplateFile("comments.pt")
|
||||
|
||||
def update(self):
|
||||
super(CommentsViewlet, self).update()
|
||||
super().update()
|
||||
discussion_allowed = self.is_discussion_allowed()
|
||||
anonymous_allowed_or_can_reply = (
|
||||
self.is_anonymous() and
|
||||
self.anonymous_discussion_allowed() or
|
||||
self.can_reply()
|
||||
self.is_anonymous()
|
||||
and self.anonymous_discussion_allowed()
|
||||
or self.can_reply()
|
||||
)
|
||||
if discussion_allowed and anonymous_allowed_or_can_reply:
|
||||
z2.switch_on(self, request_layer=IFormLayer)
|
||||
@ -326,30 +322,29 @@ class CommentsViewlet(ViewletBase):
|
||||
# view methods
|
||||
|
||||
def can_reply(self):
|
||||
"""Returns true if current user has the 'Reply to item' permission.
|
||||
"""
|
||||
return getSecurityManager().checkPermission('Reply to item',
|
||||
aq_inner(self.context))
|
||||
"""Returns true if current user has the 'Reply to item' permission."""
|
||||
return getSecurityManager().checkPermission(
|
||||
"Reply to item", aq_inner(self.context)
|
||||
)
|
||||
|
||||
def can_manage(self):
|
||||
"""We keep this method for <= 1.0b9 backward compatibility. Since we do
|
||||
not want any API changes in beta releases.
|
||||
not want any API changes in beta releases.
|
||||
"""
|
||||
return self.can_review()
|
||||
|
||||
def can_review(self):
|
||||
"""Returns true if current user has the 'Review comments' permission.
|
||||
"""
|
||||
return getSecurityManager().checkPermission('Review comments',
|
||||
aq_inner(self.context))
|
||||
"""Returns true if current user has the 'Review comments' permission."""
|
||||
return getSecurityManager().checkPermission(
|
||||
"Review comments", aq_inner(self.context)
|
||||
)
|
||||
|
||||
def can_delete_own(self, comment):
|
||||
"""Returns true if the current user can delete the comment. Only
|
||||
comments without replies can be deleted.
|
||||
"""
|
||||
try:
|
||||
return comment.restrictedTraverse(
|
||||
'@@delete-own-comment').can_delete()
|
||||
return comment.restrictedTraverse("@@delete-own-comment").can_delete()
|
||||
except Unauthorized:
|
||||
return False
|
||||
|
||||
@ -358,8 +353,7 @@ class CommentsViewlet(ViewletBase):
|
||||
no replies. This is used to prepare hidden form buttons for JS.
|
||||
"""
|
||||
try:
|
||||
return comment.restrictedTraverse(
|
||||
'@@delete-own-comment').could_delete()
|
||||
return comment.restrictedTraverse("@@delete-own-comment").could_delete()
|
||||
except Unauthorized:
|
||||
return False
|
||||
|
||||
@ -367,58 +361,63 @@ class CommentsViewlet(ViewletBase):
|
||||
"""Returns true if current user has the 'Edit comments'
|
||||
permission.
|
||||
"""
|
||||
return getSecurityManager().checkPermission('Edit comments',
|
||||
aq_inner(reply))
|
||||
return getSecurityManager().checkPermission("Edit comments", aq_inner(reply))
|
||||
|
||||
def can_delete(self, reply):
|
||||
"""Returns true if current user has the 'Delete comments'
|
||||
permission.
|
||||
"""
|
||||
return getSecurityManager().checkPermission('Delete comments',
|
||||
aq_inner(reply))
|
||||
return getSecurityManager().checkPermission("Delete comments", aq_inner(reply))
|
||||
|
||||
def is_discussion_allowed(self):
|
||||
context = aq_inner(self.context)
|
||||
return context.restrictedTraverse('@@conversation_view').enabled()
|
||||
return context.restrictedTraverse("@@conversation_view").enabled()
|
||||
|
||||
def comment_transform_message(self):
|
||||
"""Returns the description that shows up above the comment text,
|
||||
dependent on the text_transform setting and the comment moderation
|
||||
workflow in the discussion control panel.
|
||||
dependent on the text_transform setting and the comment moderation
|
||||
workflow in the discussion control panel.
|
||||
"""
|
||||
context = aq_inner(self.context)
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
|
||||
# text transform setting
|
||||
if settings.text_transform == 'text/x-web-intelligent':
|
||||
message = translate(Message(COMMENT_DESCRIPTION_INTELLIGENT_TEXT),
|
||||
context=self.request)
|
||||
elif settings.text_transform == 'text/x-web-markdown':
|
||||
message = translate(Message(COMMENT_DESCRIPTION_MARKDOWN),
|
||||
context=self.request)
|
||||
if settings.text_transform == "text/x-web-intelligent":
|
||||
message = translate(
|
||||
Message(COMMENT_DESCRIPTION_INTELLIGENT_TEXT), context=self.request
|
||||
)
|
||||
elif settings.text_transform == "text/x-web-markdown":
|
||||
message = translate(
|
||||
Message(COMMENT_DESCRIPTION_MARKDOWN), context=self.request
|
||||
)
|
||||
else:
|
||||
message = translate(Message(COMMENT_DESCRIPTION_PLAIN_TEXT),
|
||||
context=self.request)
|
||||
message = translate(
|
||||
Message(COMMENT_DESCRIPTION_PLAIN_TEXT), context=self.request
|
||||
)
|
||||
|
||||
# comment workflow
|
||||
wftool = getToolByName(context, 'portal_workflow', None)
|
||||
workflow_chain = wftool.getChainForPortalType('Discussion Item')
|
||||
wftool = getToolByName(context, "portal_workflow", None)
|
||||
workflow_chain = wftool.getChainForPortalType("Discussion Item")
|
||||
if workflow_chain:
|
||||
comment_workflow = workflow_chain[0]
|
||||
comment_workflow = wftool[comment_workflow]
|
||||
# check if the current workflow implements a pending state. If this
|
||||
# is true comments are moderated
|
||||
if 'pending' in comment_workflow.states:
|
||||
message = message + ' ' + \
|
||||
translate(Message(COMMENT_DESCRIPTION_MODERATION_ENABLED),
|
||||
context=self.request)
|
||||
if "pending" in comment_workflow.states:
|
||||
message = (
|
||||
message
|
||||
+ " "
|
||||
+ translate(
|
||||
Message(COMMENT_DESCRIPTION_MODERATION_ENABLED),
|
||||
context=self.request,
|
||||
)
|
||||
)
|
||||
|
||||
return message
|
||||
|
||||
def has_replies(self, workflow_actions=False):
|
||||
"""Returns true if there are replies.
|
||||
"""
|
||||
"""Returns true if there are replies."""
|
||||
if self.get_replies(workflow_actions) is not None:
|
||||
try:
|
||||
next(self.get_replies(workflow_actions))
|
||||
@ -442,31 +441,32 @@ class CommentsViewlet(ViewletBase):
|
||||
if conversation is None:
|
||||
return iter([])
|
||||
|
||||
wf = getToolByName(context, 'portal_workflow')
|
||||
wf = getToolByName(context, "portal_workflow")
|
||||
# workflow_actions is only true when user
|
||||
# has 'Manage portal' permission
|
||||
|
||||
def replies_with_workflow_actions():
|
||||
# Generator that returns replies dict with workflow actions
|
||||
for r in conversation.getThreads():
|
||||
comment_obj = r['comment']
|
||||
comment_obj = r["comment"]
|
||||
# list all possible workflow actions
|
||||
actions = [
|
||||
a for a in wf.listActionInfos(object=comment_obj)
|
||||
if a['category'] == 'workflow' and a['allowed']
|
||||
a
|
||||
for a in wf.listActionInfos(object=comment_obj)
|
||||
if a["category"] == "workflow" and a["allowed"]
|
||||
]
|
||||
r = r.copy()
|
||||
r['actions'] = actions
|
||||
r["actions"] = actions
|
||||
yield r
|
||||
|
||||
def published_replies():
|
||||
# Generator that returns replies dict with workflow status.
|
||||
for r in conversation.getThreads():
|
||||
comment_obj = r['comment']
|
||||
workflow_status = wf.getInfoFor(comment_obj, 'review_state')
|
||||
if workflow_status == 'published':
|
||||
comment_obj = r["comment"]
|
||||
workflow_status = wf.getInfoFor(comment_obj, "review_state")
|
||||
if workflow_status == "published":
|
||||
r = r.copy()
|
||||
r['workflow_status'] = workflow_status
|
||||
r["workflow_status"] = workflow_status
|
||||
yield r
|
||||
|
||||
# Return all direct replies
|
||||
@ -480,20 +480,16 @@ class CommentsViewlet(ViewletBase):
|
||||
if username is None:
|
||||
return None
|
||||
else:
|
||||
return '{0}/author/{1}'.format(self.context.portal_url(), username)
|
||||
return f"{self.context.portal_url()}/author/{username}"
|
||||
|
||||
def get_commenter_portrait(self, username=None):
|
||||
|
||||
if username is None:
|
||||
# return the default user image if no username is given
|
||||
return 'defaultUser.png'
|
||||
return "defaultUser.png"
|
||||
else:
|
||||
portal_membership = getToolByName(self.context,
|
||||
'portal_membership',
|
||||
None)
|
||||
return portal_membership\
|
||||
.getPersonalPortrait(username)\
|
||||
.absolute_url()
|
||||
portal_membership = getToolByName(self.context, "portal_membership", None)
|
||||
return portal_membership.getPersonalPortrait(username).absolute_url()
|
||||
|
||||
def anonymous_discussion_allowed(self):
|
||||
# Check if anonymous comments are allowed in the registry
|
||||
@ -520,20 +516,18 @@ class CommentsViewlet(ViewletBase):
|
||||
return settings.show_commenter_image
|
||||
|
||||
def is_anonymous(self):
|
||||
portal_membership = getToolByName(self.context,
|
||||
'portal_membership',
|
||||
None)
|
||||
portal_membership = getToolByName(self.context, "portal_membership", None)
|
||||
return portal_membership.isAnonymousUser()
|
||||
|
||||
def login_action(self):
|
||||
return '{0}/login_form?came_from={1}'.format(
|
||||
return "{}/login_form?came_from={}".format(
|
||||
self.navigation_root_url,
|
||||
quote(self.request.get('URL', '')),
|
||||
quote(self.request.get("URL", "")),
|
||||
)
|
||||
|
||||
def format_time(self, time):
|
||||
# We have to transform Python datetime into Zope DateTime
|
||||
# before we can call toLocalizedTime.
|
||||
util = getToolByName(self.context, 'translation_service')
|
||||
util = getToolByName(self.context, "translation_service")
|
||||
zope_time = DateTime(time.isoformat())
|
||||
return util.toLocalizedTime(zope_time, long_format=True)
|
||||
|
@ -48,7 +48,7 @@
|
||||
|
||||
<!-- Moderate comments enabled view -->
|
||||
<browser:page
|
||||
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
|
||||
for="plone.base.interfaces.IPloneSiteRoot"
|
||||
name="moderate-comments-enabled"
|
||||
layer="..interfaces.IDiscussionLayer"
|
||||
class=".moderation.ModerateCommentsEnabled"
|
||||
@ -141,14 +141,14 @@
|
||||
<!-- Control panel -->
|
||||
<browser:page
|
||||
name="discussion-controlpanel"
|
||||
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
|
||||
for="plone.base.interfaces.IPloneSiteRoot"
|
||||
class=".controlpanel.DiscussionSettingsControlPanel"
|
||||
permission="cmf.ManagePortal"
|
||||
/>
|
||||
<!-- Deprecated controlpanel url -->
|
||||
<browser:page
|
||||
name="discussion-settings"
|
||||
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
|
||||
for="plone.base.interfaces.IPloneSiteRoot"
|
||||
class=".controlpanel.DiscussionSettingsControlPanel"
|
||||
permission="cmf.ManagePortal"
|
||||
/>
|
||||
|
@ -1,12 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.interfaces import _
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.app.discussion.upgrades import update_registry
|
||||
from ..interfaces import _
|
||||
from ..interfaces import IDiscussionSettings
|
||||
from ..upgrades import update_registry
|
||||
from plone.app.registry.browser import controlpanel
|
||||
from plone.base.interfaces.controlpanel import IConfigurationChangedEvent
|
||||
from plone.base.interfaces.controlpanel import IMailSchema
|
||||
from plone.registry.interfaces import IRecordModifiedEvent
|
||||
from plone.registry.interfaces import IRegistry
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
from Products.CMFPlone.interfaces.controlpanel import IMailSchema
|
||||
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile
|
||||
from Products.statusmessages.interfaces import IStatusMessage
|
||||
from z3c.form import button
|
||||
@ -16,87 +16,76 @@ from zope.component import getUtility
|
||||
from zope.component import queryUtility
|
||||
from zope.component.hooks import getSite
|
||||
|
||||
# try/except was added because Configuration Changed Event was moved inside the
|
||||
# controlpanel file in the PR #2495 on Products.CMFPlone
|
||||
try:
|
||||
from Products.CMFPlone.interfaces.controlpanel import IConfigurationChangedEvent # noqa: E501
|
||||
except ImportError:
|
||||
from Products.CMFPlone.interfaces import IConfigurationChangedEvent
|
||||
|
||||
|
||||
class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
|
||||
"""Discussion settings form.
|
||||
"""
|
||||
"""Discussion settings form."""
|
||||
|
||||
schema = IDiscussionSettings
|
||||
id = 'DiscussionSettingsEditForm'
|
||||
label = _(u'Discussion settings')
|
||||
id = "DiscussionSettingsEditForm"
|
||||
label = _("Discussion settings")
|
||||
description = _(
|
||||
u'help_discussion_settings_editform',
|
||||
default=u'Some discussion related settings are not '
|
||||
u'located in the Discussion Control Panel.\n'
|
||||
u'To enable comments for a specific content type, '
|
||||
u'go to the Types Control Panel of this type and '
|
||||
u'choose "Allow comments".\n'
|
||||
u'To enable the moderation workflow for comments, '
|
||||
u'go to the Types Control Panel, choose '
|
||||
u'"Comment" and set workflow to '
|
||||
u'"Comment Review Workflow".',
|
||||
"help_discussion_settings_editform",
|
||||
default="Some discussion related settings are not "
|
||||
"located in the Discussion Control Panel.\n"
|
||||
"To enable comments for a specific content type, "
|
||||
"go to the Types Control Panel of this type and "
|
||||
'choose "Allow comments".\n'
|
||||
"To enable the moderation workflow for comments, "
|
||||
"go to the Types Control Panel, choose "
|
||||
'"Comment" and set workflow to '
|
||||
'"Comment Review Workflow".',
|
||||
)
|
||||
|
||||
def updateFields(self):
|
||||
super(DiscussionSettingsEditForm, self).updateFields()
|
||||
self.fields['globally_enabled'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['moderation_enabled'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['edit_comment_enabled'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['delete_own_comment_enabled'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['anonymous_comments'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['show_commenter_image'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['moderator_notification_enabled'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
self.fields['user_notification_enabled'].widgetFactory = \
|
||||
SingleCheckBoxFieldWidget
|
||||
super().updateFields()
|
||||
self.fields["globally_enabled"].widgetFactory = SingleCheckBoxFieldWidget
|
||||
self.fields["moderation_enabled"].widgetFactory = SingleCheckBoxFieldWidget
|
||||
self.fields["edit_comment_enabled"].widgetFactory = SingleCheckBoxFieldWidget
|
||||
self.fields[
|
||||
"delete_own_comment_enabled"
|
||||
].widgetFactory = SingleCheckBoxFieldWidget
|
||||
self.fields["anonymous_comments"].widgetFactory = SingleCheckBoxFieldWidget
|
||||
self.fields["show_commenter_image"].widgetFactory = SingleCheckBoxFieldWidget
|
||||
self.fields[
|
||||
"moderator_notification_enabled"
|
||||
].widgetFactory = SingleCheckBoxFieldWidget
|
||||
self.fields[
|
||||
"user_notification_enabled"
|
||||
].widgetFactory = SingleCheckBoxFieldWidget
|
||||
|
||||
def updateWidgets(self):
|
||||
try:
|
||||
super(DiscussionSettingsEditForm, self).updateWidgets()
|
||||
super().updateWidgets()
|
||||
except KeyError:
|
||||
# upgrade profile not visible in prefs_install_products_form
|
||||
# provide auto-upgrade
|
||||
update_registry(self.context)
|
||||
super(DiscussionSettingsEditForm, self).updateWidgets()
|
||||
self.widgets['globally_enabled'].label = _(u'Enable Comments')
|
||||
self.widgets['anonymous_comments'].label = _(u'Anonymous Comments')
|
||||
self.widgets['show_commenter_image'].label = _(u'Commenter Image')
|
||||
self.widgets['moderator_notification_enabled'].label = _(
|
||||
u'Moderator Email Notification',
|
||||
super().updateWidgets()
|
||||
self.widgets["globally_enabled"].label = _("Enable Comments")
|
||||
self.widgets["anonymous_comments"].label = _("Anonymous Comments")
|
||||
self.widgets["show_commenter_image"].label = _("Commenter Image")
|
||||
self.widgets["moderator_notification_enabled"].label = _(
|
||||
"Moderator Email Notification",
|
||||
)
|
||||
self.widgets['user_notification_enabled'].label = _(
|
||||
u'User Email Notification',
|
||||
self.widgets["user_notification_enabled"].label = _(
|
||||
"User Email Notification",
|
||||
)
|
||||
|
||||
@button.buttonAndHandler(_('Save'), name=None)
|
||||
@button.buttonAndHandler(_("Save"), name=None)
|
||||
def handleSave(self, action):
|
||||
data, errors = self.extractData()
|
||||
if errors:
|
||||
self.status = self.formErrorsMessage
|
||||
return
|
||||
self.applyChanges(data)
|
||||
IStatusMessage(self.request).addStatusMessage(_(u'Changes saved'),
|
||||
'info')
|
||||
self.context.REQUEST.RESPONSE.redirect('@@discussion-controlpanel')
|
||||
IStatusMessage(self.request).addStatusMessage(_("Changes saved"), "info")
|
||||
self.context.REQUEST.RESPONSE.redirect("@@discussion-controlpanel")
|
||||
|
||||
@button.buttonAndHandler(_('Cancel'), name='cancel')
|
||||
@button.buttonAndHandler(_("Cancel"), name="cancel")
|
||||
def handleCancel(self, action):
|
||||
IStatusMessage(self.request).addStatusMessage(_(u'Edit cancelled'),
|
||||
'info')
|
||||
IStatusMessage(self.request).addStatusMessage(_("Edit cancelled"), "info")
|
||||
self.request.response.redirect(
|
||||
'{0}/{1}'.format(
|
||||
"{}/{}".format(
|
||||
self.context.absolute_url(),
|
||||
self.control_panel_view,
|
||||
),
|
||||
@ -104,15 +93,15 @@ class DiscussionSettingsEditForm(controlpanel.RegistryEditForm):
|
||||
|
||||
|
||||
class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
|
||||
"""Discussion settings control panel.
|
||||
"""
|
||||
"""Discussion settings control panel."""
|
||||
|
||||
form = DiscussionSettingsEditForm
|
||||
index = ViewPageTemplateFile('controlpanel.pt')
|
||||
index = ViewPageTemplateFile("controlpanel.pt")
|
||||
|
||||
def __call__(self):
|
||||
self.mailhost_warning()
|
||||
self.custom_comment_workflow_warning()
|
||||
return super(DiscussionSettingsControlPanel, self).__call__()
|
||||
return super().__call__()
|
||||
|
||||
@property
|
||||
def site_url(self):
|
||||
@ -123,43 +112,44 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
|
||||
|
||||
def settings(self):
|
||||
"""Compose a string that contains all registry settings that are
|
||||
needed for the discussion control panel.
|
||||
needed for the discussion control panel.
|
||||
"""
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
wftool = getToolByName(self.context, 'portal_workflow', None)
|
||||
workflow_chain = wftool.getChainForPortalType('Discussion Item')
|
||||
wftool = getToolByName(self.context, "portal_workflow", None)
|
||||
workflow_chain = wftool.getChainForPortalType("Discussion Item")
|
||||
output = []
|
||||
|
||||
# Globally enabled
|
||||
if settings.globally_enabled:
|
||||
output.append('globally_enabled')
|
||||
output.append("globally_enabled")
|
||||
|
||||
# Comment moderation
|
||||
one_state_worklow_disabled = \
|
||||
'comment_one_state_workflow' not in workflow_chain
|
||||
comment_review_workflow_disabled = \
|
||||
'comment_review_workflow' not in workflow_chain
|
||||
one_state_worklow_disabled = "comment_one_state_workflow" not in workflow_chain
|
||||
comment_review_workflow_disabled = (
|
||||
"comment_review_workflow" not in workflow_chain
|
||||
)
|
||||
if one_state_worklow_disabled and comment_review_workflow_disabled:
|
||||
output.append('moderation_custom')
|
||||
output.append("moderation_custom")
|
||||
elif settings.moderation_enabled:
|
||||
output.append('moderation_enabled')
|
||||
output.append("moderation_enabled")
|
||||
|
||||
if settings.edit_comment_enabled:
|
||||
output.append('edit_comment_enabled')
|
||||
output.append("edit_comment_enabled")
|
||||
|
||||
if settings.delete_own_comment_enabled:
|
||||
output.append('delete_own_comment_enabled')
|
||||
output.append("delete_own_comment_enabled")
|
||||
|
||||
# Anonymous comments
|
||||
if settings.anonymous_comments:
|
||||
output.append('anonymous_comments')
|
||||
output.append("anonymous_comments")
|
||||
|
||||
# Invalid mail setting
|
||||
ctrlOverview = getMultiAdapter((self.context, self.request),
|
||||
name='overview-controlpanel')
|
||||
ctrlOverview = getMultiAdapter(
|
||||
(self.context, self.request), name="overview-controlpanel"
|
||||
)
|
||||
if ctrlOverview.mailhost_warning():
|
||||
output.append('invalid_mail_setup')
|
||||
output.append("invalid_mail_setup")
|
||||
|
||||
# Workflow
|
||||
if workflow_chain:
|
||||
@ -167,69 +157,71 @@ class DiscussionSettingsControlPanel(controlpanel.ControlPanelFormWrapper):
|
||||
output.append(discussion_workflow)
|
||||
|
||||
# Merge all settings into one string
|
||||
return ' '.join(output)
|
||||
return " ".join(output)
|
||||
|
||||
def mailhost_warning(self):
|
||||
"""Returns true if mailhost is not configured properly.
|
||||
"""
|
||||
"""Returns true if mailhost is not configured properly."""
|
||||
# Copied from Products.CMFPlone/controlpanel/browser/overview.py
|
||||
registry = getUtility(IRegistry)
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
|
||||
mailhost = mail_settings.smtp_host
|
||||
email = mail_settings.email_from_address
|
||||
if mailhost and email:
|
||||
pass
|
||||
else:
|
||||
message = _(u'discussion_text_no_mailhost_configured',
|
||||
default=u'You have not configured a mail host or a site \'From\' address, various features including contact forms, email notification and password reset will not work. Go to the E-Mail Settings to fix this.') # noqa: E501
|
||||
IStatusMessage(self.request).addStatusMessage(message, 'warning')
|
||||
message = _(
|
||||
"discussion_text_no_mailhost_configured",
|
||||
default="You have not configured a mail host or a site 'From' address, various features including contact forms, email notification and password reset will not work. Go to the E-Mail Settings to fix this.",
|
||||
) # noqa: E501
|
||||
IStatusMessage(self.request).addStatusMessage(message, "warning")
|
||||
|
||||
def custom_comment_workflow_warning(self):
|
||||
"""Return True if a custom comment workflow is enabled."""
|
||||
wftool = getToolByName(self.context, 'portal_workflow', None)
|
||||
workflow_chain = wftool.getChainForPortalType('Discussion Item')
|
||||
one_state_workflow_enabled = \
|
||||
'comment_one_state_workflow' in workflow_chain
|
||||
comment_review_workflow_enabled = \
|
||||
'comment_review_workflow' in workflow_chain
|
||||
wftool = getToolByName(self.context, "portal_workflow", None)
|
||||
workflow_chain = wftool.getChainForPortalType("Discussion Item")
|
||||
one_state_workflow_enabled = "comment_one_state_workflow" in workflow_chain
|
||||
comment_review_workflow_enabled = "comment_review_workflow" in workflow_chain
|
||||
if one_state_workflow_enabled or comment_review_workflow_enabled:
|
||||
pass
|
||||
else:
|
||||
message = _(u'discussion_text_custom_comment_workflow',
|
||||
default=u'You have configured a custom workflow for the \'Discussion Item\' content type. You can enable/disable the comment moderation in this control panel only if you use one of the default \'Discussion Item\' workflows. Go to the Types control panel to choose a workflow for the \'Discussion Item\' type.') # noqa: E501
|
||||
IStatusMessage(self.request).addStatusMessage(message, 'warning')
|
||||
message = _(
|
||||
"discussion_text_custom_comment_workflow",
|
||||
default="You have configured a custom workflow for the 'Discussion Item' content type. You can enable/disable the comment moderation in this control panel only if you use one of the default 'Discussion Item' workflows. Go to the Types control panel to choose a workflow for the 'Discussion Item' type.",
|
||||
) # noqa: E501
|
||||
IStatusMessage(self.request).addStatusMessage(message, "warning")
|
||||
|
||||
|
||||
def notify_configuration_changed(event):
|
||||
"""Event subscriber that is called every time the configuration changed.
|
||||
"""
|
||||
"""Event subscriber that is called every time the configuration changed."""
|
||||
portal = getSite()
|
||||
wftool = getToolByName(portal, 'portal_workflow', None)
|
||||
wftool = getToolByName(portal, "portal_workflow", None)
|
||||
|
||||
if IRecordModifiedEvent.providedBy(event):
|
||||
# Discussion control panel setting changed
|
||||
if event.record.fieldName == 'moderation_enabled':
|
||||
if event.record.fieldName == "moderation_enabled":
|
||||
# Moderation enabled has changed
|
||||
if event.record.value is True:
|
||||
# Enable moderation workflow
|
||||
wftool.setChainForPortalTypes(('Discussion Item',),
|
||||
'comment_review_workflow')
|
||||
wftool.setChainForPortalTypes(
|
||||
("Discussion Item",), "comment_review_workflow"
|
||||
)
|
||||
else:
|
||||
# Disable moderation workflow
|
||||
wftool.setChainForPortalTypes(('Discussion Item',),
|
||||
'comment_one_state_workflow')
|
||||
wftool.setChainForPortalTypes(
|
||||
("Discussion Item",), "comment_one_state_workflow"
|
||||
)
|
||||
|
||||
if IConfigurationChangedEvent.providedBy(event):
|
||||
# Types control panel setting changed
|
||||
if 'workflow' in event.data:
|
||||
if "workflow" in event.data:
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
workflow_chain = wftool.getChainForPortalType('Discussion Item')
|
||||
workflow_chain = wftool.getChainForPortalType("Discussion Item")
|
||||
if workflow_chain:
|
||||
workflow = workflow_chain[0]
|
||||
if workflow == 'comment_one_state_workflow':
|
||||
if workflow == "comment_one_state_workflow":
|
||||
settings.moderation_enabled = False
|
||||
elif workflow == 'comment_review_workflow':
|
||||
elif workflow == "comment_review_workflow":
|
||||
settings.moderation_enabled = True
|
||||
else:
|
||||
# Custom workflow
|
||||
|
@ -1,19 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ..interfaces import IDiscussionSettings
|
||||
from Acquisition import aq_base
|
||||
from Acquisition import aq_chain
|
||||
from Acquisition import aq_inner
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.base.interfaces import INonStructuralFolder
|
||||
from plone.base.interfaces import IPloneSiteRoot
|
||||
from plone.base.utils import safe_hasattr
|
||||
from plone.registry.interfaces import IRegistry
|
||||
from Products.CMFCore.interfaces import IFolderish
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
from Products.CMFPlone.interfaces import INonStructuralFolder
|
||||
from Products.CMFPlone.interfaces import IPloneSiteRoot
|
||||
from Products.CMFPlone.utils import safe_hasattr
|
||||
from zope.component import queryUtility
|
||||
|
||||
|
||||
try:
|
||||
from plone.dexterity.interfaces import IDexterityContent
|
||||
|
||||
DEXTERITY_INSTALLED = True
|
||||
except ImportError:
|
||||
DEXTERITY_INSTALLED = False
|
||||
@ -26,15 +26,14 @@ def traverse_parents(context):
|
||||
if not IPloneSiteRoot.providedBy(obj):
|
||||
obj_is_folderish = IFolderish.providedBy(obj)
|
||||
obj_is_stuctural = not INonStructuralFolder.providedBy(obj)
|
||||
if (obj_is_folderish and obj_is_stuctural):
|
||||
flag = getattr(obj, 'allow_discussion', None)
|
||||
if obj_is_folderish and obj_is_stuctural:
|
||||
flag = getattr(obj, "allow_discussion", None)
|
||||
if flag is not None:
|
||||
return flag
|
||||
return None
|
||||
|
||||
|
||||
class ConversationView(object):
|
||||
|
||||
class ConversationView:
|
||||
def enabled(self):
|
||||
if DEXTERITY_INSTALLED and IDexterityContent.providedBy(self.context):
|
||||
return self._enabled_for_dexterity_types()
|
||||
@ -42,7 +41,7 @@ class ConversationView(object):
|
||||
return self._enabled_for_archetypes()
|
||||
|
||||
def _enabled_for_archetypes(self):
|
||||
""" Returns True if discussion is enabled for this conversation.
|
||||
"""Returns True if discussion is enabled for this conversation.
|
||||
|
||||
This method checks five different settings in order to figure out if
|
||||
discussion is enabled on a specific content object:
|
||||
@ -82,7 +81,7 @@ class ConversationView(object):
|
||||
return False
|
||||
|
||||
# If discussion is disabled for the object, bail out
|
||||
obj_flag = getattr(aq_base(context), 'allow_discussion', None)
|
||||
obj_flag = getattr(aq_base(context), "allow_discussion", None)
|
||||
if obj_flag is False:
|
||||
return False
|
||||
|
||||
@ -91,16 +90,16 @@ class ConversationView(object):
|
||||
folder_allow_discussion = traverse_parents(context)
|
||||
|
||||
if folder_allow_discussion:
|
||||
if not getattr(self, 'allow_discussion', None):
|
||||
if not getattr(self, "allow_discussion", None):
|
||||
return True
|
||||
else:
|
||||
if obj_flag:
|
||||
return True
|
||||
|
||||
# Check if discussion is allowed on the content type
|
||||
portal_types = getToolByName(self, 'portal_types')
|
||||
portal_types = getToolByName(self, "portal_types")
|
||||
document_fti = getattr(portal_types, context.portal_type)
|
||||
if not document_fti.getProperty('allow_discussion'):
|
||||
if not document_fti.getProperty("allow_discussion"):
|
||||
# If discussion is not allowed on the content type,
|
||||
# check if 'allow discussion' is overridden on the content object.
|
||||
if not obj_flag:
|
||||
@ -109,7 +108,7 @@ class ConversationView(object):
|
||||
return True
|
||||
|
||||
def _enabled_for_dexterity_types(self):
|
||||
""" Returns True if discussion is enabled for this conversation.
|
||||
"""Returns True if discussion is enabled for this conversation.
|
||||
|
||||
This method checks five different settings in order to figure out if
|
||||
discussion is enable on a specific content object:
|
||||
@ -134,11 +133,11 @@ class ConversationView(object):
|
||||
return False
|
||||
|
||||
# Check if discussion is allowed on the content object
|
||||
if safe_hasattr(context, 'allow_discussion'):
|
||||
if safe_hasattr(context, "allow_discussion"):
|
||||
if context.allow_discussion is not None:
|
||||
return context.allow_discussion
|
||||
|
||||
# Check if discussion is allowed on the content type
|
||||
portal_types = getToolByName(self, 'portal_types')
|
||||
portal_types = getToolByName(self, "portal_types")
|
||||
document_fti = getattr(portal_types, context.portal_type)
|
||||
return document_fti.getProperty('allow_discussion')
|
||||
return document_fti.getProperty("allow_discussion")
|
||||
|
@ -1,11 +1,10 @@
|
||||
# coding: utf-8
|
||||
from AccessControl import getSecurityManager
|
||||
from AccessControl import Unauthorized
|
||||
from Acquisition import aq_inner
|
||||
from Acquisition import aq_parent
|
||||
from plone.app.discussion.events import CommentDeletedEvent
|
||||
from plone.app.discussion.events import CommentPublishedEvent
|
||||
from plone.app.discussion.events import CommentTransitionEvent
|
||||
from plone.app.discussion.events import CommentDeletedEvent
|
||||
from plone.app.discussion.interfaces import _
|
||||
from plone.app.discussion.interfaces import IComment
|
||||
from plone.app.discussion.interfaces import IReplies
|
||||
@ -18,21 +17,20 @@ from zope.event import notify
|
||||
|
||||
# Translations for generated values in buttons
|
||||
# States
|
||||
_('comment_pending', default='pending')
|
||||
_("comment_pending", default="pending")
|
||||
# _('comment_approved', default='published')
|
||||
_('comment_published', default='published')
|
||||
_('comment_rejected', default='rejected')
|
||||
_('comment_spam', default='marked as spam')
|
||||
_("comment_published", default="published")
|
||||
_("comment_rejected", default="rejected")
|
||||
_("comment_spam", default="marked as spam")
|
||||
# Transitions
|
||||
_('Recall')
|
||||
_('Approve')
|
||||
_('Reject')
|
||||
_('Spam')
|
||||
_("Recall")
|
||||
_("Approve")
|
||||
_("Reject")
|
||||
_("Spam")
|
||||
PMF = _
|
||||
|
||||
|
||||
class TranslationHelper(BrowserView):
|
||||
|
||||
def translate(self, text=""):
|
||||
return _(text)
|
||||
|
||||
@ -44,22 +42,21 @@ class TranslationHelper(BrowserView):
|
||||
class View(BrowserView):
|
||||
"""Show comment moderation view."""
|
||||
|
||||
template = ViewPageTemplateFile('moderation.pt')
|
||||
template = ViewPageTemplateFile("moderation.pt")
|
||||
try:
|
||||
template.id = '@@moderate-comments'
|
||||
template.id = "@@moderate-comments"
|
||||
except AttributeError:
|
||||
# id is not writeable in Zope 2.12
|
||||
pass
|
||||
|
||||
def __init__(self, context, request):
|
||||
super(View, self).__init__(context, request)
|
||||
self.workflowTool = getToolByName(self.context, 'portal_workflow')
|
||||
super().__init__(context, request)
|
||||
self.workflowTool = getToolByName(self.context, "portal_workflow")
|
||||
self.transitions = []
|
||||
|
||||
def __call__(self):
|
||||
self.request.set('disable_border', True)
|
||||
self.request.set('review_state',
|
||||
self.request.get('review_state', 'pending'))
|
||||
self.request.set("disable_border", True)
|
||||
self.request.set("review_state", self.request.get("review_state", "pending"))
|
||||
return self.template()
|
||||
|
||||
def comments(self):
|
||||
@ -67,15 +64,19 @@ class View(BrowserView):
|
||||
|
||||
review_state is string or list of strings.
|
||||
"""
|
||||
catalog = getToolByName(self.context, 'portal_catalog')
|
||||
if self.request.review_state == 'all':
|
||||
return catalog(object_provides=IComment.__identifier__,
|
||||
sort_on='created',
|
||||
sort_order='reverse')
|
||||
return catalog(object_provides=IComment.__identifier__,
|
||||
review_state=self.request.review_state,
|
||||
sort_on='created',
|
||||
sort_order='reverse')
|
||||
catalog = getToolByName(self.context, "portal_catalog")
|
||||
if self.request.review_state == "all":
|
||||
return catalog(
|
||||
object_provides=IComment.__identifier__,
|
||||
sort_on="created",
|
||||
sort_order="reverse",
|
||||
)
|
||||
return catalog(
|
||||
object_provides=IComment.__identifier__,
|
||||
review_state=self.request.review_state,
|
||||
sort_on="created",
|
||||
sort_order="reverse",
|
||||
)
|
||||
|
||||
def moderation_enabled(self):
|
||||
"""Return true if a review workflow is enabled on 'Discussion Item'
|
||||
@ -84,11 +85,10 @@ class View(BrowserView):
|
||||
A 'review workflow' is characterized by implementing a 'pending'
|
||||
workflow state.
|
||||
"""
|
||||
workflows = self.workflowTool.getChainForPortalType(
|
||||
'Discussion Item')
|
||||
workflows = self.workflowTool.getChainForPortalType("Discussion Item")
|
||||
if workflows:
|
||||
comment_workflow = self.workflowTool[workflows[0]]
|
||||
if 'pending' in comment_workflow.states:
|
||||
if "pending" in comment_workflow.states:
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -100,11 +100,10 @@ class View(BrowserView):
|
||||
A 'review multipe state workflow' is characterized by implementing
|
||||
a 'rejected' workflow state and a 'spam' workflow state.
|
||||
"""
|
||||
workflows = self.workflowTool.getChainForPortalType(
|
||||
'Discussion Item')
|
||||
workflows = self.workflowTool.getChainForPortalType("Discussion Item")
|
||||
if workflows:
|
||||
comment_workflow = self.workflowTool[workflows[0]]
|
||||
if 'spam' in comment_workflow.states:
|
||||
if "spam" in comment_workflow.states:
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -125,27 +124,26 @@ class View(BrowserView):
|
||||
"""
|
||||
if obj:
|
||||
transitions = [
|
||||
a for a in self.workflowTool.listActionInfos(object=obj)
|
||||
if a['category'] == 'workflow' and a['allowed']
|
||||
]
|
||||
a
|
||||
for a in self.workflowTool.listActionInfos(object=obj)
|
||||
if a["category"] == "workflow" and a["allowed"]
|
||||
]
|
||||
return transitions
|
||||
|
||||
|
||||
class ModerateCommentsEnabled(BrowserView):
|
||||
|
||||
def __call__(self):
|
||||
"""Returns true if a 'review workflow' is enabled on 'Discussion Item'
|
||||
content type. A 'review workflow' is characterized by implementing
|
||||
a 'pending' workflow state.
|
||||
content type. A 'review workflow' is characterized by implementing
|
||||
a 'pending' workflow state.
|
||||
"""
|
||||
context = aq_inner(self.context)
|
||||
workflowTool = getToolByName(context, 'portal_workflow', None)
|
||||
comment_workflow = workflowTool.getChainForPortalType(
|
||||
'Discussion Item')
|
||||
workflowTool = getToolByName(context, "portal_workflow", None)
|
||||
comment_workflow = workflowTool.getChainForPortalType("Discussion Item")
|
||||
if comment_workflow:
|
||||
comment_workflow = comment_workflow[0]
|
||||
comment_workflow = workflowTool[comment_workflow]
|
||||
if 'pending' in comment_workflow.states:
|
||||
if "pending" in comment_workflow.states:
|
||||
return True
|
||||
|
||||
return False
|
||||
@ -184,13 +182,15 @@ class DeleteComment(BrowserView):
|
||||
content_object.reindexObject()
|
||||
notify(CommentDeletedEvent(self.context, comment))
|
||||
IStatusMessage(self.context.REQUEST).addStatusMessage(
|
||||
_('Comment deleted.'),
|
||||
type='info')
|
||||
_("Comment deleted."), type="info"
|
||||
)
|
||||
came_from = self.context.REQUEST.HTTP_REFERER
|
||||
# if the referrer already has a came_from in it, don't redirect back
|
||||
if (len(came_from) == 0 or 'came_from=' in came_from or
|
||||
not getToolByName(
|
||||
content_object, 'portal_url').isURLInPortal(came_from)):
|
||||
if (
|
||||
len(came_from) == 0
|
||||
or "came_from=" in came_from
|
||||
or not getToolByName(content_object, "portal_url").isURLInPortal(came_from)
|
||||
):
|
||||
came_from = content_object.absolute_url()
|
||||
return self.context.REQUEST.RESPONSE.redirect(came_from)
|
||||
|
||||
@ -198,8 +198,7 @@ class DeleteComment(BrowserView):
|
||||
"""Returns true if current user has the 'Delete comments'
|
||||
permission.
|
||||
"""
|
||||
return getSecurityManager().checkPermission('Delete comments',
|
||||
aq_inner(reply))
|
||||
return getSecurityManager().checkPermission("Delete comments", aq_inner(reply))
|
||||
|
||||
|
||||
class DeleteOwnComment(DeleteComment):
|
||||
@ -213,26 +212,23 @@ class DeleteOwnComment(DeleteComment):
|
||||
"""
|
||||
|
||||
def could_delete(self, comment=None):
|
||||
"""Returns true if the comment could be deleted if it had no replies.
|
||||
"""
|
||||
"""Returns true if the comment could be deleted if it had no replies."""
|
||||
sm = getSecurityManager()
|
||||
comment = comment or aq_inner(self.context)
|
||||
userid = sm.getUser().getId()
|
||||
return (
|
||||
sm.checkPermission('Delete own comments', comment) and
|
||||
'Owner' in comment.get_local_roles_for_userid(userid)
|
||||
)
|
||||
return sm.checkPermission(
|
||||
"Delete own comments", comment
|
||||
) and "Owner" in comment.get_local_roles_for_userid(userid)
|
||||
|
||||
def can_delete(self, comment=None):
|
||||
comment = comment or self.context
|
||||
return (
|
||||
len(IReplies(aq_inner(comment))) == 0 and
|
||||
self.could_delete(comment=comment)
|
||||
return len(IReplies(aq_inner(comment))) == 0 and self.could_delete(
|
||||
comment=comment
|
||||
)
|
||||
|
||||
def __call__(self):
|
||||
if self.can_delete():
|
||||
super(DeleteOwnComment, self).__call__()
|
||||
super().__call__()
|
||||
else:
|
||||
raise Unauthorized("You're not allowed to delete this comment.")
|
||||
|
||||
@ -262,33 +258,37 @@ class CommentTransition(BrowserView):
|
||||
"""Call CommentTransition."""
|
||||
comment = aq_inner(self.context)
|
||||
content_object = aq_parent(aq_parent(comment))
|
||||
workflow_action = self.request.form.get('workflow_action', 'publish')
|
||||
workflowTool = getToolByName(self.context, 'portal_workflow')
|
||||
workflow_action = self.request.form.get("workflow_action", "publish")
|
||||
workflowTool = getToolByName(self.context, "portal_workflow")
|
||||
workflowTool.doActionFor(comment, workflow_action)
|
||||
comment.reindexObject()
|
||||
content_object.reindexObject(idxs=['total_comments'])
|
||||
content_object.reindexObject(idxs=["total_comments"])
|
||||
notify(CommentPublishedEvent(self.context, comment))
|
||||
# for complexer workflows:
|
||||
notify(CommentTransitionEvent(self.context, comment))
|
||||
comment_state_translated = ''
|
||||
comment_state_translated = ""
|
||||
if workflowTool.getWorkflowsFor(comment):
|
||||
review_state_new = workflowTool.getInfoFor(ob=comment, name='review_state')
|
||||
review_state_new = workflowTool.getInfoFor(ob=comment, name="review_state")
|
||||
helper = self.context.restrictedTraverse("translationhelper")
|
||||
comment_state_translated = helper.translate_comment_review_state(review_state_new)
|
||||
comment_state_translated = helper.translate_comment_review_state(
|
||||
review_state_new
|
||||
)
|
||||
|
||||
msgid = _(
|
||||
"comment_transmitted",
|
||||
default='Comment ${comment_state_translated}.',
|
||||
mapping={"comment_state_translated": comment_state_translated})
|
||||
default="Comment ${comment_state_translated}.",
|
||||
mapping={"comment_state_translated": comment_state_translated},
|
||||
)
|
||||
translated = self.context.translate(msgid)
|
||||
IStatusMessage(self.request).add(translated, type='info')
|
||||
IStatusMessage(self.request).add(translated, type="info")
|
||||
|
||||
came_from = self.context.REQUEST.HTTP_REFERER
|
||||
# if the referrer already has a came_from in it, don't redirect back
|
||||
if (len(came_from) == 0
|
||||
or 'came_from=' in came_from
|
||||
or not getToolByName(
|
||||
content_object, 'portal_url').isURLInPortal(came_from)):
|
||||
if (
|
||||
len(came_from) == 0
|
||||
or "came_from=" in came_from
|
||||
or not getToolByName(content_object, "portal_url").isURLInPortal(came_from)
|
||||
):
|
||||
came_from = content_object.absolute_url()
|
||||
return self.context.REQUEST.RESPONSE.redirect(came_from)
|
||||
|
||||
@ -317,19 +317,19 @@ class BulkActionsView(BrowserView):
|
||||
"""
|
||||
|
||||
def __init__(self, context, request):
|
||||
super(BulkActionsView, self).__init__(context, request)
|
||||
self.workflowTool = getToolByName(context, 'portal_workflow')
|
||||
super().__init__(context, request)
|
||||
self.workflowTool = getToolByName(context, "portal_workflow")
|
||||
|
||||
def __call__(self):
|
||||
"""Call BulkActionsView."""
|
||||
if 'form.select.BulkAction' in self.request:
|
||||
bulkaction = self.request.get('form.select.BulkAction')
|
||||
self.paths = self.request.get('paths')
|
||||
if "form.select.BulkAction" in self.request:
|
||||
bulkaction = self.request.get("form.select.BulkAction")
|
||||
self.paths = self.request.get("paths")
|
||||
if self.paths:
|
||||
if bulkaction == '-1':
|
||||
if bulkaction == "-1":
|
||||
# no bulk action was selected
|
||||
pass
|
||||
elif bulkaction == 'delete':
|
||||
elif bulkaction == "delete":
|
||||
self.delete()
|
||||
else:
|
||||
self.transmit(bulkaction)
|
||||
@ -346,13 +346,14 @@ class BulkActionsView(BrowserView):
|
||||
comment = context.restrictedTraverse(path)
|
||||
content_object = aq_parent(aq_parent(comment))
|
||||
allowed_transitions = [
|
||||
transition['id'] for transition in self.workflowTool.listActionInfos(object=comment)
|
||||
if transition['category'] == 'workflow' and transition['allowed']
|
||||
]
|
||||
transition["id"]
|
||||
for transition in self.workflowTool.listActionInfos(object=comment)
|
||||
if transition["category"] == "workflow" and transition["allowed"]
|
||||
]
|
||||
if action in allowed_transitions:
|
||||
self.workflowTool.doActionFor(comment, action)
|
||||
comment.reindexObject()
|
||||
content_object.reindexObject(idxs=['total_comments'])
|
||||
content_object.reindexObject(idxs=["total_comments"])
|
||||
notify(CommentPublishedEvent(content_object, comment))
|
||||
# for complexer workflows:
|
||||
notify(CommentTransitionEvent(self.context, comment))
|
||||
@ -370,5 +371,5 @@ class BulkActionsView(BrowserView):
|
||||
conversation = aq_parent(comment)
|
||||
content_object = aq_parent(conversation)
|
||||
del conversation[comment.id]
|
||||
content_object.reindexObject(idxs=['total_comments'])
|
||||
content_object.reindexObject(idxs=["total_comments"])
|
||||
notify(CommentDeletedEvent(content_object, comment))
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Implement the ++comments++ traversal namespace. This should return the
|
||||
IDiscussion container for the context, from which traversal will continue
|
||||
into an actual comment object.
|
||||
@ -15,7 +14,7 @@ from zope.traversing.interfaces import TraversalError
|
||||
|
||||
@implementer(ITraversable)
|
||||
@adapter(Interface, IBrowserRequest)
|
||||
class ConversationNamespace(object):
|
||||
class ConversationNamespace:
|
||||
"""Allow traversal into a conversation via a ++conversation++name
|
||||
namespace. The name is the name of an adapter from context to
|
||||
IConversation. The special name 'default' will be taken as the default
|
||||
@ -29,8 +28,8 @@ class ConversationNamespace(object):
|
||||
|
||||
def traverse(self, name, ignore):
|
||||
|
||||
if name == 'default':
|
||||
name = u''
|
||||
if name == "default":
|
||||
name = ""
|
||||
|
||||
conversation = queryAdapter(self.context, IConversation, name=name)
|
||||
if conversation is None:
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Captcha validator, see captcha.txt for design notes.
|
||||
"""
|
||||
from Acquisition import aq_inner
|
||||
@ -39,16 +38,17 @@ class CaptchaValidator(validator.SimpleFieldValidator):
|
||||
# We adapt the CaptchaValidator class to all form fields (IField)
|
||||
|
||||
def validate(self, value):
|
||||
super(CaptchaValidator, self).validate(value)
|
||||
super().validate(value)
|
||||
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
|
||||
if settings.captcha in ('captcha', 'recaptcha', 'norobots'):
|
||||
captcha = getMultiAdapter((aq_inner(self.context), self.request),
|
||||
name=settings.captcha)
|
||||
if settings.captcha in ("captcha", "recaptcha", "norobots"):
|
||||
captcha = getMultiAdapter(
|
||||
(aq_inner(self.context), self.request), name=settings.captcha
|
||||
)
|
||||
if not captcha.verify(input=value):
|
||||
if settings.captcha == 'norobots':
|
||||
if settings.captcha == "norobots":
|
||||
raise WrongNorobotsAnswer
|
||||
else:
|
||||
raise WrongCaptchaCode
|
||||
@ -57,5 +57,4 @@ class CaptchaValidator(validator.SimpleFieldValidator):
|
||||
|
||||
|
||||
# Register Captcha validator for the Captcha field in the ICaptcha Form
|
||||
validator.WidgetValidatorDiscriminators(CaptchaValidator,
|
||||
field=ICaptcha['captcha'])
|
||||
validator.WidgetValidatorDiscriminators(CaptchaValidator, field=ICaptcha["captcha"])
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Catalog indexers, using plone.indexer. These will populate standard catalog
|
||||
indexes with values based on the IComment interface.
|
||||
|
||||
@ -7,13 +6,12 @@ Also provide event handlers to actually catalog the comments.
|
||||
from DateTime import DateTime
|
||||
from plone.app.discussion.interfaces import IComment
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.base.utils import safe_text
|
||||
from plone.indexer import indexer
|
||||
from plone.uuid.interfaces import IUUID
|
||||
from Products.CMFCore.interfaces import IContentish
|
||||
from Products.CMFPlone.utils import safe_unicode
|
||||
from Products.ZCatalog.interfaces import IZCatalog
|
||||
|
||||
import six
|
||||
|
||||
MAX_DESCRIPTION = 25
|
||||
|
||||
@ -24,7 +22,7 @@ MAX_DESCRIPTION = 25
|
||||
def total_comments(object):
|
||||
# Total number of comments on a conversation
|
||||
# Indexers won't work on old discussion items
|
||||
if object.meta_type != 'Discussion Item':
|
||||
if object.meta_type != "Discussion Item":
|
||||
try:
|
||||
conversation = IConversation(object)
|
||||
return conversation.total_comments()
|
||||
@ -38,7 +36,7 @@ def total_comments(object):
|
||||
def last_comment_date(object):
|
||||
# Date of the latest comment on a conversation
|
||||
# Indexers won't work on old discussion items
|
||||
if object.meta_type != 'Discussion Item':
|
||||
if object.meta_type != "Discussion Item":
|
||||
try:
|
||||
conversation = IConversation(object)
|
||||
return conversation.last_comment_date
|
||||
@ -52,7 +50,7 @@ def last_comment_date(object):
|
||||
def commentators(object):
|
||||
# List of commentators on a conversation
|
||||
# Indexers won't work on old discussion items
|
||||
if object.meta_type != 'Discussion Item':
|
||||
if object.meta_type != "Discussion Item":
|
||||
try:
|
||||
conversation = IConversation(object)
|
||||
return conversation.public_commentators
|
||||
@ -61,6 +59,7 @@ def commentators(object):
|
||||
# implemented an adapter for it
|
||||
pass
|
||||
|
||||
|
||||
# Comment Indexers
|
||||
|
||||
|
||||
@ -73,26 +72,24 @@ def title(object):
|
||||
def creator(object):
|
||||
if not object.creator:
|
||||
return
|
||||
value = safe_unicode(object.creator)
|
||||
if six.PY2:
|
||||
return value.encode('utf8')
|
||||
value = safe_text(object.creator)
|
||||
return value
|
||||
|
||||
|
||||
@indexer(IComment)
|
||||
def description(object):
|
||||
# Return the first 25 words of the comment text and append ' [...]'
|
||||
text = ' '.join(
|
||||
object.getText(targetMimetype='text/plain').split()[:MAX_DESCRIPTION],
|
||||
text = " ".join(
|
||||
object.getText(targetMimetype="text/plain").split()[:MAX_DESCRIPTION],
|
||||
)
|
||||
if len(object.getText().split()) > 25:
|
||||
text += ' [...]'
|
||||
text += " [...]"
|
||||
return text
|
||||
|
||||
|
||||
@indexer(IComment)
|
||||
def searchable_text(object):
|
||||
return object.getText(targetMimetype='text/plain')
|
||||
return object.getText(targetMimetype="text/plain")
|
||||
|
||||
|
||||
@indexer(IComment)
|
||||
@ -106,42 +103,42 @@ def in_response_to(object):
|
||||
def effective(object):
|
||||
# the catalog index needs Zope DateTime instead of Python datetime
|
||||
return DateTime(
|
||||
object.creation_date.year,
|
||||
object.creation_date.month,
|
||||
object.creation_date.day,
|
||||
object.creation_date.hour,
|
||||
object.creation_date.minute,
|
||||
object.creation_date.second,
|
||||
'GMT',
|
||||
)
|
||||
object.creation_date.year,
|
||||
object.creation_date.month,
|
||||
object.creation_date.day,
|
||||
object.creation_date.hour,
|
||||
object.creation_date.minute,
|
||||
object.creation_date.second,
|
||||
"GMT",
|
||||
)
|
||||
|
||||
|
||||
@indexer(IComment)
|
||||
def created(object):
|
||||
# the catalog index needs Zope DateTime instead of Python datetime
|
||||
return DateTime(
|
||||
object.creation_date.year,
|
||||
object.creation_date.month,
|
||||
object.creation_date.day,
|
||||
object.creation_date.hour,
|
||||
object.creation_date.minute,
|
||||
object.creation_date.second,
|
||||
'GMT',
|
||||
)
|
||||
object.creation_date.year,
|
||||
object.creation_date.month,
|
||||
object.creation_date.day,
|
||||
object.creation_date.hour,
|
||||
object.creation_date.minute,
|
||||
object.creation_date.second,
|
||||
"GMT",
|
||||
)
|
||||
|
||||
|
||||
@indexer(IComment)
|
||||
def modified(object):
|
||||
# the catalog index needs Zope DateTime instead of Python datetime
|
||||
return DateTime(
|
||||
object.modification_date.year,
|
||||
object.modification_date.month,
|
||||
object.modification_date.day,
|
||||
object.modification_date.hour,
|
||||
object.modification_date.minute,
|
||||
object.modification_date.second,
|
||||
'GMT',
|
||||
)
|
||||
object.modification_date.year,
|
||||
object.modification_date.month,
|
||||
object.modification_date.day,
|
||||
object.modification_date.hour,
|
||||
object.modification_date.minute,
|
||||
object.modification_date.second,
|
||||
"GMT",
|
||||
)
|
||||
|
||||
|
||||
# Override the conversation indexers for comments
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The default comment class and factory.
|
||||
"""
|
||||
from AccessControl import ClassSecurityInfo
|
||||
@ -21,14 +20,14 @@ from plone.app.discussion.events import ReplyRemovedEvent
|
||||
from plone.app.discussion.interfaces import IComment
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.base.interfaces.controlpanel import IMailSchema
|
||||
from plone.base.utils import safe_text
|
||||
from plone.registry.interfaces import IRegistry
|
||||
from Products.CMFCore import permissions
|
||||
from Products.CMFCore.CMFCatalogAware import CatalogAware
|
||||
from Products.CMFCore.CMFCatalogAware import WorkflowAware
|
||||
from Products.CMFCore.DynamicType import DynamicType
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
from Products.CMFPlone.interfaces.controlpanel import IMailSchema
|
||||
from Products.CMFPlone.utils import safe_unicode
|
||||
from smtplib import SMTPException
|
||||
from zope.annotation.interfaces import IAnnotatable
|
||||
from zope.component import getUtility
|
||||
@ -40,61 +39,69 @@ from zope.i18nmessageid import Message
|
||||
from zope.interface import implementer
|
||||
|
||||
import logging
|
||||
import six
|
||||
|
||||
|
||||
COMMENT_TITLE = _(
|
||||
u'comment_title',
|
||||
default=u'${author_name} on ${content}',
|
||||
)
|
||||
"comment_title",
|
||||
default="${author_name} on ${content}",
|
||||
)
|
||||
|
||||
MAIL_NOTIFICATION_MESSAGE = _(
|
||||
u'mail_notification_message',
|
||||
default=u'A comment on "${title}" '
|
||||
u'has been posted here: ${link}\n\n'
|
||||
u'---\n'
|
||||
u'${text}\n'
|
||||
u'---\n',
|
||||
)
|
||||
"mail_notification_message",
|
||||
default='A comment on "${title}" '
|
||||
"has been posted here: ${link}\n\n"
|
||||
"---\n"
|
||||
"${text}\n"
|
||||
"---\n",
|
||||
)
|
||||
|
||||
MAIL_NOTIFICATION_MESSAGE_MODERATOR = _(
|
||||
u'mail_notification_message_moderator2',
|
||||
default=u'A comment on "${title}" '
|
||||
u'has been posted by ${commentator}\n'
|
||||
u'here: ${link}\n\n'
|
||||
u'---\n\n'
|
||||
u'${text}\n\n'
|
||||
u'---\n\n'
|
||||
u'Log in to moderate.\n\n',
|
||||
)
|
||||
"mail_notification_message_moderator2",
|
||||
default='A comment on "${title}" '
|
||||
"has been posted by ${commentator}\n"
|
||||
"here: ${link}\n\n"
|
||||
"---\n\n"
|
||||
"${text}\n\n"
|
||||
"---\n\n"
|
||||
"Log in to moderate.\n\n",
|
||||
)
|
||||
|
||||
logger = logging.getLogger('plone.app.discussion')
|
||||
logger = logging.getLogger("plone.app.discussion")
|
||||
|
||||
|
||||
@implementer(IComment)
|
||||
class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
|
||||
RoleManager, Owned, Implicit, Persistent):
|
||||
class Comment(
|
||||
CatalogAware,
|
||||
WorkflowAware,
|
||||
DynamicType,
|
||||
Traversable,
|
||||
RoleManager,
|
||||
Owned,
|
||||
Implicit,
|
||||
Persistent,
|
||||
):
|
||||
"""A comment.
|
||||
|
||||
This object attempts to be as lightweight as possible. We implement a
|
||||
number of standard methods instead of subclassing, to have total control
|
||||
over what goes into the object.
|
||||
"""
|
||||
|
||||
security = ClassSecurityInfo()
|
||||
|
||||
meta_type = portal_type = 'Discussion Item'
|
||||
meta_type = portal_type = "Discussion Item"
|
||||
# This needs to be kept in sync with types/Discussion_Item.xml title
|
||||
fti_title = 'Comment'
|
||||
fti_title = "Comment"
|
||||
|
||||
__parent__ = None
|
||||
|
||||
comment_id = None # long
|
||||
in_reply_to = None # long
|
||||
|
||||
title = u''
|
||||
title = ""
|
||||
|
||||
mime_type = None
|
||||
text = u''
|
||||
text = ""
|
||||
|
||||
creator = None
|
||||
creation_date = None
|
||||
@ -113,19 +120,22 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
|
||||
|
||||
def __init__(self):
|
||||
self.creation_date = self.modification_date = datetime.utcnow()
|
||||
self.mime_type = 'text/plain'
|
||||
self.mime_type = "text/plain"
|
||||
|
||||
user = getSecurityManager().getUser()
|
||||
if user and user.getId():
|
||||
aclpath = [x for x in user.getPhysicalPath() if x]
|
||||
self._owner = (aclpath, user.getId(),)
|
||||
self._owner = (
|
||||
aclpath,
|
||||
user.getId(),
|
||||
)
|
||||
self.__ac_local_roles__ = {
|
||||
user.getId(): ['Owner'],
|
||||
user.getId(): ["Owner"],
|
||||
}
|
||||
|
||||
@property
|
||||
def __name__(self):
|
||||
return self.comment_id and six.text_type(self.comment_id) or None
|
||||
return self.comment_id and str(self.comment_id) or None
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
@ -137,32 +147,30 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
|
||||
|
||||
def getText(self, targetMimetype=None):
|
||||
"""The body text of a comment."""
|
||||
transforms = getToolByName(self, 'portal_transforms')
|
||||
transforms = getToolByName(self, "portal_transforms")
|
||||
|
||||
if targetMimetype is None:
|
||||
targetMimetype = 'text/x-html-safe'
|
||||
targetMimetype = "text/x-html-safe"
|
||||
|
||||
sourceMimetype = getattr(self, 'mime_type', None)
|
||||
sourceMimetype = getattr(self, "mime_type", None)
|
||||
if sourceMimetype is None:
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
sourceMimetype = settings.text_transform
|
||||
text = self.text
|
||||
if text is None:
|
||||
return ''
|
||||
if six.PY2 and isinstance(text, six.text_type):
|
||||
text = text.encode('utf8')
|
||||
return ""
|
||||
transform = transforms.convertTo(
|
||||
targetMimetype,
|
||||
text,
|
||||
context=self,
|
||||
mimetype=sourceMimetype)
|
||||
targetMimetype, text, context=self, mimetype=sourceMimetype
|
||||
)
|
||||
if transform:
|
||||
return transform.getData()
|
||||
else:
|
||||
logger = logging.getLogger('plone.app.discussion')
|
||||
msg = u'Transform "{0}" => "{1}" not available. Failed to ' \
|
||||
u'transform comment "{2}".'
|
||||
logger = logging.getLogger("plone.app.discussion")
|
||||
msg = (
|
||||
'Transform "{0}" => "{1}" not available. Failed to '
|
||||
'transform comment "{2}".'
|
||||
)
|
||||
logger.error(
|
||||
msg.format(
|
||||
sourceMimetype,
|
||||
@ -182,8 +190,8 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
|
||||
author_name = translate(
|
||||
Message(
|
||||
_(
|
||||
u'label_anonymous',
|
||||
default=u'Anonymous',
|
||||
"label_anonymous",
|
||||
default="Anonymous",
|
||||
),
|
||||
),
|
||||
)
|
||||
@ -194,9 +202,14 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
|
||||
# conversation, the parent of the conversation is the content object).
|
||||
content = aq_base(self.__parent__.__parent__)
|
||||
title = translate(
|
||||
Message(COMMENT_TITLE,
|
||||
mapping={'author_name': safe_unicode(author_name),
|
||||
'content': safe_unicode(content.Title())}))
|
||||
Message(
|
||||
COMMENT_TITLE,
|
||||
mapping={
|
||||
"author_name": safe_text(author_name),
|
||||
"content": safe_text(content.Title()),
|
||||
},
|
||||
)
|
||||
)
|
||||
return title
|
||||
|
||||
def Creator(self):
|
||||
@ -224,25 +237,23 @@ CommentFactory = Factory(Comment)
|
||||
|
||||
|
||||
def notify_workflow(obj, event):
|
||||
"""Tell the workflow tool when a comment is added
|
||||
"""
|
||||
tool = getToolByName(obj, 'portal_workflow', None)
|
||||
"""Tell the workflow tool when a comment is added"""
|
||||
tool = getToolByName(obj, "portal_workflow", None)
|
||||
if tool is not None:
|
||||
tool.notifyCreated(obj)
|
||||
|
||||
|
||||
def notify_content_object(obj, event):
|
||||
"""Tell the content object when a comment is added
|
||||
"""
|
||||
"""Tell the content object when a comment is added"""
|
||||
content_obj = aq_parent(aq_parent(obj))
|
||||
content_obj.reindexObject(idxs=('total_comments',
|
||||
'last_comment_date',
|
||||
'commentators'))
|
||||
content_obj.reindexObject(
|
||||
idxs=("total_comments", "last_comment_date", "commentators")
|
||||
)
|
||||
|
||||
|
||||
def notify_content_object_deleted(obj, event):
|
||||
"""Remove all comments of a content object when the content object has been
|
||||
deleted.
|
||||
deleted.
|
||||
"""
|
||||
if IAnnotatable.providedBy(obj):
|
||||
conversation = IConversation(obj)
|
||||
@ -251,40 +262,40 @@ def notify_content_object_deleted(obj, event):
|
||||
|
||||
|
||||
def notify_comment_added(obj, event):
|
||||
""" Notify custom discussion events when a comment is added or replied
|
||||
"""
|
||||
"""Notify custom discussion events when a comment is added or replied"""
|
||||
conversation = aq_parent(obj)
|
||||
context = aq_parent(conversation)
|
||||
if getattr(obj, 'in_reply_to', None):
|
||||
if getattr(obj, "in_reply_to", None):
|
||||
return notify(ReplyAddedEvent(context, obj))
|
||||
return notify(CommentAddedEvent(context, obj))
|
||||
|
||||
|
||||
def notify_comment_modified(obj, event):
|
||||
""" Notify custom discussion events when a comment, or a reply, is modified
|
||||
"""
|
||||
"""Notify custom discussion events when a comment, or a reply, is modified"""
|
||||
conversation = aq_parent(obj)
|
||||
context = aq_parent(conversation)
|
||||
if getattr(obj, 'in_reply_to', None):
|
||||
if getattr(obj, "in_reply_to", None):
|
||||
return notify(ReplyModifiedEvent(context, obj))
|
||||
return notify(CommentModifiedEvent(context, obj))
|
||||
|
||||
|
||||
def notify_comment_removed(obj, event):
|
||||
""" Notify custom discussion events when a comment or reply is removed
|
||||
"""
|
||||
"""Notify custom discussion events when a comment or reply is removed"""
|
||||
conversation = aq_parent(obj)
|
||||
context = aq_parent(conversation)
|
||||
if getattr(obj, 'in_reply_to', None):
|
||||
if getattr(obj, "in_reply_to", None):
|
||||
return notify(ReplyRemovedEvent(context, obj))
|
||||
return notify(CommentRemovedEvent(context, obj))
|
||||
|
||||
|
||||
def notify_content_object_moved(obj, event):
|
||||
"""Update all comments of a content object that has been moved.
|
||||
"""
|
||||
if event.oldParent is None or event.newParent is None \
|
||||
or event.oldName is None or event.newName is None:
|
||||
"""Update all comments of a content object that has been moved."""
|
||||
if (
|
||||
event.oldParent is None
|
||||
or event.newParent is None
|
||||
or event.oldName is None
|
||||
or event.newName is None
|
||||
):
|
||||
return
|
||||
|
||||
# This method is also called for sublocations of moved objects. We
|
||||
@ -293,21 +304,19 @@ def notify_content_object_moved(obj, event):
|
||||
# in the object hierarchy. The object is already moved at this point. so
|
||||
# obj.getPhysicalPath retruns the new path get the part of the path that
|
||||
# was moved.
|
||||
moved_path = obj.getPhysicalPath()[
|
||||
len(event.newParent.getPhysicalPath()) + 1:
|
||||
]
|
||||
moved_path = obj.getPhysicalPath()[len(event.newParent.getPhysicalPath()) + 1 :]
|
||||
|
||||
# Remove comments at the old location from catalog
|
||||
catalog = getToolByName(obj, 'portal_catalog')
|
||||
old_path = '/'.join(
|
||||
event.oldParent.getPhysicalPath() +
|
||||
(event.oldName,) +
|
||||
moved_path,
|
||||
catalog = getToolByName(obj, "portal_catalog")
|
||||
old_path = "/".join(
|
||||
event.oldParent.getPhysicalPath() + (event.oldName,) + moved_path,
|
||||
)
|
||||
brains = catalog.searchResults(
|
||||
dict(
|
||||
path={"query": old_path},
|
||||
portal_type="Discussion Item",
|
||||
)
|
||||
)
|
||||
brains = catalog.searchResults(dict(
|
||||
path={'query': old_path},
|
||||
portal_type='Discussion Item',
|
||||
))
|
||||
for brain in brains:
|
||||
catalog.uncatalog_object(brain.getPath())
|
||||
# Reindex comment at the new location
|
||||
@ -320,11 +329,11 @@ def notify_content_object_moved(obj, event):
|
||||
def notify_user(obj, event):
|
||||
"""Tell users when a comment has been added.
|
||||
|
||||
This method composes and sends emails to all users that have added a
|
||||
comment to this conversation and enabled user notification.
|
||||
This method composes and sends emails to all users that have added a
|
||||
comment to this conversation and enabled user notification.
|
||||
|
||||
This requires the user_notification setting to be enabled in the
|
||||
discussion control panel.
|
||||
This requires the user_notification setting to be enabled in the
|
||||
discussion control panel.
|
||||
"""
|
||||
|
||||
# Check if user notification is enabled
|
||||
@ -334,9 +343,9 @@ def notify_user(obj, event):
|
||||
return
|
||||
|
||||
# Get informations that are necessary to send an email
|
||||
mail_host = getToolByName(obj, 'MailHost')
|
||||
mail_host = getToolByName(obj, "MailHost")
|
||||
registry = getUtility(IRegistry)
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
|
||||
sender = mail_settings.email_from_address
|
||||
|
||||
# Check if a sender address is available
|
||||
@ -360,15 +369,14 @@ def notify_user(obj, event):
|
||||
if not emails:
|
||||
return
|
||||
|
||||
subject = translate(_(u'A comment has been posted.'),
|
||||
context=obj.REQUEST)
|
||||
subject = translate(_("A comment has been posted."), context=obj.REQUEST)
|
||||
message = translate(
|
||||
Message(
|
||||
MAIL_NOTIFICATION_MESSAGE,
|
||||
mapping={
|
||||
'title': safe_unicode(content_object.title),
|
||||
'link': content_object.absolute_url() + '/view#' + obj.id,
|
||||
'text': obj.text,
|
||||
"title": safe_text(content_object.title),
|
||||
"link": content_object.absolute_url() + "/view#" + obj.id,
|
||||
"text": obj.text,
|
||||
},
|
||||
),
|
||||
context=obj.REQUEST,
|
||||
@ -381,12 +389,11 @@ def notify_user(obj, event):
|
||||
email,
|
||||
sender,
|
||||
subject,
|
||||
charset='utf-8',
|
||||
charset="utf-8",
|
||||
)
|
||||
except SMTPException:
|
||||
logger.error(
|
||||
'SMTP exception while trying to send an ' +
|
||||
'email from %s to %s',
|
||||
"SMTP exception while trying to send an " + "email from %s to %s",
|
||||
sender,
|
||||
email,
|
||||
)
|
||||
@ -395,15 +402,15 @@ def notify_user(obj, event):
|
||||
def notify_moderator(obj, event):
|
||||
"""Tell the moderator when a comment needs attention.
|
||||
|
||||
This method sends an email to the moderator if comment moderation a new
|
||||
comment has been added that needs to be approved.
|
||||
This method sends an email to the moderator if comment moderation a new
|
||||
comment has been added that needs to be approved.
|
||||
|
||||
The moderator_notification setting has to be enabled in the discussion
|
||||
control panel.
|
||||
The moderator_notification setting has to be enabled in the discussion
|
||||
control panel.
|
||||
|
||||
Configure the moderator e-mail address in the discussion control panel.
|
||||
If no moderator is configured but moderator notifications are turned on,
|
||||
the site admin email (from the mail control panel) will be used.
|
||||
Configure the moderator e-mail address in the discussion control panel.
|
||||
If no moderator is configured but moderator notifications are turned on,
|
||||
the site admin email (from the mail control panel) will be used.
|
||||
"""
|
||||
# Check if moderator notification is enabled
|
||||
registry = queryUtility(IRegistry)
|
||||
@ -412,9 +419,9 @@ def notify_moderator(obj, event):
|
||||
return
|
||||
|
||||
# Get informations that are necessary to send an email
|
||||
mail_host = getToolByName(obj, 'MailHost')
|
||||
mail_host = getToolByName(obj, "MailHost")
|
||||
registry = getUtility(IRegistry)
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
|
||||
sender = mail_settings.email_from_address
|
||||
|
||||
if settings.moderator_email:
|
||||
@ -430,22 +437,23 @@ def notify_moderator(obj, event):
|
||||
content_object = aq_parent(conversation)
|
||||
|
||||
# Compose email
|
||||
subject = translate(_(u'A comment has been posted.'), context=obj.REQUEST)
|
||||
subject = translate(_("A comment has been posted."), context=obj.REQUEST)
|
||||
message = translate(
|
||||
Message(
|
||||
MAIL_NOTIFICATION_MESSAGE_MODERATOR,
|
||||
mapping={
|
||||
'title': safe_unicode(content_object.title),
|
||||
'link': content_object.absolute_url() + '/view#' + obj.id,
|
||||
'text': obj.text,
|
||||
'commentator': obj.author_email or translate(
|
||||
Message(
|
||||
_(
|
||||
u'label_anonymous',
|
||||
default=u'Anonymous',
|
||||
),
|
||||
"title": safe_text(content_object.title),
|
||||
"link": content_object.absolute_url() + "/view#" + obj.id,
|
||||
"text": obj.text,
|
||||
"commentator": obj.author_email
|
||||
or translate(
|
||||
Message(
|
||||
_(
|
||||
"label_anonymous",
|
||||
default="Anonymous",
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
context=obj.REQUEST,
|
||||
@ -453,12 +461,12 @@ def notify_moderator(obj, event):
|
||||
|
||||
# Send email
|
||||
try:
|
||||
mail_host.send(message, mto, sender, subject, charset='utf-8')
|
||||
mail_host.send(message, mto, sender, subject, charset="utf-8")
|
||||
except SMTPException as e:
|
||||
logger.error(
|
||||
'SMTP exception (%s) while trying to send an ' +
|
||||
'email notification to the comment moderator ' +
|
||||
'(from %s to %s, message: %s)',
|
||||
"SMTP exception (%s) while trying to send an "
|
||||
+ "email notification to the comment moderator "
|
||||
+ "(from %s to %s, message: %s)",
|
||||
e,
|
||||
sender,
|
||||
mto,
|
||||
|
@ -39,7 +39,7 @@
|
||||
description="Commenting infrastructure for Plone"
|
||||
directory="profiles/default"
|
||||
provides="Products.GenericSetup.interfaces.EXTENSION"
|
||||
for="Products.CMFPlone.interfaces.IPloneSiteRoot"
|
||||
for="plone.base.interfaces.IPloneSiteRoot"
|
||||
/>
|
||||
<!-- For upgrade steps see upgrades.zcml. -->
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Content rules handlers
|
||||
"""
|
||||
from plone.app.discussion import _
|
||||
@ -7,101 +6,94 @@ from plone.app.discussion import _
|
||||
try:
|
||||
from plone.stringinterp.adapters import BaseSubstitution
|
||||
except ImportError:
|
||||
class BaseSubstitution(object):
|
||||
""" Fallback class if plone.stringinterp is not available
|
||||
"""
|
||||
|
||||
class BaseSubstitution:
|
||||
"""Fallback class if plone.stringinterp is not available"""
|
||||
|
||||
def __init__(self, context, **kwargs):
|
||||
self.context = context
|
||||
|
||||
|
||||
try:
|
||||
from plone.app.contentrules.handlers import execute
|
||||
except ImportError:
|
||||
|
||||
def execute(context, event):
|
||||
return False
|
||||
|
||||
|
||||
def execute_comment(event):
|
||||
""" Execute comment content rules
|
||||
"""
|
||||
"""Execute comment content rules"""
|
||||
execute(event.object, event)
|
||||
|
||||
|
||||
class CommentSubstitution(BaseSubstitution):
|
||||
""" Comment string substitution
|
||||
"""
|
||||
"""Comment string substitution"""
|
||||
|
||||
def __init__(self, context, **kwargs):
|
||||
super(CommentSubstitution, self).__init__(context, **kwargs)
|
||||
super().__init__(context, **kwargs)
|
||||
|
||||
@property
|
||||
def event(self):
|
||||
""" event that triggered the content rule
|
||||
"""
|
||||
return self.context.REQUEST.get('event')
|
||||
"""event that triggered the content rule"""
|
||||
return self.context.REQUEST.get("event")
|
||||
|
||||
@property
|
||||
def comment(self):
|
||||
""" Get changed inline comment
|
||||
"""
|
||||
"""Get changed inline comment"""
|
||||
return self.event.comment
|
||||
|
||||
|
||||
class Id(CommentSubstitution):
|
||||
""" Comment id string substitution
|
||||
"""
|
||||
category = _(u'Comments')
|
||||
description = _(u'Comment id')
|
||||
"""Comment id string substitution"""
|
||||
|
||||
category = _("Comments")
|
||||
description = _("Comment id")
|
||||
|
||||
def safe_call(self):
|
||||
""" Safe call
|
||||
"""
|
||||
return getattr(self.comment, 'comment_id', u'')
|
||||
"""Safe call"""
|
||||
return getattr(self.comment, "comment_id", "")
|
||||
|
||||
|
||||
class Text(CommentSubstitution):
|
||||
""" Comment text
|
||||
"""
|
||||
category = _(u'Comments')
|
||||
description = _(u'Comment text')
|
||||
"""Comment text"""
|
||||
|
||||
category = _("Comments")
|
||||
description = _("Comment text")
|
||||
|
||||
def safe_call(self):
|
||||
""" Safe call
|
||||
"""
|
||||
return getattr(self.comment, 'text', u'')
|
||||
"""Safe call"""
|
||||
return getattr(self.comment, "text", "")
|
||||
|
||||
|
||||
class AuthorUserName(CommentSubstitution):
|
||||
""" Comment author user name string substitution
|
||||
"""
|
||||
category = _(u'Comments')
|
||||
description = _(u'Comment author user name')
|
||||
"""Comment author user name string substitution"""
|
||||
|
||||
category = _("Comments")
|
||||
description = _("Comment author user name")
|
||||
|
||||
def safe_call(self):
|
||||
""" Safe call
|
||||
"""
|
||||
return getattr(self.comment, 'author_username', u'')
|
||||
"""Safe call"""
|
||||
return getattr(self.comment, "author_username", "")
|
||||
|
||||
|
||||
class AuthorFullName(CommentSubstitution):
|
||||
""" Comment author full name string substitution
|
||||
"""
|
||||
category = _(u'Comments')
|
||||
description = _(u'Comment author full name')
|
||||
"""Comment author full name string substitution"""
|
||||
|
||||
category = _("Comments")
|
||||
description = _("Comment author full name")
|
||||
|
||||
def safe_call(self):
|
||||
""" Safe call
|
||||
"""
|
||||
return getattr(self.comment, 'author_name', u'')
|
||||
"""Safe call"""
|
||||
return getattr(self.comment, "author_name", "")
|
||||
|
||||
|
||||
class AuthorEmail(CommentSubstitution):
|
||||
""" Comment author email string substitution
|
||||
"""
|
||||
category = _(u'Comments')
|
||||
description = _(u'Comment author email')
|
||||
"""Comment author email string substitution"""
|
||||
|
||||
category = _("Comments")
|
||||
description = _("Comment author email")
|
||||
|
||||
def safe_call(self):
|
||||
""" Safe call
|
||||
"""
|
||||
return getattr(self.comment, 'author_email', u'')
|
||||
"""Safe call"""
|
||||
return getattr(self.comment, "author_email", "")
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The conversation and replies adapters
|
||||
|
||||
The conversation is responsible for storing all comments. It provides a
|
||||
@ -10,6 +9,10 @@ manipulate the same data structures, but provide an API for finding and
|
||||
manipulating the comments directly in reply to a particular comment or at the
|
||||
top level of the conversation.
|
||||
"""
|
||||
from .comment import Comment
|
||||
from .interfaces import DISCUSSION_ANNOTATION_KEY as ANNOTATION_KEY
|
||||
from .interfaces import IConversation
|
||||
from .interfaces import IReplies
|
||||
from AccessControl.SpecialUsers import nobody as user_nobody
|
||||
from Acquisition import aq_base
|
||||
from Acquisition import aq_inner
|
||||
@ -22,11 +25,7 @@ from OFS.event import ObjectWillBeAddedEvent
|
||||
from OFS.event import ObjectWillBeRemovedEvent
|
||||
from OFS.Traversable import Traversable
|
||||
from persistent import Persistent
|
||||
from plone.app.discussion.comment import Comment
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IReplies
|
||||
from Products.CMFPlone import DISCUSSION_ANNOTATION_KEY as ANNOTATION_KEY
|
||||
from Products.CMFPlone.interfaces import IHideFromBreadcrumbs
|
||||
from plone.base.interfaces import IHideFromBreadcrumbs
|
||||
from zope.annotation.interfaces import IAnnotatable
|
||||
from zope.annotation.interfaces import IAnnotations
|
||||
from zope.component import adapter
|
||||
@ -37,7 +36,6 @@ from zope.lifecycleevent import ObjectAddedEvent
|
||||
from zope.lifecycleevent import ObjectCreatedEvent
|
||||
from zope.lifecycleevent import ObjectRemovedEvent
|
||||
|
||||
import six
|
||||
import time
|
||||
|
||||
|
||||
@ -51,7 +49,7 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
|
||||
__allow_access_to_unprotected_subobjects__ = True
|
||||
|
||||
def __init__(self, id='++conversation++default'):
|
||||
def __init__(self, id="++conversation++default"):
|
||||
self.id = id
|
||||
|
||||
# username -> count of comments; key is removed when count reaches 0
|
||||
@ -72,12 +70,11 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
|
||||
def enabled(self):
|
||||
parent = aq_inner(self.__parent__)
|
||||
return parent.restrictedTraverse('@@conversation_view').enabled()
|
||||
return parent.restrictedTraverse("@@conversation_view").enabled()
|
||||
|
||||
def total_comments(self):
|
||||
public_comments = [
|
||||
x for x in self.values()
|
||||
if user_nobody.has_permission('View', x)
|
||||
x for x in self.values() if user_nobody.has_permission("View", x)
|
||||
]
|
||||
return len(public_comments)
|
||||
|
||||
@ -88,7 +85,7 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
comment_keys = self._comments.keys()
|
||||
for comment_key in reversed(comment_keys):
|
||||
comment = self._comments[comment_key]
|
||||
if user_nobody.has_permission('View', comment):
|
||||
if user_nobody.has_permission("View", comment):
|
||||
return comment.creation_date
|
||||
return None
|
||||
|
||||
@ -100,7 +97,7 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
def public_commentators(self):
|
||||
retval = set()
|
||||
for comment in self._comments.values():
|
||||
if not user_nobody.has_permission('View', comment):
|
||||
if not user_nobody.has_permission("View", comment):
|
||||
continue
|
||||
retval.add(comment.author_username)
|
||||
return tuple(retval)
|
||||
@ -109,8 +106,7 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
return self._comments.keys()
|
||||
|
||||
def getComments(self, start=0, size=None):
|
||||
"""Get unthreaded comments
|
||||
"""
|
||||
"""Get unthreaded comments"""
|
||||
count = 0
|
||||
for comment in self._comments.values(min=start):
|
||||
# Yield the acquisition wrapped comment
|
||||
@ -121,20 +117,18 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
return
|
||||
|
||||
def getThreads(self, start=0, size=None, root=0, depth=None):
|
||||
"""Get threaded comments
|
||||
"""
|
||||
"""Get threaded comments"""
|
||||
|
||||
def recurse(comment_id, d=0):
|
||||
# Yield the current comment before we look for its children
|
||||
yield {'id': comment_id, 'comment': self[comment_id], 'depth': d}
|
||||
yield {"id": comment_id, "comment": self[comment_id], "depth": d}
|
||||
|
||||
# Recurse if there are children and we are not out of our depth
|
||||
if depth is None or d + 1 < depth:
|
||||
children = self._children.get(comment_id, None)
|
||||
if children is not None:
|
||||
for child_id in children:
|
||||
for value in recurse(child_id, d + 1):
|
||||
yield value
|
||||
yield from recurse(child_id, d + 1)
|
||||
|
||||
# Find top level threads
|
||||
comments = self._children.get(root, None)
|
||||
@ -148,8 +142,7 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
return
|
||||
|
||||
# Let the closure recurse
|
||||
for value in recurse(comment_id):
|
||||
yield value
|
||||
yield from recurse(comment_id)
|
||||
|
||||
def addComment(self, comment):
|
||||
"""Add a new comment. The parent id should have been set already. The
|
||||
@ -209,8 +202,7 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
return int(key) in self._comments
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get an item by its int key
|
||||
"""
|
||||
"""Get an item by its int key"""
|
||||
try:
|
||||
comment_id = int(key)
|
||||
except ValueError:
|
||||
@ -218,8 +210,7 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
return self._comments[comment_id].__of__(self)
|
||||
|
||||
def __delitem__(self, key, suppress_container_modified=False):
|
||||
"""Delete an item by its int key
|
||||
"""
|
||||
"""Delete an item by its int key"""
|
||||
|
||||
key = int(key)
|
||||
|
||||
@ -269,21 +260,30 @@ class Conversation(Traversable, Persistent, Explicit):
|
||||
return self._comments.keys()
|
||||
|
||||
def items(self):
|
||||
return [(i[0], i[1].__of__(self),) for i in self._comments.items()]
|
||||
return [
|
||||
(
|
||||
i[0],
|
||||
i[1].__of__(self),
|
||||
)
|
||||
for i in self._comments.items()
|
||||
]
|
||||
|
||||
def values(self):
|
||||
return [v.__of__(self) for v in self._comments.values()]
|
||||
|
||||
def iterkeys(self):
|
||||
return six.iterkeys(self._comments)
|
||||
return self._comments.keys()
|
||||
|
||||
def itervalues(self):
|
||||
for v in six.itervalues(self._comments):
|
||||
for v in self._comments.values():
|
||||
yield v.__of__(self)
|
||||
|
||||
def iteritems(self):
|
||||
for k, v in six.iteritems(self._comments):
|
||||
yield (k, v.__of__(self),)
|
||||
for k, v in self._comments.items():
|
||||
yield (
|
||||
k,
|
||||
v.__of__(self),
|
||||
)
|
||||
|
||||
def allowedContentTypes(self):
|
||||
return []
|
||||
@ -309,6 +309,7 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
|
||||
@implementer(IConversation) # pragma: no cover
|
||||
@adapter(IAnnotatable) # pragma: no cover
|
||||
def conversationCanonicalAdapterFactory(content): # pragma: no cover
|
||||
@ -327,7 +328,7 @@ else:
|
||||
|
||||
@implementer(IReplies)
|
||||
@adapter(Conversation) # relies on implementation details
|
||||
class ConversationReplies(object):
|
||||
class ConversationReplies:
|
||||
"""An IReplies adapter for conversations.
|
||||
|
||||
This makes it easy to work with top-level comments.
|
||||
@ -350,16 +351,14 @@ class ConversationReplies(object):
|
||||
return int(key) in self.children
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""Get an item by its int key
|
||||
"""
|
||||
"""Get an item by its int key"""
|
||||
key = int(key)
|
||||
if key not in self.children:
|
||||
raise KeyError(key)
|
||||
return self.conversation[key]
|
||||
|
||||
def __delitem__(self, key):
|
||||
"""Delete an item by its int key
|
||||
"""
|
||||
"""Delete an item by its int key"""
|
||||
key = int(key)
|
||||
if key not in self.children:
|
||||
raise KeyError(key)
|
||||
@ -392,7 +391,10 @@ class ConversationReplies(object):
|
||||
|
||||
def iteritems(self):
|
||||
for key in self.children:
|
||||
yield (key, self.conversation[key],)
|
||||
yield (
|
||||
key,
|
||||
self.conversation[key],
|
||||
)
|
||||
|
||||
@property
|
||||
def children(self):
|
||||
@ -418,11 +420,12 @@ class CommentReplies(ConversationReplies):
|
||||
self.conversation = aq_parent(self.comment)
|
||||
conversation_has_no_children = not hasattr(
|
||||
self.conversation,
|
||||
'_children',
|
||||
"_children",
|
||||
)
|
||||
if self.conversation is None or conversation_has_no_children:
|
||||
raise TypeError("This adapter doesn't know what to do with the "
|
||||
'parent conversation')
|
||||
raise TypeError(
|
||||
"This adapter doesn't know what to do with the " "parent conversation"
|
||||
)
|
||||
|
||||
self.comment_id = self.comment.comment_id
|
||||
|
||||
|
@ -1,13 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
""" Custom discussion events
|
||||
"""
|
||||
from plone.app.discussion.interfaces import ICommentAddedEvent
|
||||
from plone.app.discussion.interfaces import ICommentModifiedEvent
|
||||
from plone.app.discussion.interfaces import ICommentRemovedEvent
|
||||
from plone.app.discussion.interfaces import IDiscussionEvent
|
||||
from plone.app.discussion.interfaces import ICommentDeletedEvent
|
||||
from plone.app.discussion.interfaces import ICommentModifiedEvent
|
||||
from plone.app.discussion.interfaces import ICommentPublishedEvent
|
||||
from plone.app.discussion.interfaces import ICommentRemovedEvent
|
||||
from plone.app.discussion.interfaces import ICommentTransitionEvent
|
||||
from plone.app.discussion.interfaces import IDiscussionEvent
|
||||
from plone.app.discussion.interfaces import IReplyAddedEvent
|
||||
from plone.app.discussion.interfaces import IReplyModifiedEvent
|
||||
from plone.app.discussion.interfaces import IReplyRemovedEvent
|
||||
@ -15,9 +14,8 @@ from zope.interface import implementer
|
||||
|
||||
|
||||
@implementer(IDiscussionEvent)
|
||||
class DiscussionEvent(object):
|
||||
""" Custom event
|
||||
"""
|
||||
class DiscussionEvent:
|
||||
"""Custom event"""
|
||||
|
||||
def __init__(self, context, comment, **kwargs):
|
||||
self.object = context
|
||||
@ -28,55 +26,47 @@ class DiscussionEvent(object):
|
||||
# Add event to the request to be able to access comment attributes
|
||||
# in content-rules dynamic strings
|
||||
request = context.REQUEST
|
||||
request.set('event', self)
|
||||
request.set("event", self)
|
||||
|
||||
|
||||
@implementer(ICommentAddedEvent)
|
||||
class CommentAddedEvent(DiscussionEvent):
|
||||
""" Event to be triggered when a Comment is added
|
||||
"""
|
||||
"""Event to be triggered when a Comment is added"""
|
||||
|
||||
|
||||
@implementer(ICommentModifiedEvent)
|
||||
class CommentModifiedEvent(DiscussionEvent):
|
||||
""" Event to be triggered when a Comment is modified
|
||||
"""
|
||||
"""Event to be triggered when a Comment is modified"""
|
||||
|
||||
|
||||
@implementer(ICommentRemovedEvent)
|
||||
class CommentRemovedEvent(DiscussionEvent):
|
||||
""" Event to be triggered when a Comment is removed
|
||||
"""
|
||||
"""Event to be triggered when a Comment is removed"""
|
||||
|
||||
|
||||
@implementer(IReplyAddedEvent)
|
||||
class ReplyAddedEvent(DiscussionEvent):
|
||||
""" Event to be triggered when a Comment reply is added
|
||||
"""
|
||||
"""Event to be triggered when a Comment reply is added"""
|
||||
|
||||
|
||||
@implementer(IReplyModifiedEvent)
|
||||
class ReplyModifiedEvent(DiscussionEvent):
|
||||
""" Event to be triggered when a Comment reply is modified
|
||||
"""
|
||||
"""Event to be triggered when a Comment reply is modified"""
|
||||
|
||||
|
||||
@implementer(IReplyRemovedEvent)
|
||||
class ReplyRemovedEvent(DiscussionEvent):
|
||||
""" Event to be triggered when a Comment reply is removed
|
||||
"""
|
||||
"""Event to be triggered when a Comment reply is removed"""
|
||||
|
||||
|
||||
@implementer(ICommentDeletedEvent)
|
||||
class CommentDeletedEvent(DiscussionEvent):
|
||||
""" Event to be triggered when a Comment is deleted
|
||||
"""
|
||||
"""Event to be triggered when a Comment is deleted"""
|
||||
|
||||
|
||||
@implementer(ICommentPublishedEvent)
|
||||
class CommentPublishedEvent(DiscussionEvent):
|
||||
""" Event to be triggered when a Comment is publicated
|
||||
"""
|
||||
"""Event to be triggered when a Comment is publicated"""
|
||||
|
||||
|
||||
@implementer(ICommentTransitionEvent)
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Interfaces for plone.app.discussion
|
||||
"""
|
||||
from plone.app.discussion import _
|
||||
@ -12,11 +11,14 @@ from zope.interface.common.mapping import IIterableMapping
|
||||
from zope.interface.interfaces import IObjectEvent
|
||||
|
||||
|
||||
DISCUSSION_ANNOTATION_KEY = "plone.app.discussion:conversation"
|
||||
|
||||
|
||||
def isEmail(value):
|
||||
portal = getUtility(ISiteRoot)
|
||||
reg_tool = getToolByName(portal, 'portal_registration')
|
||||
reg_tool = getToolByName(portal, "portal_registration")
|
||||
if not (value and reg_tool.isValidEmail(value)):
|
||||
raise Invalid(_('Invalid email address.'))
|
||||
raise Invalid(_("Invalid email address."))
|
||||
return True
|
||||
|
||||
|
||||
@ -42,25 +44,24 @@ class IConversation(IIterableMapping):
|
||||
"""
|
||||
|
||||
total_comments = schema.Int(
|
||||
title=_(u'Total number of public comments on this item'),
|
||||
title=_("Total number of public comments on this item"),
|
||||
min=0,
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
last_comment_date = schema.Date(
|
||||
title=_(u'Date of the most recent public comment'),
|
||||
title=_("Date of the most recent public comment"),
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
commentators = schema.Set(
|
||||
title=_(u'The set of unique commentators (usernames)'),
|
||||
title=_("The set of unique commentators (usernames)"),
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
public_commentators = schema.Set(
|
||||
title=_(
|
||||
u'The set of unique commentators (usernames) '
|
||||
u'of published_comments',
|
||||
"The set of unique commentators (usernames) " "of published_comments",
|
||||
),
|
||||
readonly=True,
|
||||
)
|
||||
@ -72,8 +73,7 @@ class IConversation(IIterableMapping):
|
||||
"""
|
||||
|
||||
def __delitem__(key):
|
||||
"""Delete the comment with the given key. The key is a long id.
|
||||
"""
|
||||
"""Delete the comment with the given key. The key is a long id."""
|
||||
|
||||
def getComments(start=0, size=None):
|
||||
"""Return an iterator of comment objects for rendering.
|
||||
@ -130,8 +130,7 @@ class IReplies(IIterableMapping):
|
||||
"""
|
||||
|
||||
def __delitem__(key):
|
||||
"""Delete the comment with the given key. The key is a long id.
|
||||
"""
|
||||
"""Delete the comment with the given key. The key is a long id."""
|
||||
|
||||
|
||||
class IComment(Interface):
|
||||
@ -141,61 +140,58 @@ class IComment(Interface):
|
||||
"""
|
||||
|
||||
portal_type = schema.ASCIILine(
|
||||
title=_(u'Portal type'),
|
||||
default='Discussion Item',
|
||||
title=_("Portal type"),
|
||||
default="Discussion Item",
|
||||
)
|
||||
|
||||
__parent__ = schema.Object(
|
||||
title=_(u'Conversation'), schema=Interface)
|
||||
__parent__ = schema.Object(title=_("Conversation"), schema=Interface)
|
||||
|
||||
__name__ = schema.TextLine(title=_(u'Name'))
|
||||
__name__ = schema.TextLine(title=_("Name"))
|
||||
|
||||
comment_id = schema.Int(
|
||||
title=_(u'A comment id unique to this conversation'))
|
||||
comment_id = schema.Int(title=_("A comment id unique to this conversation"))
|
||||
|
||||
in_reply_to = schema.Int(
|
||||
title=_(u'Id of comment this comment is in reply to'),
|
||||
title=_("Id of comment this comment is in reply to"),
|
||||
required=False,
|
||||
)
|
||||
|
||||
# for logged in comments - set to None for anonymous
|
||||
author_username = schema.TextLine(title=_(u'Name'), required=False)
|
||||
author_username = schema.TextLine(title=_("Name"), required=False)
|
||||
|
||||
# for anonymous comments only, set to None for logged in comments
|
||||
author_name = schema.TextLine(title=_(u'Name'), required=False)
|
||||
author_email = schema.TextLine(title=_(u'Email'),
|
||||
required=False,
|
||||
constraint=isEmail,
|
||||
)
|
||||
author_name = schema.TextLine(title=_("Name"), required=False)
|
||||
author_email = schema.TextLine(
|
||||
title=_("Email"),
|
||||
required=False,
|
||||
constraint=isEmail,
|
||||
)
|
||||
|
||||
title = schema.TextLine(title=_(u'label_subject',
|
||||
default=u'Subject'))
|
||||
title = schema.TextLine(title=_("label_subject", default="Subject"))
|
||||
|
||||
mime_type = schema.ASCIILine(title=_(u'MIME type'), default='text/plain')
|
||||
mime_type = schema.ASCIILine(title=_("MIME type"), default="text/plain")
|
||||
text = schema.Text(
|
||||
title=_(
|
||||
u'label_comment',
|
||||
default=u'Comment',
|
||||
"label_comment",
|
||||
default="Comment",
|
||||
),
|
||||
)
|
||||
|
||||
user_notification = schema.Bool(
|
||||
title=_(
|
||||
u'Notify me of new comments via email.',
|
||||
"Notify me of new comments via email.",
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
creator = schema.TextLine(title=_(u'Username of the commenter'))
|
||||
creation_date = schema.Date(title=_(u'Creation date'))
|
||||
modification_date = schema.Date(title=_(u'Modification date'))
|
||||
creator = schema.TextLine(title=_("Username of the commenter"))
|
||||
creation_date = schema.Date(title=_("Creation date"))
|
||||
modification_date = schema.Date(title=_("Modification date"))
|
||||
|
||||
|
||||
class ICaptcha(Interface):
|
||||
"""Captcha/ReCaptcha text field to extend the existing comment form.
|
||||
"""
|
||||
captcha = schema.TextLine(title=_(u'Captcha'),
|
||||
required=False)
|
||||
"""Captcha/ReCaptcha text field to extend the existing comment form."""
|
||||
|
||||
captcha = schema.TextLine(title=_("Captcha"), required=False)
|
||||
|
||||
|
||||
class IDiscussionSettings(Interface):
|
||||
@ -210,40 +206,38 @@ class IDiscussionSettings(Interface):
|
||||
# - Search control panel: Show comments in search results
|
||||
|
||||
globally_enabled = schema.Bool(
|
||||
title=_(u'label_globally_enabled',
|
||||
default=u'Globally enable comments'),
|
||||
title=_("label_globally_enabled", default="Globally enable comments"),
|
||||
description=_(
|
||||
u'help_globally_enabled',
|
||||
default=u'If selected, users are able to post comments on the '
|
||||
u'site. However, you will still need to enable comments '
|
||||
u'for specific content types, folders or content '
|
||||
u'objects before users will be able to post comments.',
|
||||
"help_globally_enabled",
|
||||
default="If selected, users are able to post comments on the "
|
||||
"site. However, you will still need to enable comments "
|
||||
"for specific content types, folders or content "
|
||||
"objects before users will be able to post comments.",
|
||||
),
|
||||
required=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
anonymous_comments = schema.Bool(
|
||||
title=_(u'label_anonymous_comments',
|
||||
default='Enable anonymous comments'),
|
||||
title=_("label_anonymous_comments", default="Enable anonymous comments"),
|
||||
description=_(
|
||||
u'help_anonymous_comments',
|
||||
default=u'If selected, anonymous users are able to post '
|
||||
u'comments without logging in. It is highly '
|
||||
u'recommended to use a captcha solution to prevent '
|
||||
u'spam if this setting is enabled.',
|
||||
"help_anonymous_comments",
|
||||
default="If selected, anonymous users are able to post "
|
||||
"comments without logging in. It is highly "
|
||||
"recommended to use a captcha solution to prevent "
|
||||
"spam if this setting is enabled.",
|
||||
),
|
||||
required=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
anonymous_email_enabled = schema.Bool(
|
||||
title=_(u'label_anonymous_email_enabled',
|
||||
default=u'Enable anonymous email field'),
|
||||
title=_(
|
||||
"label_anonymous_email_enabled", default="Enable anonymous email field"
|
||||
),
|
||||
description=_(
|
||||
u'help_anonymous_email_enabled',
|
||||
default=u'If selected, anonymous user will have to '
|
||||
u'give their email.',
|
||||
"help_anonymous_email_enabled",
|
||||
default="If selected, anonymous user will have to " "give their email.",
|
||||
),
|
||||
required=False,
|
||||
default=False,
|
||||
@ -251,130 +245,137 @@ class IDiscussionSettings(Interface):
|
||||
|
||||
moderation_enabled = schema.Bool(
|
||||
title=_(
|
||||
u'label_moderation_enabled',
|
||||
default='Enable comment moderation',
|
||||
"label_moderation_enabled",
|
||||
default="Enable comment moderation",
|
||||
),
|
||||
description=_(
|
||||
u'help_moderation_enabled',
|
||||
default=u'If selected, comments will enter a "Pending" state '
|
||||
u'in which they are invisible to the public. A user '
|
||||
u'with the "Review comments" permission ("Reviewer" '
|
||||
u'or "Manager") can approve comments to make them '
|
||||
u'visible to the public. If you want to enable a '
|
||||
u'custom comment workflow, you have to go to the '
|
||||
u'types control panel.',
|
||||
"help_moderation_enabled",
|
||||
default='If selected, comments will enter a "Pending" state '
|
||||
"in which they are invisible to the public. A user "
|
||||
'with the "Review comments" permission ("Reviewer" '
|
||||
'or "Manager") can approve comments to make them '
|
||||
"visible to the public. If you want to enable a "
|
||||
"custom comment workflow, you have to go to the "
|
||||
"types control panel.",
|
||||
),
|
||||
required=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
edit_comment_enabled = schema.Bool(
|
||||
title=_(u'label_edit_comment_enabled',
|
||||
default='Enable editing of comments'),
|
||||
description=_(u'help_edit_comment_enabled',
|
||||
default=u'If selected, supports editing '
|
||||
'of comments for users with the "Edit comments" '
|
||||
'permission.'),
|
||||
title=_("label_edit_comment_enabled", default="Enable editing of comments"),
|
||||
description=_(
|
||||
"help_edit_comment_enabled",
|
||||
default="If selected, supports editing "
|
||||
'of comments for users with the "Edit comments" '
|
||||
"permission.",
|
||||
),
|
||||
required=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
delete_own_comment_enabled = schema.Bool(
|
||||
title=_(u'label_delete_own_comment_enabled',
|
||||
default='Enable deleting own comments'),
|
||||
description=_(u'help_delete_own_comment_enabled',
|
||||
default=u'If selected, supports deleting '
|
||||
'of own comments for users with the '
|
||||
'"Delete own comments" permission.'),
|
||||
title=_(
|
||||
"label_delete_own_comment_enabled", default="Enable deleting own comments"
|
||||
),
|
||||
description=_(
|
||||
"help_delete_own_comment_enabled",
|
||||
default="If selected, supports deleting "
|
||||
"of own comments for users with the "
|
||||
'"Delete own comments" permission.',
|
||||
),
|
||||
required=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
text_transform = schema.Choice(
|
||||
title=_(u'label_text_transform',
|
||||
default='Comment text transform'),
|
||||
title=_("label_text_transform", default="Comment text transform"),
|
||||
description=_(
|
||||
u'help_text_transform',
|
||||
default=u'Use this setting to choose if the comment text '
|
||||
u'should be transformed in any way. You can choose '
|
||||
u'between "Plain text" and "Intelligent text". '
|
||||
u'"Intelligent text" converts plain text into HTML '
|
||||
u'where line breaks and indentation is preserved, '
|
||||
u'and web and email addresses are made into '
|
||||
u'clickable links.'),
|
||||
"help_text_transform",
|
||||
default="Use this setting to choose if the comment text "
|
||||
"should be transformed in any way. You can choose "
|
||||
'between "Plain text" and "Intelligent text". '
|
||||
'"Intelligent text" converts plain text into HTML '
|
||||
"where line breaks and indentation is preserved, "
|
||||
"and web and email addresses are made into "
|
||||
"clickable links.",
|
||||
),
|
||||
required=True,
|
||||
default='text/plain',
|
||||
vocabulary='plone.app.discussion.vocabularies.TextTransformVocabulary',
|
||||
default="text/plain",
|
||||
vocabulary="plone.app.discussion.vocabularies.TextTransformVocabulary",
|
||||
)
|
||||
|
||||
captcha = schema.Choice(
|
||||
title=_(u'label_captcha',
|
||||
default='Captcha'),
|
||||
title=_("label_captcha", default="Captcha"),
|
||||
description=_(
|
||||
u'help_captcha',
|
||||
default=u'Use this setting to enable or disable Captcha '
|
||||
u'validation for comments. Install '
|
||||
u'plone.formwidget.captcha, '
|
||||
u'plone.formwidget.recaptcha, collective.akismet, or '
|
||||
u'collective.z3cform.norobots if there are no options '
|
||||
u'available.'),
|
||||
"help_captcha",
|
||||
default="Use this setting to enable or disable Captcha "
|
||||
"validation for comments. Install "
|
||||
"plone.formwidget.captcha, "
|
||||
"plone.formwidget.recaptcha, collective.akismet, or "
|
||||
"collective.z3cform.norobots if there are no options "
|
||||
"available.",
|
||||
),
|
||||
required=True,
|
||||
default='disabled',
|
||||
vocabulary='plone.app.discussion.vocabularies.CaptchaVocabulary',
|
||||
default="disabled",
|
||||
vocabulary="plone.app.discussion.vocabularies.CaptchaVocabulary",
|
||||
)
|
||||
|
||||
show_commenter_image = schema.Bool(
|
||||
title=_(u'label_show_commenter_image',
|
||||
default=u'Show commenter image'),
|
||||
title=_("label_show_commenter_image", default="Show commenter image"),
|
||||
description=_(
|
||||
u'help_show_commenter_image',
|
||||
default=u'If selected, an image of the user is shown next to '
|
||||
u'the comment.'),
|
||||
"help_show_commenter_image",
|
||||
default="If selected, an image of the user is shown next to "
|
||||
"the comment.",
|
||||
),
|
||||
required=False,
|
||||
default=True,
|
||||
)
|
||||
|
||||
moderator_notification_enabled = schema.Bool(
|
||||
title=_(u'label_moderator_notification_enabled',
|
||||
default=u'Enable moderator email notification'),
|
||||
title=_(
|
||||
"label_moderator_notification_enabled",
|
||||
default="Enable moderator email notification",
|
||||
),
|
||||
description=_(
|
||||
u'help_moderator_notification_enabled',
|
||||
default=u'If selected, the moderator is notified if a comment '
|
||||
u'needs attention. The moderator email address can '
|
||||
u'be set below.'),
|
||||
"help_moderator_notification_enabled",
|
||||
default="If selected, the moderator is notified if a comment "
|
||||
"needs attention. The moderator email address can "
|
||||
"be set below.",
|
||||
),
|
||||
required=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
moderator_email = schema.ASCIILine(
|
||||
title=_(
|
||||
u'label_moderator_email',
|
||||
default=u'Moderator Email Address',
|
||||
"label_moderator_email",
|
||||
default="Moderator Email Address",
|
||||
),
|
||||
description=_(
|
||||
u'help_moderator_email',
|
||||
default=u'Address to which moderator notifications '
|
||||
u'will be sent.'),
|
||||
"help_moderator_email",
|
||||
default="Address to which moderator notifications " "will be sent.",
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
user_notification_enabled = schema.Bool(
|
||||
title=_(
|
||||
u'label_user_notification_enabled',
|
||||
default=u'Enable user email notification',
|
||||
"label_user_notification_enabled",
|
||||
default="Enable user email notification",
|
||||
),
|
||||
description=_(
|
||||
u'help_user_notification_enabled',
|
||||
default=u'If selected, users can choose to be notified '
|
||||
u'of new comments by email.'),
|
||||
"help_user_notification_enabled",
|
||||
default="If selected, users can choose to be notified "
|
||||
"of new comments by email.",
|
||||
),
|
||||
required=False,
|
||||
default=False,
|
||||
)
|
||||
|
||||
|
||||
class IDiscussionLayer(Interface):
|
||||
"""Request marker installed via browserlayer.xml.
|
||||
"""
|
||||
"""Request marker installed via browserlayer.xml."""
|
||||
|
||||
|
||||
class ICommentingTool(Interface):
|
||||
@ -384,54 +385,46 @@ class ICommentingTool(Interface):
|
||||
of Plone that had a portal_discussion tool.
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Custom events
|
||||
#
|
||||
|
||||
|
||||
class IDiscussionEvent(IObjectEvent):
|
||||
""" Discussion custom event
|
||||
"""
|
||||
"""Discussion custom event"""
|
||||
|
||||
|
||||
class ICommentAddedEvent(IDiscussionEvent):
|
||||
""" Comment added
|
||||
"""
|
||||
"""Comment added"""
|
||||
|
||||
|
||||
class ICommentModifiedEvent(IDiscussionEvent):
|
||||
""" Comment modified
|
||||
"""
|
||||
"""Comment modified"""
|
||||
|
||||
|
||||
class ICommentRemovedEvent(IDiscussionEvent):
|
||||
""" Comment removed
|
||||
"""
|
||||
"""Comment removed"""
|
||||
|
||||
|
||||
class IReplyAddedEvent(IDiscussionEvent):
|
||||
""" Comment reply added
|
||||
"""
|
||||
"""Comment reply added"""
|
||||
|
||||
|
||||
class IReplyModifiedEvent(IDiscussionEvent):
|
||||
""" Comment reply modified
|
||||
"""
|
||||
"""Comment reply modified"""
|
||||
|
||||
|
||||
class IReplyRemovedEvent(IDiscussionEvent):
|
||||
""" Comment reply removed
|
||||
"""
|
||||
"""Comment reply removed"""
|
||||
|
||||
|
||||
class ICommentPublishedEvent(IDiscussionEvent):
|
||||
""" Notify user on comment publication
|
||||
"""
|
||||
"""Notify user on comment publication"""
|
||||
|
||||
|
||||
class ICommentDeletedEvent(IDiscussionEvent):
|
||||
""" Notify user on comment delete
|
||||
"""
|
||||
"""Notify user on comment delete"""
|
||||
|
||||
|
||||
class ICommentTransitionEvent(IDiscussionEvent):
|
||||
|
@ -1,14 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
|
||||
|
||||
def index_object(obj, event):
|
||||
"""Index the object when it is added/modified to the conversation.
|
||||
"""
|
||||
"""Index the object when it is added/modified to the conversation."""
|
||||
obj.indexObject()
|
||||
|
||||
|
||||
def unindex_object(obj, event):
|
||||
"""Unindex the object when it is removed from the conversation.
|
||||
"""
|
||||
"""Unindex the object when it is removed from the conversation."""
|
||||
obj.unindexObject()
|
||||
|
@ -72,7 +72,7 @@
|
||||
<!-- Control panel event subscribers -->
|
||||
|
||||
<subscriber
|
||||
for="Products.CMFPlone.interfaces.events.IConfigurationChangedEvent"
|
||||
for="plone.base.interfaces.events.IConfigurationChangedEvent"
|
||||
handler=".browser.controlpanel.notify_configuration_changed"
|
||||
/>
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.app.robotframework.testing import REMOTE_LIBRARY_ROBOT_TESTING
|
||||
@ -15,40 +14,43 @@ from zope.component import queryUtility
|
||||
|
||||
try:
|
||||
import plone.app.collection # noqa
|
||||
COLLECTION_TYPE = 'Collection'
|
||||
|
||||
COLLECTION_TYPE = "Collection"
|
||||
except ImportError:
|
||||
COLLECTION_TYPE = 'Topic'
|
||||
COLLECTION_TYPE = "Topic"
|
||||
|
||||
|
||||
class PloneAppDiscussion(PloneSandboxLayer):
|
||||
|
||||
defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)
|
||||
|
||||
USER_NAME = 'johndoe'
|
||||
USER_PASSWORD = 'secret'
|
||||
MEMBER_NAME = 'janedoe'
|
||||
MEMBER_PASSWORD = 'secret'
|
||||
USER_WITH_FULLNAME_NAME = 'jim'
|
||||
USER_WITH_FULLNAME_FULLNAME = 'Jim Fulton'
|
||||
USER_WITH_FULLNAME_PASSWORD = 'secret'
|
||||
MANAGER_USER_NAME = 'manager'
|
||||
MANAGER_USER_PASSWORD = 'secret'
|
||||
REVIEWER_NAME = 'reviewer'
|
||||
REVIEWER_PASSWORD = 'secret'
|
||||
USER_NAME = "johndoe"
|
||||
USER_PASSWORD = "secret"
|
||||
MEMBER_NAME = "janedoe"
|
||||
MEMBER_PASSWORD = "secret"
|
||||
USER_WITH_FULLNAME_NAME = "jim"
|
||||
USER_WITH_FULLNAME_FULLNAME = "Jim Fulton"
|
||||
USER_WITH_FULLNAME_PASSWORD = "secret"
|
||||
MANAGER_USER_NAME = "manager"
|
||||
MANAGER_USER_PASSWORD = "secret"
|
||||
REVIEWER_NAME = "reviewer"
|
||||
REVIEWER_PASSWORD = "secret"
|
||||
|
||||
def setUpZope(self, app, configurationContext):
|
||||
# Load ZCML
|
||||
import plone.app.discussion
|
||||
self.loadZCML(package=plone.app.discussion,
|
||||
context=configurationContext,
|
||||
)
|
||||
|
||||
self.loadZCML(
|
||||
package=plone.app.discussion,
|
||||
context=configurationContext,
|
||||
)
|
||||
|
||||
def setUpPloneSite(self, portal):
|
||||
# Install into Plone site using portal_setup
|
||||
applyProfile(portal, 'plone.app.discussion:default')
|
||||
applyProfile(portal, "plone.app.discussion:default")
|
||||
|
||||
# Creates some users
|
||||
acl_users = getToolByName(portal, 'acl_users')
|
||||
acl_users = getToolByName(portal, "acl_users")
|
||||
acl_users.userFolderAddUser(
|
||||
self.USER_NAME,
|
||||
self.USER_PASSWORD,
|
||||
@ -58,41 +60,42 @@ class PloneAppDiscussion(PloneSandboxLayer):
|
||||
acl_users.userFolderAddUser(
|
||||
self.MEMBER_NAME,
|
||||
self.MEMBER_PASSWORD,
|
||||
['Member'],
|
||||
["Member"],
|
||||
[],
|
||||
)
|
||||
acl_users.userFolderAddUser(
|
||||
self.USER_WITH_FULLNAME_NAME,
|
||||
self.USER_WITH_FULLNAME_PASSWORD,
|
||||
['Member'],
|
||||
["Member"],
|
||||
[],
|
||||
)
|
||||
acl_users.userFolderAddUser(
|
||||
self.REVIEWER_NAME,
|
||||
self.REVIEWER_PASSWORD,
|
||||
['Member'],
|
||||
["Member"],
|
||||
[],
|
||||
)
|
||||
mtool = getToolByName(portal, 'portal_membership', None)
|
||||
gtool = getToolByName(portal, 'portal_groups', None)
|
||||
gtool.addPrincipalToGroup(self.REVIEWER_NAME, 'Reviewers')
|
||||
mtool.addMember('jim', 'Jim', ['Member'], [])
|
||||
mtool.getMemberById('jim').setMemberProperties(
|
||||
{'fullname': 'Jim Fult\xc3\xb8rn'})
|
||||
mtool = getToolByName(portal, "portal_membership", None)
|
||||
gtool = getToolByName(portal, "portal_groups", None)
|
||||
gtool.addPrincipalToGroup(self.REVIEWER_NAME, "Reviewers")
|
||||
mtool.addMember("jim", "Jim", ["Member"], [])
|
||||
mtool.getMemberById("jim").setMemberProperties(
|
||||
{"fullname": "Jim Fult\xc3\xb8rn"}
|
||||
)
|
||||
|
||||
acl_users.userFolderAddUser(
|
||||
self.MANAGER_USER_NAME,
|
||||
self.MANAGER_USER_PASSWORD,
|
||||
['Manager'],
|
||||
["Manager"],
|
||||
[],
|
||||
)
|
||||
|
||||
# Add a document
|
||||
setRoles(portal, TEST_USER_ID, ['Manager'])
|
||||
setRoles(portal, TEST_USER_ID, ["Manager"])
|
||||
portal.invokeFactory(
|
||||
id='doc1',
|
||||
title='Document 1',
|
||||
type_name='Document',
|
||||
id="doc1",
|
||||
title="Document 1",
|
||||
type_name="Document",
|
||||
)
|
||||
|
||||
|
||||
@ -112,12 +115,12 @@ class PloneAppDiscussionRobot(PloneAppDiscussion):
|
||||
PLONE_APP_DISCUSSION_ROBOT_FIXTURE = PloneAppDiscussionRobot()
|
||||
PLONE_APP_DISCUSSION_FIXTURE = PloneAppDiscussion()
|
||||
PLONE_APP_DISCUSSION_INTEGRATION_TESTING = IntegrationTesting(
|
||||
bases=(PLONE_APP_DISCUSSION_FIXTURE,),
|
||||
name='PloneAppDiscussion:Integration')
|
||||
bases=(PLONE_APP_DISCUSSION_FIXTURE,), name="PloneAppDiscussion:Integration"
|
||||
)
|
||||
PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING = FunctionalTesting(
|
||||
bases=(PLONE_APP_DISCUSSION_FIXTURE,),
|
||||
name='PloneAppDiscussion:Functional')
|
||||
bases=(PLONE_APP_DISCUSSION_FIXTURE,), name="PloneAppDiscussion:Functional"
|
||||
)
|
||||
PLONE_APP_DISCUSSION_ROBOT_TESTING = FunctionalTesting(
|
||||
bases=(PLONE_APP_DISCUSSION_ROBOT_FIXTURE,),
|
||||
name='PloneAppDiscussion:Robot',
|
||||
name="PloneAppDiscussion:Robot",
|
||||
)
|
||||
|
@ -252,7 +252,7 @@ Now we can post an anonymous comment.
|
||||
|
||||
>>> unprivileged_browser.open(urldoc)
|
||||
>>> unprivileged_browser.getControl(name='form.widgets.text').value = "This is an anonymous comment"
|
||||
>>> unprivileged_browser.getControl(name='form.widgets.author_name').value = u'John'
|
||||
>>> unprivileged_browser.getControl(name='form.widgets.author_name').value = 'John'
|
||||
>>> unprivileged_browser.getControl(name='form.widgets.author_email').value = 'john@acme.com'
|
||||
>>> unprivileged_browser.getControl(name='form.buttons.comment').click()
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test the plone.app.discussion catalog indexes
|
||||
"""
|
||||
from datetime import datetime
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from plone.app.discussion.testing import ( # noqa
|
||||
PLONE_APP_DISCUSSION_INTEGRATION_TESTING,
|
||||
)
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
@ -21,33 +22,29 @@ class CatalogSetupTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
self.portal = self.layer["portal"]
|
||||
|
||||
def test_catalog_installed(self):
|
||||
self.assertTrue(
|
||||
'total_comments' in
|
||||
self.portal.portal_catalog.indexes(),
|
||||
"total_comments" in self.portal.portal_catalog.indexes(),
|
||||
)
|
||||
self.assertTrue(
|
||||
'commentators' in
|
||||
self.portal.portal_catalog.indexes(),
|
||||
"commentators" in self.portal.portal_catalog.indexes(),
|
||||
)
|
||||
self.assertTrue(
|
||||
'total_comments' in
|
||||
self.portal.portal_catalog.schema(),
|
||||
"total_comments" in self.portal.portal_catalog.schema(),
|
||||
)
|
||||
self.assertTrue(
|
||||
'in_response_to' in
|
||||
self.portal.portal_catalog.schema(),
|
||||
"in_response_to" in self.portal.portal_catalog.schema(),
|
||||
)
|
||||
|
||||
def test_collection_criteria_installed(self):
|
||||
if 'portal_atct' not in self.portal:
|
||||
if "portal_atct" not in self.portal:
|
||||
return
|
||||
try:
|
||||
self.portal.portal_atct.getIndex('commentators')
|
||||
self.portal.portal_atct.getIndex('total_comments')
|
||||
self.portal.portal_atct.getMetadata('total_comments')
|
||||
self.portal.portal_atct.getIndex("commentators")
|
||||
self.portal.portal_atct.getIndex("total_comments")
|
||||
self.portal.portal_atct.getMetadata("total_comments")
|
||||
except AttributeError:
|
||||
self.fail()
|
||||
|
||||
@ -57,19 +54,19 @@ class ConversationCatalogTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
|
||||
workflow = self.portal.portal_workflow
|
||||
workflow.doActionFor(self.portal.doc1, 'publish')
|
||||
workflow.doActionFor(self.portal.doc1, "publish")
|
||||
|
||||
self.catalog = getToolByName(self.portal, 'portal_catalog')
|
||||
self.catalog = getToolByName(self.portal, "portal_catalog")
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.title = 'Comment 1'
|
||||
comment1.text = 'Comment text'
|
||||
comment1.creator = 'jim'
|
||||
comment1.author_username = 'Jim'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.title = "Comment 1"
|
||||
comment1.text = "Comment text"
|
||||
comment1.creator = "jim"
|
||||
comment1.author_username = "Jim"
|
||||
comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12)
|
||||
comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12)
|
||||
|
||||
@ -79,9 +76,9 @@ class ConversationCatalogTest(unittest.TestCase):
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
self.conversation = conversation
|
||||
@ -91,54 +88,54 @@ class ConversationCatalogTest(unittest.TestCase):
|
||||
self.new_comment1_id = new_comment1_id
|
||||
|
||||
def test_total_comments(self):
|
||||
self.assertTrue('total_comments' in self.doc1_brain)
|
||||
self.assertTrue("total_comments" in self.doc1_brain)
|
||||
self.assertEqual(self.doc1_brain.total_comments, 1)
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.title = 'Comment 2'
|
||||
comment2.text = 'Comment text'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.title = "Comment 2"
|
||||
comment2.text = "Comment text"
|
||||
new_comment2_id = self.conversation.addComment(comment2)
|
||||
|
||||
comment2 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_comment2_id),
|
||||
f"++conversation++default/{new_comment2_id}",
|
||||
)
|
||||
comment2.reindexObject()
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
doc1_brain = brains[0]
|
||||
self.assertEqual(doc1_brain.total_comments, 2)
|
||||
|
||||
def test_last_comment_date(self):
|
||||
self.assertTrue('last_comment_date' in self.doc1_brain)
|
||||
self.assertTrue("last_comment_date" in self.doc1_brain)
|
||||
self.assertEqual(
|
||||
self.doc1_brain.last_comment_date,
|
||||
datetime(2006, 9, 17, 14, 18, 12),
|
||||
)
|
||||
|
||||
# Add another comment and check if last comment date is updated.
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.title = 'Comment 2'
|
||||
comment2.text = 'Comment text'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.title = "Comment 2"
|
||||
comment2.text = "Comment text"
|
||||
comment2.creation_date = datetime(2009, 9, 17, 14, 18, 12)
|
||||
comment2.modification_date = datetime(2009, 9, 17, 14, 18, 12)
|
||||
new_comment2_id = self.conversation.addComment(comment2)
|
||||
|
||||
comment2 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_comment2_id),
|
||||
f"++conversation++default/{new_comment2_id}",
|
||||
)
|
||||
comment2.reindexObject()
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
doc1_brain = brains[0]
|
||||
@ -153,9 +150,9 @@ class ConversationCatalogTest(unittest.TestCase):
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
doc1_brain = brains[0]
|
||||
@ -169,44 +166,44 @@ class ConversationCatalogTest(unittest.TestCase):
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
doc1_brain = brains[0]
|
||||
self.assertEqual(doc1_brain.last_comment_date, None)
|
||||
|
||||
def test_commentators(self):
|
||||
self.assertTrue('commentators' in self.doc1_brain)
|
||||
self.assertEqual(self.doc1_brain.commentators, ('Jim',))
|
||||
self.assertTrue("commentators" in self.doc1_brain)
|
||||
self.assertEqual(self.doc1_brain.commentators, ("Jim",))
|
||||
|
||||
# add another comment with another author
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.title = 'Comment 2'
|
||||
comment2.text = 'Comment text'
|
||||
comment2.creator = 'emma'
|
||||
comment2.author_username = 'Emma'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.title = "Comment 2"
|
||||
comment2.text = "Comment text"
|
||||
comment2.creator = "emma"
|
||||
comment2.author_username = "Emma"
|
||||
new_comment2_id = self.conversation.addComment(comment2)
|
||||
|
||||
comment2 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_comment2_id),
|
||||
f"++conversation++default/{new_comment2_id}",
|
||||
)
|
||||
comment2.reindexObject()
|
||||
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
doc1_brain = brains[0]
|
||||
|
||||
self.assertEqual(
|
||||
sorted(doc1_brain.commentators),
|
||||
sorted(('Jim', 'Emma')),
|
||||
sorted(("Jim", "Emma")),
|
||||
)
|
||||
|
||||
# remove one comments
|
||||
@ -214,22 +211,22 @@ class ConversationCatalogTest(unittest.TestCase):
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
doc1_brain = brains[0]
|
||||
self.assertEqual(doc1_brain.commentators, ('Jim',))
|
||||
self.assertEqual(doc1_brain.commentators, ("Jim",))
|
||||
|
||||
# remove all comments
|
||||
del self.conversation[self.new_comment1_id]
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
doc1_brain = brains[0]
|
||||
@ -239,9 +236,9 @@ class ConversationCatalogTest(unittest.TestCase):
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Discussion Item',
|
||||
portal_type="Discussion Item",
|
||||
),
|
||||
)
|
||||
comment1_brain = brains[0]
|
||||
@ -250,14 +247,14 @@ class ConversationCatalogTest(unittest.TestCase):
|
||||
self.assertEqual(comment1_brain.total_comments, None)
|
||||
|
||||
def test_dont_index_private_commentators(self):
|
||||
self.comment1.manage_permission('View', roles=tuple())
|
||||
self.comment1.manage_permission("View", roles=tuple())
|
||||
self.portal.doc1.reindexObject()
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
doc1_brain = brains[0]
|
||||
@ -269,70 +266,70 @@ class CommentCatalogTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.catalog = getToolByName(self.portal, 'portal_catalog')
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.catalog = getToolByName(self.portal, "portal_catalog")
|
||||
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
self.conversation = conversation
|
||||
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1.creator = 'jim'
|
||||
comment1.author_name = 'Jim'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
comment1.creator = "jim"
|
||||
comment1.author_name = "Jim"
|
||||
new_comment1_id = conversation.addComment(comment1)
|
||||
self.comment_id = new_comment1_id
|
||||
|
||||
# Comment brain
|
||||
self.comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_comment1_id),
|
||||
f"++conversation++default/{new_comment1_id}",
|
||||
)
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.comment.getPhysicalPath()),
|
||||
"query": "/".join(self.comment.getPhysicalPath()),
|
||||
},
|
||||
),
|
||||
)
|
||||
self.comment_brain = brains[0]
|
||||
|
||||
def test_title(self):
|
||||
self.assertEqual(self.comment_brain.Title, 'Jim on Document 1')
|
||||
self.assertEqual(self.comment_brain.Title, "Jim on Document 1")
|
||||
|
||||
def test_no_name_title(self):
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
cid = self.conversation.addComment(comment)
|
||||
|
||||
# Comment brain
|
||||
comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(cid),
|
||||
f"++conversation++default/{cid}",
|
||||
)
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(comment.getPhysicalPath()),
|
||||
"query": "/".join(comment.getPhysicalPath()),
|
||||
},
|
||||
),
|
||||
)
|
||||
comment_brain = brains[0]
|
||||
self.assertEqual(comment_brain.Title, 'Anonymous on Document 1')
|
||||
self.assertEqual(comment_brain.Title, "Anonymous on Document 1")
|
||||
|
||||
def test_type(self):
|
||||
self.assertEqual(self.comment_brain.portal_type, 'Discussion Item')
|
||||
self.assertEqual(self.comment_brain.Type, 'Comment')
|
||||
self.assertEqual(self.comment_brain.portal_type, "Discussion Item")
|
||||
self.assertEqual(self.comment_brain.Type, "Comment")
|
||||
|
||||
def test_review_state(self):
|
||||
self.assertEqual(self.comment_brain.review_state, 'published')
|
||||
self.assertEqual(self.comment_brain.review_state, "published")
|
||||
|
||||
def test_creator(self):
|
||||
self.assertEqual(self.comment_brain.Creator, 'jim')
|
||||
self.assertEqual(self.comment_brain.Creator, "jim")
|
||||
|
||||
def test_in_response_to(self):
|
||||
"""Make sure in_response_to returns the title or id of the content
|
||||
object the comment was added to.
|
||||
object the comment was added to.
|
||||
"""
|
||||
self.assertEqual(self.comment_brain.in_response_to, 'Document 1')
|
||||
self.assertEqual(self.comment_brain.in_response_to, "Document 1")
|
||||
|
||||
def test_add_comment(self):
|
||||
self.assertTrue(self.comment_brain)
|
||||
@ -344,7 +341,7 @@ class CommentCatalogTest(unittest.TestCase):
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.comment.getPhysicalPath()),
|
||||
"query": "/".join(self.comment.getPhysicalPath()),
|
||||
},
|
||||
),
|
||||
)
|
||||
@ -352,54 +349,54 @@ class CommentCatalogTest(unittest.TestCase):
|
||||
|
||||
def test_reindex_comment(self):
|
||||
# Make sure a comment is reindexed on the catalog when is modified
|
||||
self.comment.text = 'Another text'
|
||||
self.comment.text = "Another text"
|
||||
notify(ObjectModifiedEvent(self.comment))
|
||||
brains = self.catalog.searchResults(SearchableText='Another text')
|
||||
brains = self.catalog.searchResults(SearchableText="Another text")
|
||||
self.assertEqual(len(brains), 1)
|
||||
|
||||
def test_remove_comments_when_content_object_is_removed(self):
|
||||
"""Make sure all comments are removed from the catalog, if the content
|
||||
object is removed.
|
||||
object is removed.
|
||||
"""
|
||||
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
|
||||
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
|
||||
self.assertEqual(len(brains), 1)
|
||||
self.portal.manage_delObjects(['doc1'])
|
||||
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
|
||||
self.portal.manage_delObjects(["doc1"])
|
||||
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
|
||||
self.assertEqual(len(brains), 0)
|
||||
|
||||
def test_move_comments_when_content_object_is_moved(self):
|
||||
# Create two folders and a content object with a comment
|
||||
self.portal.invokeFactory(
|
||||
id='folder1',
|
||||
title='Folder 1',
|
||||
type_name='Folder',
|
||||
id="folder1",
|
||||
title="Folder 1",
|
||||
type_name="Folder",
|
||||
)
|
||||
self.portal.invokeFactory(
|
||||
id='folder2',
|
||||
title='Folder 2',
|
||||
type_name='Folder',
|
||||
id="folder2",
|
||||
title="Folder 2",
|
||||
type_name="Folder",
|
||||
)
|
||||
self.portal.folder1.invokeFactory(
|
||||
id='moveme',
|
||||
title='Move Me',
|
||||
type_name='Document',
|
||||
id="moveme",
|
||||
title="Move Me",
|
||||
type_name="Document",
|
||||
)
|
||||
conversation = IConversation(self.portal.folder1.moveme)
|
||||
comment = createObject('plone.Comment')
|
||||
comment = createObject("plone.Comment")
|
||||
comment_id = conversation.addComment(comment)
|
||||
# We need to commit here so that _p_jar isn't None and move will work
|
||||
transaction.savepoint(optimistic=True)
|
||||
|
||||
# Move moveme from folder1 to folder2
|
||||
cp = self.portal.folder1.manage_cutObjects(ids=('moveme',))
|
||||
cp = self.portal.folder1.manage_cutObjects(ids=("moveme",))
|
||||
self.portal.folder2.manage_pasteObjects(cp)
|
||||
|
||||
# Make sure no old comment brains are
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
portal_type='Discussion Item',
|
||||
portal_type="Discussion Item",
|
||||
path={
|
||||
'query': '/'.join(self.portal.folder1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.folder1.getPhysicalPath()),
|
||||
},
|
||||
),
|
||||
)
|
||||
@ -407,61 +404,60 @@ class CommentCatalogTest(unittest.TestCase):
|
||||
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
portal_type='Discussion Item',
|
||||
portal_type="Discussion Item",
|
||||
path={
|
||||
'query': '/'.join(self.portal.folder2.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.folder2.getPhysicalPath()),
|
||||
},
|
||||
),
|
||||
)
|
||||
self.assertEqual(len(brains), 1)
|
||||
self.assertEqual(
|
||||
brains[0].getPath(),
|
||||
'/plone/folder2/moveme/++conversation++default/' +
|
||||
str(comment_id),
|
||||
"/plone/folder2/moveme/++conversation++default/" + str(comment_id),
|
||||
)
|
||||
|
||||
def test_move_upper_level_folder(self):
|
||||
# create a folder with a nested structure
|
||||
self.portal.invokeFactory(
|
||||
id='sourcefolder',
|
||||
title='Source Folder',
|
||||
type_name='Folder',
|
||||
id="sourcefolder",
|
||||
title="Source Folder",
|
||||
type_name="Folder",
|
||||
)
|
||||
self.portal.sourcefolder.invokeFactory(
|
||||
id='moveme',
|
||||
title='Move Me',
|
||||
type_name='Folder',
|
||||
id="moveme",
|
||||
title="Move Me",
|
||||
type_name="Folder",
|
||||
)
|
||||
self.portal.sourcefolder.moveme.invokeFactory(
|
||||
id='mydocument',
|
||||
title='My Document',
|
||||
type_name='Folder',
|
||||
id="mydocument",
|
||||
title="My Document",
|
||||
type_name="Folder",
|
||||
)
|
||||
self.portal.invokeFactory(
|
||||
id='targetfolder',
|
||||
title='Target Folder',
|
||||
type_name='Folder',
|
||||
id="targetfolder",
|
||||
title="Target Folder",
|
||||
type_name="Folder",
|
||||
)
|
||||
|
||||
# create comment on my-document
|
||||
conversation = IConversation(
|
||||
self.portal.sourcefolder.moveme.mydocument,
|
||||
)
|
||||
comment = createObject('plone.Comment')
|
||||
comment = createObject("plone.Comment")
|
||||
comment_id = conversation.addComment(comment)
|
||||
|
||||
# We need to commit here so that _p_jar isn't None and move will work
|
||||
transaction.savepoint(optimistic=True)
|
||||
|
||||
# Move moveme from folder1 to folder2
|
||||
cp = self.portal.sourcefolder.manage_cutObjects(ids=('moveme',))
|
||||
cp = self.portal.sourcefolder.manage_cutObjects(ids=("moveme",))
|
||||
self.portal.targetfolder.manage_pasteObjects(cp)
|
||||
|
||||
# Make sure no old comment brains are left
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
portal_type='Discussion Item',
|
||||
path={'query': '/plone/sourcefolder/moveme'},
|
||||
portal_type="Discussion Item",
|
||||
path={"query": "/plone/sourcefolder/moveme"},
|
||||
),
|
||||
)
|
||||
self.assertEqual(len(brains), 0)
|
||||
@ -469,49 +465,47 @@ class CommentCatalogTest(unittest.TestCase):
|
||||
# make sure comments are correctly index on the target
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
portal_type='Discussion Item',
|
||||
path={'query': '/plone/targetfolder/moveme'},
|
||||
portal_type="Discussion Item",
|
||||
path={"query": "/plone/targetfolder/moveme"},
|
||||
),
|
||||
)
|
||||
self.assertEqual(len(brains), 1)
|
||||
self.assertEqual(
|
||||
brains[0].getPath(),
|
||||
'/plone/targetfolder/moveme/mydocument/++conversation++default/' +
|
||||
str(comment_id),
|
||||
"/plone/targetfolder/moveme/mydocument/++conversation++default/"
|
||||
+ str(comment_id),
|
||||
)
|
||||
|
||||
def test_update_comments_when_content_object_is_renamed(self):
|
||||
# We need to commit here so that _p_jar isn't None and move will work
|
||||
transaction.savepoint(optimistic=True)
|
||||
|
||||
self.portal.manage_renameObject('doc1', 'doc2')
|
||||
self.portal.manage_renameObject("doc1", "doc2")
|
||||
|
||||
brains = self.catalog.searchResults(
|
||||
portal_type='Discussion Item',
|
||||
portal_type="Discussion Item",
|
||||
)
|
||||
self.assertEqual(len(brains), 1)
|
||||
self.assertEqual(
|
||||
brains[0].getPath(),
|
||||
'/plone/doc2/++conversation++default/' +
|
||||
str(self.comment_id),
|
||||
"/plone/doc2/++conversation++default/" + str(self.comment_id),
|
||||
)
|
||||
|
||||
def test_clear_and_rebuild_catalog(self):
|
||||
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
|
||||
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
|
||||
self.assertTrue(brains)
|
||||
|
||||
# Clear and rebuild catalog
|
||||
self.catalog.clearFindAndRebuild()
|
||||
|
||||
# Check if comment is still there
|
||||
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
|
||||
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
|
||||
self.assertTrue(brains)
|
||||
comment_brain = brains[0]
|
||||
self.assertEqual(comment_brain.Title, u'Jim on Document 1')
|
||||
self.assertEqual(comment_brain.Title, "Jim on Document 1")
|
||||
self.assertEqual(
|
||||
comment_brain.getPath(),
|
||||
'/plone/doc1/++conversation++default/' +
|
||||
str(self.comment_id),
|
||||
"/plone/doc1/++conversation++default/" + str(self.comment_id),
|
||||
)
|
||||
|
||||
def test_clear_and_rebuild_catalog_for_nested_comments(self):
|
||||
@ -526,25 +520,25 @@ class CommentCatalogTest(unittest.TestCase):
|
||||
# +- Comment 2
|
||||
# +- Comment 2_1
|
||||
|
||||
comment1_1 = createObject('plone.Comment')
|
||||
comment1_1.title = 'Re: Comment 1'
|
||||
comment1_1.text = 'Comment text'
|
||||
comment1_1 = createObject("plone.Comment")
|
||||
comment1_1.title = "Re: Comment 1"
|
||||
comment1_1.text = "Comment text"
|
||||
|
||||
comment1_1_1 = createObject('plone.Comment')
|
||||
comment1_1_1.title = 'Re: Re: Comment 1'
|
||||
comment1_1_1.text = 'Comment text'
|
||||
comment1_1_1 = createObject("plone.Comment")
|
||||
comment1_1_1.title = "Re: Re: Comment 1"
|
||||
comment1_1_1.text = "Comment text"
|
||||
|
||||
comment1_2 = createObject('plone.Comment')
|
||||
comment1_2.title = 'Re: Comment 1 (2)'
|
||||
comment1_2.text = 'Comment text'
|
||||
comment1_2 = createObject("plone.Comment")
|
||||
comment1_2.title = "Re: Comment 1 (2)"
|
||||
comment1_2.text = "Comment text"
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.title = 'Comment 2'
|
||||
comment2.text = 'Comment text'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.title = "Comment 2"
|
||||
comment2.text = "Comment text"
|
||||
|
||||
comment2_1 = createObject('plone.Comment')
|
||||
comment2_1.title = 'Re: Comment 2'
|
||||
comment2_1.text = 'Comment text'
|
||||
comment2_1 = createObject("plone.Comment")
|
||||
comment2_1.title = "Re: Comment 2"
|
||||
comment2_1.text = "Comment text"
|
||||
|
||||
# Create the nested comment structure
|
||||
new_id_1 = self.conversation.addComment(self.comment)
|
||||
@ -566,7 +560,7 @@ class CommentCatalogTest(unittest.TestCase):
|
||||
self.catalog.clearFindAndRebuild()
|
||||
|
||||
# Check if comments are still there
|
||||
brains = self.catalog.searchResults({'portal_type': 'Discussion Item'})
|
||||
brains = self.catalog.searchResults({"portal_type": "Discussion Item"})
|
||||
self.assertTrue(brains)
|
||||
self.assertEqual(len(brains), 6)
|
||||
|
||||
@ -576,19 +570,19 @@ class NoConversationCatalogTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
|
||||
self.catalog = getToolByName(self.portal, 'portal_catalog')
|
||||
self.catalog = getToolByName(self.portal, "portal_catalog")
|
||||
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
|
||||
brains = self.catalog.searchResults(
|
||||
dict(
|
||||
path={
|
||||
'query': '/'.join(self.portal.doc1.getPhysicalPath()),
|
||||
"query": "/".join(self.portal.doc1.getPhysicalPath()),
|
||||
},
|
||||
portal_type='Document',
|
||||
portal_type="Document",
|
||||
),
|
||||
)
|
||||
self.conversation = conversation
|
||||
@ -596,11 +590,10 @@ class NoConversationCatalogTest(unittest.TestCase):
|
||||
self.doc1_brain = brains[0]
|
||||
|
||||
def test_total_comments(self):
|
||||
self.assertTrue('total_comments' in self.doc1_brain)
|
||||
self.assertTrue("total_comments" in self.doc1_brain)
|
||||
self.assertEqual(self.doc1_brain.total_comments, 0)
|
||||
|
||||
# Make sure no conversation has been created
|
||||
self.assertTrue(
|
||||
'plone.app.discussion:conversation' not in
|
||||
IAnnotations(self.portal.doc1),
|
||||
"plone.app.discussion:conversation" not in IAnnotations(self.portal.doc1),
|
||||
)
|
||||
|
@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.browser.comment import View
|
||||
from plone.app.discussion.interfaces import IComment
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IReplies
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
@ -12,11 +11,10 @@ from zope.component import getMultiAdapter
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import six
|
||||
import unittest
|
||||
|
||||
|
||||
logger = logging.getLogger('plone.app.discussion.tests')
|
||||
logger = logging.getLogger("plone.app.discussion.tests")
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
|
||||
|
||||
@ -25,30 +23,30 @@ class CommentTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
|
||||
workflow = self.portal.portal_workflow
|
||||
workflow.doActionFor(self.portal.doc1, 'publish')
|
||||
workflow.doActionFor(self.portal.doc1, "publish")
|
||||
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.catalog = getToolByName(self.portal, 'portal_catalog')
|
||||
self.document_brain = self.catalog.searchResults(
|
||||
portal_type='Document')[0]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.catalog = getToolByName(self.portal, "portal_catalog")
|
||||
self.document_brain = self.catalog.searchResults(portal_type="Document")[0]
|
||||
|
||||
def test_factory(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
self.assertTrue(IComment.providedBy(comment1))
|
||||
|
||||
def test_UTCDates(self):
|
||||
utc_to_local_diff = \
|
||||
datetime.datetime.now() - datetime.datetime.utcnow()
|
||||
utc_to_local_diff = datetime.datetime.now() - datetime.datetime.utcnow()
|
||||
utc_to_local_diff = abs(utc_to_local_diff.seconds)
|
||||
if utc_to_local_diff < 60:
|
||||
logger.warning('Your computer is living in a timezone where local '
|
||||
'time equals utc time. Some potential errors can '
|
||||
'get hidden by that')
|
||||
comment1 = createObject('plone.Comment')
|
||||
logger.warning(
|
||||
"Your computer is living in a timezone where local "
|
||||
"time equals utc time. Some potential errors can "
|
||||
"get hidden by that"
|
||||
)
|
||||
comment1 = createObject("plone.Comment")
|
||||
local_utc = datetime.datetime.utcnow()
|
||||
for date in (comment1.creation_date, comment1.modification_date):
|
||||
difference = abs(date - local_utc)
|
||||
@ -58,171 +56,163 @@ class CommentTest(unittest.TestCase):
|
||||
self.assertFalse(difference // 10)
|
||||
|
||||
def test_id(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.comment_id = 123
|
||||
self.assertEqual('123', comment1.id)
|
||||
self.assertEqual('123', comment1.getId())
|
||||
self.assertEqual(u'123', comment1.__name__)
|
||||
self.assertEqual("123", comment1.id)
|
||||
self.assertEqual("123", comment1.getId())
|
||||
self.assertEqual("123", comment1.__name__)
|
||||
|
||||
def test_uid(self):
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
conversation.addComment(comment1)
|
||||
comment_brain = self.catalog.searchResults(
|
||||
portal_type='Discussion Item',
|
||||
portal_type="Discussion Item",
|
||||
)[0]
|
||||
self.assertTrue(comment_brain.UID)
|
||||
|
||||
def test_uid_is_unique(self):
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
conversation.addComment(comment1)
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2 = createObject("plone.Comment")
|
||||
conversation.addComment(comment2)
|
||||
brains = self.catalog.searchResults(
|
||||
portal_type='Discussion Item',
|
||||
portal_type="Discussion Item",
|
||||
)
|
||||
self.assertNotEqual(brains[0].UID, brains[1].UID)
|
||||
|
||||
def test_comment_uid_differs_from_content_uid(self):
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
conversation.addComment(comment1)
|
||||
comment_brain = self.catalog.searchResults(
|
||||
portal_type='Discussion Item',
|
||||
portal_type="Discussion Item",
|
||||
)[0]
|
||||
self.assertNotEqual(self.document_brain.UID, comment_brain.UID)
|
||||
|
||||
def test_title(self):
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.author_name = 'Jim Fulton'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.author_name = "Jim Fulton"
|
||||
conversation.addComment(comment1)
|
||||
self.assertEqual('Jim Fulton on Document 1', comment1.Title())
|
||||
self.assertEqual("Jim Fulton on Document 1", comment1.Title())
|
||||
|
||||
def test_no_name_title(self):
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
conversation.addComment(comment1)
|
||||
self.assertEqual('Anonymous on Document 1', comment1.Title())
|
||||
self.assertEqual("Anonymous on Document 1", comment1.Title())
|
||||
|
||||
def test_title_special_characters(self):
|
||||
self.portal.invokeFactory(
|
||||
id='doc_sp_chars',
|
||||
title=u'Document äüö',
|
||||
type_name='Document',
|
||||
id="doc_sp_chars",
|
||||
title="Document äüö",
|
||||
type_name="Document",
|
||||
)
|
||||
conversation = IConversation(self.portal.doc_sp_chars)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.author_name = u'Tarek Ziadé'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.author_name = "Tarek Ziadé"
|
||||
conversation.addComment(comment1)
|
||||
self.assertEqual(u'Tarek Ziadé on Document äüö', comment1.Title())
|
||||
self.assertEqual("Tarek Ziadé on Document äüö", comment1.Title())
|
||||
|
||||
def test_title_special_characters_utf8(self):
|
||||
self.portal.invokeFactory(
|
||||
id='doc_sp_chars_utf8',
|
||||
title='Document ëïû',
|
||||
type_name='Document',
|
||||
id="doc_sp_chars_utf8",
|
||||
title="Document ëïû",
|
||||
type_name="Document",
|
||||
)
|
||||
conversation = IConversation(self.portal.doc_sp_chars_utf8)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.author_name = 'Hüüb Bôûmä'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.author_name = "Hüüb Bôûmä"
|
||||
conversation.addComment(comment1)
|
||||
self.assertEqual(u'Hüüb Bôûmä on Document ëïû', comment1.Title())
|
||||
self.assertEqual("Hüüb Bôûmä on Document ëïû", comment1.Title())
|
||||
|
||||
def test_creator(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.creator = 'jim'
|
||||
self.assertEqual('jim', comment1.Creator())
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.creator = "jim"
|
||||
self.assertEqual("jim", comment1.Creator())
|
||||
|
||||
def test_creator_author_name(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.author_name = 'joey'
|
||||
self.assertEqual('joey', comment1.Creator())
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.author_name = "joey"
|
||||
self.assertEqual("joey", comment1.Creator())
|
||||
|
||||
def test_owner(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
self.assertEqual((['plone', 'acl_users'], TEST_USER_ID),
|
||||
comment1.getOwnerTuple())
|
||||
comment1 = createObject("plone.Comment")
|
||||
self.assertEqual(
|
||||
(["plone", "acl_users"], TEST_USER_ID), comment1.getOwnerTuple()
|
||||
)
|
||||
|
||||
def test_type(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
self.assertEqual(comment1.Type(), 'Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
self.assertEqual(comment1.Type(), "Comment")
|
||||
|
||||
def test_mime_type(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
self.assertEqual(comment1.mime_type, 'text/plain')
|
||||
comment1 = createObject("plone.Comment")
|
||||
self.assertEqual(comment1.mime_type, "text/plain")
|
||||
|
||||
def test_getText(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'First paragraph\n\nSecond_paragraph'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "First paragraph\n\nSecond_paragraph"
|
||||
self.assertEqual(
|
||||
''.join(comment1.getText().split()),
|
||||
'<p>Firstparagraph<br><br>Second_paragraph</p>',
|
||||
"".join(comment1.getText().split()),
|
||||
"<p>Firstparagraph<br><br>Second_paragraph</p>",
|
||||
)
|
||||
|
||||
def test_getText_escapes_HTML(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = '<b>Got HTML?</b>'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "<b>Got HTML?</b>"
|
||||
self.assertEqual(
|
||||
comment1.getText(),
|
||||
'<p><b>Got HTML?</b></p>',
|
||||
"<p><b>Got HTML?</b></p>",
|
||||
)
|
||||
|
||||
def test_getText_with_non_ascii_characters(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = u'Umlaute sind ä, ö und ü.'
|
||||
out = b'<p>Umlaute sind \xc3\xa4, \xc3\xb6 und \xc3\xbc.</p>'
|
||||
if six.PY2:
|
||||
self.assertEqual(
|
||||
comment1.getText(),
|
||||
out)
|
||||
else:
|
||||
self.assertEqual(
|
||||
comment1.getText(),
|
||||
out.decode('utf8'))
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Umlaute sind ä, ö und ü."
|
||||
out = b"<p>Umlaute sind \xc3\xa4, \xc3\xb6 und \xc3\xbc.</p>"
|
||||
self.assertEqual(comment1.getText(), out.decode("utf8"))
|
||||
|
||||
def test_getText_doesnt_link(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Go to http://www.plone.org'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Go to http://www.plone.org"
|
||||
self.assertEqual(
|
||||
comment1.getText(),
|
||||
'<p>Go to http://www.plone.org</p>',
|
||||
"<p>Go to http://www.plone.org</p>",
|
||||
)
|
||||
|
||||
def test_getText_uses_comment_mime_type(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Go to http://www.plone.org'
|
||||
comment1.mime_type = 'text/x-web-intelligent'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Go to http://www.plone.org"
|
||||
comment1.mime_type = "text/x-web-intelligent"
|
||||
self.assertEqual(
|
||||
comment1.getText(),
|
||||
'Go to <a href="http://www.plone.org" ' +
|
||||
'rel="nofollow">http://www.plone.org</a>',
|
||||
'Go to <a href="http://www.plone.org" '
|
||||
+ 'rel="nofollow">http://www.plone.org</a>',
|
||||
)
|
||||
|
||||
def test_getText_uses_comment_mime_type_html(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = 'Go to <a href="http://www.plone.org">plone.org</a>'
|
||||
comment1.mime_type = 'text/html'
|
||||
comment1.mime_type = "text/html"
|
||||
self.assertEqual(
|
||||
comment1.getText(),
|
||||
'Go to <a href="http://www.plone.org">plone.org</a>',
|
||||
)
|
||||
|
||||
def test_getText_w_custom_targetMimetype(self):
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'para'
|
||||
self.assertEqual(comment1.getText(targetMimetype='text/plain'), 'para')
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "para"
|
||||
self.assertEqual(comment1.getText(targetMimetype="text/plain"), "para")
|
||||
|
||||
def test_getText_invalid_transformation_raises_error(self):
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.mime_type = 'text/x-html-safe'
|
||||
comment1.text = 'para'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.mime_type = "text/x-html-safe"
|
||||
comment1.text = "para"
|
||||
conversation.addComment(comment1)
|
||||
self.assertEqual(
|
||||
comment1.getText(targetMimetype='text/html'),
|
||||
'para')
|
||||
self.assertEqual(comment1.getText(targetMimetype="text/html"), "para")
|
||||
|
||||
def test_traversal(self):
|
||||
# make sure comments are traversable, have an id, absolute_url and
|
||||
@ -230,26 +220,29 @@ class CommentTest(unittest.TestCase):
|
||||
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
|
||||
new_comment1_id = conversation.addComment(comment1)
|
||||
|
||||
comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_comment1_id),
|
||||
f"++conversation++default/{new_comment1_id}",
|
||||
)
|
||||
self.assertTrue(IComment.providedBy(comment))
|
||||
|
||||
self.assertEqual(
|
||||
(
|
||||
'', 'plone', 'doc1', '++conversation++default',
|
||||
"",
|
||||
"plone",
|
||||
"doc1",
|
||||
"++conversation++default",
|
||||
str(new_comment1_id),
|
||||
),
|
||||
comment.getPhysicalPath(),
|
||||
)
|
||||
self.assertEqual(
|
||||
'http://nohost/plone/doc1/++conversation++default/' +
|
||||
str(new_comment1_id), comment.absolute_url(),
|
||||
"http://nohost/plone/doc1/++conversation++default/" + str(new_comment1_id),
|
||||
comment.absolute_url(),
|
||||
)
|
||||
|
||||
def test_view_blob_types(self):
|
||||
@ -258,68 +251,67 @@ class CommentTest(unittest.TestCase):
|
||||
version of the url with a /view in it.
|
||||
"""
|
||||
self.portal.invokeFactory(
|
||||
id='image1',
|
||||
title='Image',
|
||||
type_name='Image',
|
||||
id="image1",
|
||||
title="Image",
|
||||
type_name="Image",
|
||||
)
|
||||
conversation = IConversation(self.portal.image1)
|
||||
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
new_comment1_id = conversation.addComment(comment1)
|
||||
comment = self.portal.image1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_comment1_id),
|
||||
f"++conversation++default/{new_comment1_id}",
|
||||
)
|
||||
|
||||
view = View(comment, self.request)
|
||||
View.__call__(view)
|
||||
response = self.request.response
|
||||
self.assertIn('/view', response.headers['location'])
|
||||
self.assertIn("/view", response.headers["location"])
|
||||
|
||||
def test_workflow(self):
|
||||
"""Basic test for the 'comment_review_workflow'
|
||||
"""
|
||||
"""Basic test for the 'comment_review_workflow'"""
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
('comment_review_workflow,'),
|
||||
("Discussion Item",),
|
||||
("comment_review_workflow,"),
|
||||
)
|
||||
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
new_comment1_id = conversation.addComment(comment1)
|
||||
|
||||
comment = conversation[new_comment1_id]
|
||||
|
||||
# Make sure comments use the 'comment_review_workflow'
|
||||
chain = self.portal.portal_workflow.getChainFor(comment)
|
||||
self.assertEqual(('comment_review_workflow',), chain)
|
||||
self.assertEqual(("comment_review_workflow",), chain)
|
||||
|
||||
# Ensure the initial state was entered and recorded
|
||||
self.assertEqual(
|
||||
1,
|
||||
len(comment.workflow_history['comment_review_workflow']),
|
||||
len(comment.workflow_history["comment_review_workflow"]),
|
||||
)
|
||||
self.assertEqual(
|
||||
None,
|
||||
comment.workflow_history['comment_review_workflow'][0]['action'],
|
||||
comment.workflow_history["comment_review_workflow"][0]["action"],
|
||||
)
|
||||
self.assertEqual(
|
||||
'pending',
|
||||
self.portal.portal_workflow.getInfoFor(comment, 'review_state'),
|
||||
"pending",
|
||||
self.portal.portal_workflow.getInfoFor(comment, "review_state"),
|
||||
)
|
||||
|
||||
def test_fti(self):
|
||||
# test that we can look up an FTI for Discussion Item
|
||||
|
||||
self.assertIn(
|
||||
'Discussion Item',
|
||||
"Discussion Item",
|
||||
self.portal.portal_types.objectIds(),
|
||||
)
|
||||
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1 = createObject("plone.Comment")
|
||||
|
||||
fti = self.portal.portal_types.getTypeInfo(comment1)
|
||||
self.assertEqual('Discussion Item', fti.getTypeInfo(comment1).getId())
|
||||
self.assertEqual("Discussion Item", fti.getTypeInfo(comment1).getId())
|
||||
|
||||
def test_view(self):
|
||||
# make sure that the comment view is there and redirects to the right
|
||||
@ -330,21 +322,21 @@ class CommentTest(unittest.TestCase):
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
|
||||
# Create a comment
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
|
||||
# Add comment to the conversation
|
||||
new_comment1_id = conversation.addComment(comment1)
|
||||
|
||||
comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_comment1_id),
|
||||
f"++conversation++default/{new_comment1_id}",
|
||||
)
|
||||
|
||||
# make sure the view is there
|
||||
self.assertTrue(
|
||||
getMultiAdapter(
|
||||
(comment, self.request),
|
||||
name='view',
|
||||
name="view",
|
||||
),
|
||||
)
|
||||
|
||||
@ -362,11 +354,11 @@ class RepliesTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
|
||||
workflow = self.portal.portal_workflow
|
||||
workflow.doActionFor(self.portal.doc1, 'publish')
|
||||
workflow.doActionFor(self.portal.doc1, "publish")
|
||||
|
||||
def test_add_comment(self):
|
||||
# Add comments to a CommentReplies adapter
|
||||
@ -378,16 +370,16 @@ class RepliesTest(unittest.TestCase):
|
||||
# Add a comment to the conversation
|
||||
replies = IReplies(conversation)
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
new_id = replies.addComment(comment)
|
||||
comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id),
|
||||
f"++conversation++default/{new_id}",
|
||||
)
|
||||
|
||||
# Add a reply to the CommentReplies adapter of the first comment
|
||||
re_comment = createObject('plone.Comment')
|
||||
re_comment.text = 'Comment text'
|
||||
re_comment = createObject("plone.Comment")
|
||||
re_comment.text = "Comment text"
|
||||
|
||||
replies = IReplies(comment)
|
||||
|
||||
@ -415,16 +407,16 @@ class RepliesTest(unittest.TestCase):
|
||||
# Add a comment to the conversation
|
||||
replies = IReplies(conversation)
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
new_id = replies.addComment(comment)
|
||||
comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id),
|
||||
f"++conversation++default/{new_id}",
|
||||
)
|
||||
|
||||
# Add a reply to the CommentReplies adapter of the first comment
|
||||
re_comment = createObject('plone.Comment')
|
||||
re_comment.text = 'Comment text'
|
||||
re_comment = createObject("plone.Comment")
|
||||
re_comment.text = "Comment text"
|
||||
|
||||
replies = IReplies(comment)
|
||||
|
||||
@ -446,83 +438,86 @@ class RepliesTest(unittest.TestCase):
|
||||
# physical path
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
|
||||
conversation.addComment(comment1)
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
new_id = conversation.addComment(comment)
|
||||
comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id),
|
||||
f"++conversation++default/{new_id}",
|
||||
)
|
||||
|
||||
# Add a reply to the CommentReplies adapter of the first comment
|
||||
re_comment = createObject('plone.Comment')
|
||||
re_comment.text = 'Comment text'
|
||||
re_comment = createObject("plone.Comment")
|
||||
re_comment.text = "Comment text"
|
||||
replies = IReplies(comment)
|
||||
new_re_id = replies.addComment(re_comment)
|
||||
re_comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_re_id),
|
||||
f"++conversation++default/{new_re_id}",
|
||||
)
|
||||
|
||||
# Add a reply to the reply
|
||||
re_re_comment = createObject('plone.Comment')
|
||||
re_re_comment.text = 'Comment text'
|
||||
re_re_comment = createObject("plone.Comment")
|
||||
re_re_comment.text = "Comment text"
|
||||
replies = IReplies(re_comment)
|
||||
new_re_re_id = replies.addComment(re_re_comment)
|
||||
re_re_comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_re_re_id),
|
||||
f"++conversation++default/{new_re_re_id}",
|
||||
)
|
||||
|
||||
# Add a reply to the replies reply
|
||||
re_re_re_comment = createObject('plone.Comment')
|
||||
re_re_re_comment.text = 'Comment text'
|
||||
re_re_re_comment = createObject("plone.Comment")
|
||||
re_re_re_comment.text = "Comment text"
|
||||
replies = IReplies(re_re_comment)
|
||||
new_re_re_re_id = replies.addComment(re_re_re_comment)
|
||||
re_re_re_comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_re_re_re_id),
|
||||
f"++conversation++default/{new_re_re_re_id}",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
('', 'plone', 'doc1', '++conversation++default', str(new_id)),
|
||||
("", "plone", "doc1", "++conversation++default", str(new_id)),
|
||||
comment.getPhysicalPath(),
|
||||
)
|
||||
self.assertEqual(
|
||||
'http://nohost/plone/doc1/++conversation++default/' +
|
||||
str(new_id), comment.absolute_url(),
|
||||
"http://nohost/plone/doc1/++conversation++default/" + str(new_id),
|
||||
comment.absolute_url(),
|
||||
)
|
||||
self.assertEqual(
|
||||
('', 'plone', 'doc1', '++conversation++default', str(new_re_id)),
|
||||
("", "plone", "doc1", "++conversation++default", str(new_re_id)),
|
||||
re_comment.getPhysicalPath(),
|
||||
)
|
||||
self.assertEqual(
|
||||
'http://nohost/plone/doc1/++conversation++default/' +
|
||||
str(new_re_id),
|
||||
"http://nohost/plone/doc1/++conversation++default/" + str(new_re_id),
|
||||
re_comment.absolute_url(),
|
||||
)
|
||||
self.assertEqual(
|
||||
(
|
||||
'', 'plone', 'doc1', '++conversation++default',
|
||||
"",
|
||||
"plone",
|
||||
"doc1",
|
||||
"++conversation++default",
|
||||
str(new_re_re_id),
|
||||
),
|
||||
re_re_comment.getPhysicalPath(),
|
||||
)
|
||||
self.assertEqual(
|
||||
'http://nohost/plone/doc1/++conversation++default/' +
|
||||
str(new_re_re_id),
|
||||
"http://nohost/plone/doc1/++conversation++default/" + str(new_re_re_id),
|
||||
re_re_comment.absolute_url(),
|
||||
)
|
||||
self.assertEqual(
|
||||
(
|
||||
'', 'plone', 'doc1', '++conversation++default',
|
||||
"",
|
||||
"plone",
|
||||
"doc1",
|
||||
"++conversation++default",
|
||||
str(new_re_re_re_id),
|
||||
),
|
||||
re_re_re_comment.getPhysicalPath(),
|
||||
)
|
||||
self.assertEqual(
|
||||
'http://nohost/plone/doc1/++conversation++default/' +
|
||||
str(new_re_re_re_id),
|
||||
"http://nohost/plone/doc1/++conversation++default/" + str(new_re_re_re_id),
|
||||
re_re_re_comment.absolute_url(),
|
||||
)
|
||||
|
@ -1,14 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from .. import interfaces
|
||||
from ..browser.comment import EditCommentForm
|
||||
from ..browser.comments import CommentForm
|
||||
from ..browser.comments import CommentsViewlet
|
||||
from ..interfaces import IConversation
|
||||
from ..interfaces import IDiscussionSettings
|
||||
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
from AccessControl import Unauthorized
|
||||
from datetime import datetime
|
||||
from OFS.Image import Image
|
||||
from plone.app.discussion import interfaces
|
||||
from plone.app.discussion.browser.comment import EditCommentForm
|
||||
from plone.app.discussion.browser.comments import CommentForm
|
||||
from plone.app.discussion.browser.comments import CommentsViewlet
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from plone.app.testing import login
|
||||
from plone.app.testing import logout
|
||||
from plone.app.testing import setRoles
|
||||
@ -16,7 +15,6 @@ from plone.app.testing import TEST_USER_ID
|
||||
from plone.app.testing import TEST_USER_NAME
|
||||
from plone.registry.interfaces import IRegistry
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
from Products.CMFPlone.tests import dummy
|
||||
from z3c.form.interfaces import IFormLayer
|
||||
from zope import interface
|
||||
from zope.annotation.interfaces import IAttributeAnnotatable
|
||||
@ -28,33 +26,67 @@ from zope.interface import alsoProvides
|
||||
from zope.interface import Interface
|
||||
from zope.publisher.browser import TestRequest
|
||||
from zope.publisher.interfaces.browser import IBrowserRequest
|
||||
from ZPublisher.HTTPRequest import FileUpload
|
||||
|
||||
import io
|
||||
import time
|
||||
import unittest
|
||||
|
||||
|
||||
TEXT = b"file data"
|
||||
|
||||
|
||||
class DummyFile(FileUpload):
|
||||
"""Dummy upload object
|
||||
Used to fake uploaded files.
|
||||
"""
|
||||
|
||||
__allow_access_to_unprotected_subobjects__ = 1
|
||||
filename = "dummy.txt"
|
||||
data = TEXT
|
||||
headers = {}
|
||||
|
||||
def __init__(self, filename=None, data=None, headers=None):
|
||||
if filename is not None:
|
||||
self.filename = filename
|
||||
if data is not None:
|
||||
self.data = data
|
||||
if headers is not None:
|
||||
self.headers = headers
|
||||
self.file = io.BytesIO(self.data)
|
||||
|
||||
def seek(self, *args):
|
||||
pass
|
||||
|
||||
def tell(self, *args):
|
||||
return 1
|
||||
|
||||
def read(self, *args):
|
||||
return self.data
|
||||
|
||||
|
||||
class TestCommentForm(unittest.TestCase):
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal.invokeFactory('Folder', 'test-folder')
|
||||
self.folder = self.portal['test-folder']
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.portal.invokeFactory("Folder", "test-folder")
|
||||
self.folder = self.portal["test-folder"]
|
||||
|
||||
interface.alsoProvides(
|
||||
self.portal.REQUEST,
|
||||
interfaces.IDiscussionLayer,
|
||||
)
|
||||
|
||||
wftool = getToolByName(self.portal, 'portal_workflow')
|
||||
wftool.doActionFor(self.portal.doc1, action='publish')
|
||||
wftool = getToolByName(self.portal, "portal_workflow")
|
||||
wftool.doActionFor(self.portal.doc1, action="publish")
|
||||
self.portal.doc1.allow_discussion = True
|
||||
self.membershipTool = getToolByName(self.folder, 'portal_membership')
|
||||
self.membershipTool = getToolByName(self.folder, "portal_membership")
|
||||
self.memberdata = self.portal.portal_memberdata
|
||||
self.context = getattr(self.portal, 'doc1')
|
||||
self.context = getattr(self.portal, "doc1")
|
||||
|
||||
# Allow discussion
|
||||
registry = queryUtility(IRegistry)
|
||||
@ -62,8 +94,7 @@ class TestCommentForm(unittest.TestCase):
|
||||
settings.globally_enabled = True
|
||||
|
||||
def test_add_comment(self):
|
||||
"""Post a comment as logged-in user.
|
||||
"""
|
||||
"""Post a comment as logged-in user."""
|
||||
|
||||
# Allow discussion
|
||||
self.portal.doc1.allow_discussion = True
|
||||
@ -80,7 +111,7 @@ class TestCommentForm(unittest.TestCase):
|
||||
adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
|
||||
# The form should return an error if the comment text field is empty
|
||||
@ -88,46 +119,45 @@ class TestCommentForm(unittest.TestCase):
|
||||
|
||||
commentForm = getMultiAdapter(
|
||||
(self.context, request),
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
commentForm.update()
|
||||
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
|
||||
|
||||
self.assertEqual(len(errors), 1)
|
||||
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
|
||||
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
|
||||
|
||||
# The form is submitted successfully, if the required text field is
|
||||
# filled out
|
||||
request = make_request(form={'form.widgets.text': u'bar'})
|
||||
request = make_request(form={"form.widgets.text": "bar"})
|
||||
|
||||
commentForm = getMultiAdapter(
|
||||
(self.context, request),
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
commentForm.update()
|
||||
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
|
||||
|
||||
self.assertEqual(len(errors), 0)
|
||||
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
|
||||
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
|
||||
|
||||
comments = IConversation(commentForm.context).getComments()
|
||||
comments = [comment for comment in comments] # consume iterator
|
||||
self.assertEqual(len(comments), 1)
|
||||
|
||||
for comment in comments:
|
||||
self.assertEqual(comment.text, u'bar')
|
||||
self.assertEqual(comment.creator, 'test_user_1_')
|
||||
self.assertEqual(comment.getOwner().getUserName(), 'test-user')
|
||||
self.assertEqual(comment.text, "bar")
|
||||
self.assertEqual(comment.creator, "test_user_1_")
|
||||
self.assertEqual(comment.getOwner().getUserName(), "test-user")
|
||||
local_roles = comment.get_local_roles()
|
||||
self.assertEqual(len(local_roles), 1)
|
||||
userid, roles = local_roles[0]
|
||||
self.assertEqual(userid, 'test_user_1_')
|
||||
self.assertEqual(userid, "test_user_1_")
|
||||
self.assertEqual(len(roles), 1)
|
||||
self.assertEqual(roles[0], 'Owner')
|
||||
self.assertEqual(roles[0], "Owner")
|
||||
|
||||
def test_edit_comment(self):
|
||||
"""Edit a comment as logged-in user.
|
||||
"""
|
||||
"""Edit a comment as logged-in user."""
|
||||
|
||||
# Allow discussion
|
||||
self.portal.doc1.allow_discussion = True
|
||||
@ -144,65 +174,64 @@ class TestCommentForm(unittest.TestCase):
|
||||
adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
|
||||
provideAdapter(
|
||||
adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=EditCommentForm,
|
||||
name=u'edit-comment-form',
|
||||
name="edit-comment-form",
|
||||
)
|
||||
|
||||
# The form is submitted successfully, if the required text field is
|
||||
# filled out
|
||||
request = make_request(form={'form.widgets.text': u'bar'})
|
||||
request = make_request(form={"form.widgets.text": "bar"})
|
||||
|
||||
commentForm = getMultiAdapter(
|
||||
(self.context, request),
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
commentForm.update()
|
||||
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
|
||||
|
||||
self.assertEqual(len(errors), 0)
|
||||
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
|
||||
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
|
||||
|
||||
# Edit the last comment
|
||||
conversation = IConversation(self.context)
|
||||
comment = [x for x in conversation.getComments()][-1]
|
||||
request = make_request(form={'form.widgets.text': u'foobar'})
|
||||
request = make_request(form={"form.widgets.text": "foobar"})
|
||||
editForm = getMultiAdapter(
|
||||
(comment, request),
|
||||
name=u'edit-comment-form',
|
||||
name="edit-comment-form",
|
||||
)
|
||||
editForm.update()
|
||||
data, errors = editForm.extractData() # pylint: disable-msg=W0612
|
||||
|
||||
self.assertEqual(len(errors), 0)
|
||||
self.assertFalse(editForm.handleComment(editForm, 'foo'))
|
||||
self.assertFalse(editForm.handleComment(editForm, "foo"))
|
||||
comment = [x for x in conversation.getComments()][-1]
|
||||
self.assertEqual(comment.text, u'foobar')
|
||||
self.assertEqual(comment.text, "foobar")
|
||||
|
||||
comments = IConversation(commentForm.context).getComments()
|
||||
comments = [c for c in comments] # consume iterator
|
||||
self.assertEqual(len(comments), 1)
|
||||
|
||||
for comment in comments:
|
||||
self.assertEqual(comment.text, u'foobar')
|
||||
self.assertEqual(comment.creator, 'test_user_1_')
|
||||
self.assertEqual(comment.text, "foobar")
|
||||
self.assertEqual(comment.creator, "test_user_1_")
|
||||
|
||||
self.assertEqual(comment.getOwner().getUserName(), 'test-user')
|
||||
self.assertEqual(comment.getOwner().getUserName(), "test-user")
|
||||
local_roles = comment.get_local_roles()
|
||||
self.assertEqual(len(local_roles), 1)
|
||||
userid, roles = local_roles[0]
|
||||
self.assertEqual(userid, 'test_user_1_')
|
||||
self.assertEqual(userid, "test_user_1_")
|
||||
self.assertEqual(len(roles), 1)
|
||||
self.assertEqual(roles[0], 'Owner')
|
||||
self.assertEqual(roles[0], "Owner")
|
||||
|
||||
def test_delete_comment(self):
|
||||
"""Delete a comment as logged-in user.
|
||||
"""
|
||||
"""Delete a comment as logged-in user."""
|
||||
|
||||
# Allow discussion
|
||||
self.portal.doc1.allow_discussion = True
|
||||
@ -219,48 +248,47 @@ class TestCommentForm(unittest.TestCase):
|
||||
adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
|
||||
# The form is submitted successfully, if the required text field is
|
||||
# filled out
|
||||
form_request = make_request(form={'form.widgets.text': u'bar'})
|
||||
form_request = make_request(form={"form.widgets.text": "bar"})
|
||||
|
||||
commentForm = getMultiAdapter(
|
||||
(self.context, form_request),
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
|
||||
commentForm.update()
|
||||
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
|
||||
self.assertEqual(len(errors), 0)
|
||||
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
|
||||
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
|
||||
|
||||
# Delete the last comment
|
||||
conversation = IConversation(self.context)
|
||||
comment = [x for x in conversation.getComments()][-1]
|
||||
deleteView = getMultiAdapter(
|
||||
(comment, self.request),
|
||||
name=u'moderate-delete-comment',
|
||||
name="moderate-delete-comment",
|
||||
)
|
||||
# try to delete last comment without 'Delete comments' permission
|
||||
setRoles(self.portal, TEST_USER_ID, ['Member'])
|
||||
setRoles(self.portal, TEST_USER_ID, ["Member"])
|
||||
self.assertRaises(
|
||||
Unauthorized,
|
||||
comment.restrictedTraverse,
|
||||
'@@moderate-delete-comment',
|
||||
"@@moderate-delete-comment",
|
||||
)
|
||||
deleteView()
|
||||
self.assertEqual(1, len([x for x in conversation.getComments()]))
|
||||
# try to delete last comment with 'Delete comments' permission
|
||||
setRoles(self.portal, TEST_USER_ID, ['Reviewer'])
|
||||
setRoles(self.portal, TEST_USER_ID, ["Reviewer"])
|
||||
deleteView()
|
||||
self.assertEqual(0, len([x for x in conversation.getComments()]))
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
|
||||
def test_delete_own_comment(self):
|
||||
"""Delete own comment as logged-in user.
|
||||
"""
|
||||
"""Delete own comment as logged-in user."""
|
||||
|
||||
# Allow discussion
|
||||
self.portal.doc1.allow_discussion = True
|
||||
@ -277,42 +305,42 @@ class TestCommentForm(unittest.TestCase):
|
||||
adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
|
||||
# The form is submitted successfully, if the required text field is
|
||||
# filled out
|
||||
form_request = make_request(form={'form.widgets.text': u'bar'})
|
||||
form_request = make_request(form={"form.widgets.text": "bar"})
|
||||
|
||||
commentForm = getMultiAdapter(
|
||||
(self.context, form_request),
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
|
||||
commentForm.update()
|
||||
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
|
||||
self.assertEqual(len(errors), 0)
|
||||
self.assertFalse(commentForm.handleComment(commentForm, 'foo'))
|
||||
self.assertFalse(commentForm.handleComment(commentForm, "foo"))
|
||||
|
||||
# Delete the last comment
|
||||
conversation = IConversation(self.context)
|
||||
comment = [x for x in conversation.getComments()][-1]
|
||||
deleteView = getMultiAdapter(
|
||||
(comment, self.request),
|
||||
name=u'delete-own-comment',
|
||||
name="delete-own-comment",
|
||||
)
|
||||
# try to delete last comment with johndoe
|
||||
setRoles(self.portal, 'johndoe', ['Member'])
|
||||
login(self.portal, 'johndoe')
|
||||
setRoles(self.portal, "johndoe", ["Member"])
|
||||
login(self.portal, "johndoe")
|
||||
self.assertRaises(
|
||||
Unauthorized,
|
||||
comment.restrictedTraverse,
|
||||
'@@delete-own-comment',
|
||||
"@@delete-own-comment",
|
||||
)
|
||||
self.assertEqual(1, len([x for x in conversation.getComments()]))
|
||||
# try to delete last comment with the same user that created it
|
||||
login(self.portal, TEST_USER_NAME)
|
||||
setRoles(self.portal, TEST_USER_ID, ['Member'])
|
||||
setRoles(self.portal, TEST_USER_ID, ["Member"])
|
||||
deleteView()
|
||||
self.assertEqual(0, len([x for x in conversation.getComments()]))
|
||||
|
||||
@ -335,40 +363,43 @@ class TestCommentForm(unittest.TestCase):
|
||||
alsoProvides(request, IAttributeAnnotatable)
|
||||
return request
|
||||
|
||||
provideAdapter(adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name=u'comment-form')
|
||||
provideAdapter(
|
||||
adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name="comment-form",
|
||||
)
|
||||
|
||||
# Post an anonymous comment and provide a name
|
||||
request = make_request(form={
|
||||
'form.widgets.name': u'john doe',
|
||||
'form.widgets.text': u'bar',
|
||||
})
|
||||
request = make_request(
|
||||
form={
|
||||
"form.widgets.name": "john doe",
|
||||
"form.widgets.text": "bar",
|
||||
}
|
||||
)
|
||||
|
||||
commentForm = getMultiAdapter(
|
||||
(self.context, request),
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
commentForm.update()
|
||||
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
|
||||
|
||||
self.assertEqual(len(errors), 0)
|
||||
self.assertFalse(commentForm.handleComment(commentForm, 'action'))
|
||||
self.assertFalse(commentForm.handleComment(commentForm, "action"))
|
||||
|
||||
comments = IConversation(commentForm.context).getComments()
|
||||
comments = [comment for comment in comments] # consume itertor
|
||||
self.assertEqual(len(comments), 1)
|
||||
|
||||
for comment in IConversation(commentForm.context).getComments():
|
||||
self.assertEqual(comment.text, u'bar')
|
||||
self.assertEqual(comment.text, "bar")
|
||||
self.assertIsNone(comment.creator)
|
||||
roles = comment.get_local_roles()
|
||||
self.assertEqual(len(roles), 0)
|
||||
|
||||
def test_can_not_add_comments_if_discussion_is_not_allowed(self):
|
||||
"""Make sure that comments can't be posted if discussion is disabled.
|
||||
"""
|
||||
"""Make sure that comments can't be posted if discussion is disabled."""
|
||||
|
||||
# Disable discussion
|
||||
registry = queryUtility(IRegistry)
|
||||
@ -382,16 +413,18 @@ class TestCommentForm(unittest.TestCase):
|
||||
alsoProvides(request, IAttributeAnnotatable)
|
||||
return request
|
||||
|
||||
provideAdapter(adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name=u'comment-form')
|
||||
provideAdapter(
|
||||
adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name="comment-form",
|
||||
)
|
||||
|
||||
request = make_request(form={'form.widgets.text': u'bar'})
|
||||
request = make_request(form={"form.widgets.text": "bar"})
|
||||
|
||||
commentForm = getMultiAdapter(
|
||||
(self.context, request),
|
||||
name=u'comment-form',
|
||||
name="comment-form",
|
||||
)
|
||||
commentForm.update()
|
||||
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
|
||||
@ -400,14 +433,11 @@ class TestCommentForm(unittest.TestCase):
|
||||
# allowed
|
||||
self.assertEqual(len(errors), 0)
|
||||
|
||||
self.assertRaises(Unauthorized,
|
||||
commentForm.handleComment,
|
||||
commentForm,
|
||||
'foo')
|
||||
self.assertRaises(Unauthorized, commentForm.handleComment, commentForm, "foo")
|
||||
|
||||
def test_anonymous_can_not_add_comments_if_discussion_is_not_allowed(self):
|
||||
"""Make sure that anonymous users can't post comments if anonymous
|
||||
comments are disabled.
|
||||
comments are disabled.
|
||||
"""
|
||||
|
||||
# Anonymous comments are disabled by default
|
||||
@ -421,15 +451,16 @@ class TestCommentForm(unittest.TestCase):
|
||||
alsoProvides(request, IAttributeAnnotatable)
|
||||
return request
|
||||
|
||||
provideAdapter(adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name=u'comment-form')
|
||||
provideAdapter(
|
||||
adapts=(Interface, IBrowserRequest),
|
||||
provides=Interface,
|
||||
factory=CommentForm,
|
||||
name="comment-form",
|
||||
)
|
||||
|
||||
request = make_request(form={'form.widgets.text': u'bar'})
|
||||
request = make_request(form={"form.widgets.text": "bar"})
|
||||
|
||||
commentForm = getMultiAdapter((self.context, request),
|
||||
name=u'comment-form')
|
||||
commentForm = getMultiAdapter((self.context, request), name="comment-form")
|
||||
commentForm.update()
|
||||
data, errors = commentForm.extractData() # pylint: disable-msg=W0612
|
||||
|
||||
@ -438,7 +469,7 @@ class TestCommentForm(unittest.TestCase):
|
||||
Unauthorized,
|
||||
commentForm.handleComment,
|
||||
commentForm,
|
||||
'foo',
|
||||
"foo",
|
||||
)
|
||||
|
||||
|
||||
@ -447,22 +478,22 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal.invokeFactory('Folder', 'test-folder')
|
||||
self.folder = self.portal['test-folder']
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.portal.invokeFactory("Folder", "test-folder")
|
||||
self.folder = self.portal["test-folder"]
|
||||
interface.alsoProvides(
|
||||
self.request,
|
||||
interfaces.IDiscussionLayer,
|
||||
)
|
||||
|
||||
self.workflowTool = getToolByName(self.portal, 'portal_workflow')
|
||||
self.workflowTool.setDefaultChain('comment_one_state_workflow')
|
||||
self.workflowTool = getToolByName(self.portal, "portal_workflow")
|
||||
self.workflowTool.setDefaultChain("comment_one_state_workflow")
|
||||
|
||||
self.membershipTool = getToolByName(self.folder, 'portal_membership')
|
||||
self.membershipTool = getToolByName(self.folder, "portal_membership")
|
||||
self.memberdata = self.portal.portal_memberdata
|
||||
context = getattr(self.portal, 'doc1')
|
||||
context = getattr(self.portal, "doc1")
|
||||
self.viewlet = CommentsViewlet(context, self.request, None, None)
|
||||
|
||||
# Allow discussion
|
||||
@ -484,15 +515,14 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
# Anonymous has no 'can review' permission
|
||||
self.assertFalse(self.viewlet.can_review())
|
||||
# The reviewer role has the 'Review comments' permission
|
||||
self.portal.acl_users._doAddUser(
|
||||
'reviewer', 'secret', ['Reviewer'], [])
|
||||
login(self.portal, 'reviewer')
|
||||
self.portal.acl_users._doAddUser("reviewer", "secret", ["Reviewer"], [])
|
||||
login(self.portal, "reviewer")
|
||||
self.assertTrue(self.viewlet.can_review())
|
||||
|
||||
def test_can_manage(self):
|
||||
"""We keep this method for backward compatibility. This method has been
|
||||
removed in version 1.0b9 and added again in 1.0b11 because we don't
|
||||
do API changes in beta releases.
|
||||
removed in version 1.0b9 and added again in 1.0b11 because we don't
|
||||
do API changes in beta releases.
|
||||
"""
|
||||
# Portal owner has 'can review' permission
|
||||
self.assertTrue(self.viewlet.can_manage())
|
||||
@ -500,9 +530,8 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
# Anonymous has no 'can review' permission
|
||||
self.assertFalse(self.viewlet.can_manage())
|
||||
# The reviewer role has the 'Review comments' permission
|
||||
self.portal.acl_users._doAddUser(
|
||||
'reviewer', 'secret', ['Reviewer'], [])
|
||||
login(self.portal, 'reviewer')
|
||||
self.portal.acl_users._doAddUser("reviewer", "secret", ["Reviewer"], [])
|
||||
login(self.portal, "reviewer")
|
||||
self.assertTrue(self.viewlet.can_manage())
|
||||
|
||||
def test_is_discussion_allowed(self):
|
||||
@ -519,46 +548,48 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
self.assertTrue(self.viewlet.comment_transform_message())
|
||||
self.assertEqual(
|
||||
self.viewlet.comment_transform_message(),
|
||||
'You can add a comment by filling out the form below. Plain ' +
|
||||
'text formatting.')
|
||||
"You can add a comment by filling out the form below. Plain "
|
||||
+ "text formatting.",
|
||||
)
|
||||
|
||||
# Set text transform to intelligent text
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
settings.text_transform = 'text/x-web-intelligent'
|
||||
settings.text_transform = "text/x-web-intelligent"
|
||||
|
||||
# Make sure the comment description changes accordingly
|
||||
self.assertEqual(
|
||||
self.viewlet.comment_transform_message(),
|
||||
'You can add a comment by filling out the form below. ' +
|
||||
'Plain text formatting. Web and email addresses are transformed ' +
|
||||
'into clickable links.',
|
||||
"You can add a comment by filling out the form below. "
|
||||
+ "Plain text formatting. Web and email addresses are transformed "
|
||||
+ "into clickable links.",
|
||||
)
|
||||
|
||||
# Enable moderation workflow
|
||||
self.workflowTool.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
('comment_review_workflow,'))
|
||||
("Discussion Item",), ("comment_review_workflow,")
|
||||
)
|
||||
|
||||
# Make sure the comment description shows that comments are moderated
|
||||
self.assertEqual(
|
||||
self.viewlet.comment_transform_message(),
|
||||
'You can add a comment by filling out the form below. ' +
|
||||
'Plain text formatting. Web and email addresses are transformed ' +
|
||||
'into clickable links. Comments are moderated.')
|
||||
"You can add a comment by filling out the form below. "
|
||||
+ "Plain text formatting. Web and email addresses are transformed "
|
||||
+ "into clickable links. Comments are moderated.",
|
||||
)
|
||||
|
||||
def test_has_replies(self):
|
||||
self.assertEqual(self.viewlet.has_replies(), False)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
conversation.addComment(comment)
|
||||
self.assertEqual(self.viewlet.has_replies(), True)
|
||||
|
||||
def test_get_replies(self):
|
||||
self.assertFalse(self.viewlet.get_replies())
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
conversation.addComment(comment)
|
||||
conversation.addComment(comment)
|
||||
@ -571,7 +602,7 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
next(replies)
|
||||
|
||||
def test_get_replies_on_non_annotatable_object(self):
|
||||
context = self.portal.MailHost # the mail host is not annotatable
|
||||
context = self.portal.MailHost # the mail host is not annotatable
|
||||
viewlet = CommentsViewlet(context, self.request, None, None)
|
||||
replies = viewlet.get_replies()
|
||||
self.assertEqual(len(tuple(replies)), 0)
|
||||
@ -581,8 +612,8 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
|
||||
def test_get_replies_with_workflow_actions(self):
|
||||
self.assertFalse(self.viewlet.get_replies(workflow_actions=True))
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
c1 = conversation.addComment(comment)
|
||||
self.assertEqual(
|
||||
@ -591,32 +622,34 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
)
|
||||
# Enable moderation workflow
|
||||
self.workflowTool.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
('comment_review_workflow,'),
|
||||
("Discussion Item",),
|
||||
("comment_review_workflow,"),
|
||||
)
|
||||
# Check if workflow actions are available
|
||||
reply = next(self.viewlet.get_replies(workflow_actions=True))
|
||||
self.assertTrue('actions' in reply)
|
||||
self.assertTrue("actions" in reply)
|
||||
self.assertEqual(
|
||||
reply['actions'][0]['id'],
|
||||
'mark_as_spam',
|
||||
reply["actions"][0]["id"],
|
||||
"mark_as_spam",
|
||||
)
|
||||
expected_url = (
|
||||
"http://nohost/plone/doc1/++conversation++default/{0}"
|
||||
"/content_status_modify?workflow_action=mark_as_spam"
|
||||
)
|
||||
expected_url = 'http://nohost/plone/doc1/++conversation++default/{0}' \
|
||||
'/content_status_modify?workflow_action=mark_as_spam'
|
||||
self.assertEqual(
|
||||
reply['actions'][0]['url'],
|
||||
reply["actions"][0]["url"],
|
||||
expected_url.format(int(c1)),
|
||||
)
|
||||
|
||||
def test_get_commenter_home_url(self):
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
IConversation(self.portal.doc1)
|
||||
portal_membership = getToolByName(self.portal, 'portal_membership')
|
||||
portal_membership = getToolByName(self.portal, "portal_membership")
|
||||
m = portal_membership.getAuthenticatedMember()
|
||||
self.assertEqual(
|
||||
self.viewlet.get_commenter_home_url(m.getUserName()),
|
||||
'http://nohost/plone/author/test-user',
|
||||
"http://nohost/plone/author/test-user",
|
||||
)
|
||||
|
||||
def test_get_commenter_home_url_is_none(self):
|
||||
@ -625,72 +658,77 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
def test_get_commenter_portrait(self):
|
||||
|
||||
# Add a user with a member image
|
||||
self.membershipTool.addMember('jim', 'Jim', ['Member'], [])
|
||||
self.memberdata._setPortrait(Image(
|
||||
id='jim',
|
||||
file=dummy.File(),
|
||||
title='',
|
||||
), 'jim')
|
||||
self.assertEqual(
|
||||
self.memberdata._getPortrait('jim').getId(),
|
||||
'jim',
|
||||
self.membershipTool.addMember("jim", "Jim", ["Member"], [])
|
||||
self.memberdata._setPortrait(
|
||||
Image(
|
||||
id="jim",
|
||||
file=DummyFile(),
|
||||
title="",
|
||||
),
|
||||
"jim",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.memberdata._getPortrait('jim').meta_type,
|
||||
'Image',
|
||||
self.memberdata._getPortrait("jim").getId(),
|
||||
"jim",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.memberdata._getPortrait("jim").meta_type,
|
||||
"Image",
|
||||
)
|
||||
|
||||
# Add a conversation with a comment
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment.Creator = 'Jim'
|
||||
comment.author_username = 'jim'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment.Creator = "Jim"
|
||||
comment.author_username = "jim"
|
||||
conversation.addComment(comment)
|
||||
|
||||
# Call get_commenter_portrait method of the viewlet
|
||||
self.viewlet.update()
|
||||
portrait_url = self.viewlet.get_commenter_portrait('jim')
|
||||
portrait_url = self.viewlet.get_commenter_portrait("jim")
|
||||
|
||||
# Check if the correct member image URL is returned
|
||||
self.assertEqual(
|
||||
portrait_url,
|
||||
'http://nohost/plone/portal_memberdata/portraits/jim',
|
||||
"http://nohost/plone/portal_memberdata/portraits/jim",
|
||||
)
|
||||
|
||||
def test_get_commenter_portrait_is_none(self):
|
||||
|
||||
self.assertTrue(
|
||||
self.viewlet.get_commenter_portrait() in (
|
||||
'defaultUser.png',
|
||||
'defaultUser.gif',
|
||||
self.viewlet.get_commenter_portrait()
|
||||
in (
|
||||
"defaultUser.png",
|
||||
"defaultUser.gif",
|
||||
),
|
||||
)
|
||||
|
||||
def test_get_commenter_portrait_without_userimage(self):
|
||||
|
||||
# Create a user without a user image
|
||||
self.membershipTool.addMember('jim', 'Jim', ['Member'], [])
|
||||
self.membershipTool.addMember("jim", "Jim", ["Member"], [])
|
||||
|
||||
# Add a conversation with a comment
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment.Creator = 'Jim'
|
||||
comment.author_username = 'jim'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment.Creator = "Jim"
|
||||
comment.author_username = "jim"
|
||||
conversation.addComment(comment)
|
||||
|
||||
# Call get_commenter_portrait method of the viewlet
|
||||
self.viewlet.update()
|
||||
portrait_url = self.viewlet.get_commenter_portrait('jim')
|
||||
portrait_url = self.viewlet.get_commenter_portrait("jim")
|
||||
|
||||
# Check if the correct default member image URL is returned.
|
||||
# Note that Products.PlonePAS 4.0.5 and later have .png and
|
||||
# earlier versions have .gif.
|
||||
self.assertTrue(
|
||||
portrait_url in (
|
||||
'http://nohost/plone/defaultUser.png',
|
||||
'http://nohost/plone/defaultUser.gif',
|
||||
portrait_url
|
||||
in (
|
||||
"http://nohost/plone/defaultUser.png",
|
||||
"http://nohost/plone/defaultUser.gif",
|
||||
),
|
||||
)
|
||||
|
||||
@ -700,8 +738,8 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
# Allow anonymous discussion
|
||||
registry = queryUtility(IRegistry)
|
||||
registry[
|
||||
'plone.app.discussion.interfaces.IDiscussionSettings.' +
|
||||
'anonymous_comments'
|
||||
"plone.app.discussion.interfaces.IDiscussionSettings."
|
||||
+ "anonymous_comments"
|
||||
] = True
|
||||
# Test if anonymous discussion is allowed for the viewlet
|
||||
self.assertTrue(self.viewlet.anonymous_discussion_allowed())
|
||||
@ -710,8 +748,8 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
self.assertTrue(self.viewlet.show_commenter_image())
|
||||
registry = queryUtility(IRegistry)
|
||||
registry[
|
||||
'plone.app.discussion.interfaces.IDiscussionSettings.' +
|
||||
'show_commenter_image'
|
||||
"plone.app.discussion.interfaces.IDiscussionSettings."
|
||||
+ "show_commenter_image"
|
||||
] = False
|
||||
self.assertFalse(self.viewlet.show_commenter_image())
|
||||
|
||||
@ -724,7 +762,7 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
self.viewlet.update()
|
||||
self.assertEqual(
|
||||
self.viewlet.login_action(),
|
||||
'http://nohost/plone/login_form?came_from=http%3A//nohost',
|
||||
"http://nohost/plone/login_form?came_from=http%3A//nohost",
|
||||
)
|
||||
|
||||
def test_format_time(self):
|
||||
@ -737,9 +775,8 @@ class TestCommentsViewlet(unittest.TestCase):
|
||||
# a correct utc time that can be used to make datetime set the utc
|
||||
# time of the local time given above. That way, the time for the
|
||||
# example below is correct within each time zone, independent of DST
|
||||
python_time = datetime(
|
||||
*time.gmtime(time.mktime(python_time.timetuple()))[:7])
|
||||
python_time = datetime(*time.gmtime(time.mktime(python_time.timetuple()))[:7])
|
||||
localized_time = self.viewlet.format_time(python_time)
|
||||
self.assertTrue(
|
||||
localized_time in ['Feb 01, 2009 11:32 PM', '2009-02-01 23:32'],
|
||||
localized_time in ["Feb 01, 2009 11:32 PM", "2009-02-01 23:32"],
|
||||
)
|
||||
|
@ -1,11 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.interfaces import ICommentAddedEvent
|
||||
from plone.app.discussion.interfaces import ICommentRemovedEvent
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IReplies
|
||||
from plone.app.discussion.interfaces import IReplyAddedEvent
|
||||
from plone.app.discussion.interfaces import IReplyRemovedEvent
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from plone.app.discussion.testing import ( # noqa
|
||||
PLONE_APP_DISCUSSION_INTEGRATION_TESTING,
|
||||
)
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from plone.contentrules.rule.interfaces import IRuleEventType
|
||||
@ -17,31 +18,33 @@ import unittest
|
||||
|
||||
|
||||
class CommentContentRulesTest(unittest.TestCase):
|
||||
""" Test custom comments events
|
||||
"""
|
||||
"""Test custom comments events"""
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
# Setup sandbox
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
|
||||
# Setup current user properties
|
||||
member = self.portal.portal_membership.getMemberById(TEST_USER_ID)
|
||||
member.setMemberProperties({
|
||||
'fullname': 'X Manager',
|
||||
'email': 'xmanager@example.com',
|
||||
})
|
||||
member.setMemberProperties(
|
||||
{
|
||||
"fullname": "X Manager",
|
||||
"email": "xmanager@example.com",
|
||||
}
|
||||
)
|
||||
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
|
||||
self.document = self.portal['doc1']
|
||||
self.document = self.portal["doc1"]
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'This is a comment'
|
||||
comment.author_username = 'jim'
|
||||
comment.author_name = 'Jim'
|
||||
comment.author_email = 'jim@example.com'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "This is a comment"
|
||||
comment.author_username = "jim"
|
||||
comment.author_name = "Jim"
|
||||
comment.author_email = "jim@example.com"
|
||||
conversation = IConversation(self.document)
|
||||
conversation.addComment(comment)
|
||||
|
||||
@ -52,58 +55,61 @@ class CommentContentRulesTest(unittest.TestCase):
|
||||
self.assertTrue(IRuleEventType.providedBy(IReplyRemovedEvent))
|
||||
|
||||
def testCommentIdStringSubstitution(self):
|
||||
comment_id = getAdapter(self.document, IStringSubstitution,
|
||||
name=u'comment_id')
|
||||
comment_id = getAdapter(self.document, IStringSubstitution, name="comment_id")
|
||||
self.assertIsInstance(comment_id(), int)
|
||||
|
||||
def testCommentTextStringSubstitution(self):
|
||||
comment_text = getAdapter(self.document, IStringSubstitution,
|
||||
name=u'comment_text')
|
||||
self.assertEqual(comment_text(), u'This is a comment')
|
||||
comment_text = getAdapter(
|
||||
self.document, IStringSubstitution, name="comment_text"
|
||||
)
|
||||
self.assertEqual(comment_text(), "This is a comment")
|
||||
|
||||
def testCommentUserIdStringSubstitution(self):
|
||||
comment_user_id = getAdapter(self.document, IStringSubstitution,
|
||||
name=u'comment_user_id')
|
||||
self.assertEqual(comment_user_id(), u'jim')
|
||||
comment_user_id = getAdapter(
|
||||
self.document, IStringSubstitution, name="comment_user_id"
|
||||
)
|
||||
self.assertEqual(comment_user_id(), "jim")
|
||||
|
||||
def testCommentUserFullNameStringSubstitution(self):
|
||||
comment_user_fullname = getAdapter(self.document, IStringSubstitution,
|
||||
name=u'comment_user_fullname')
|
||||
self.assertEqual(comment_user_fullname(), u'Jim')
|
||||
comment_user_fullname = getAdapter(
|
||||
self.document, IStringSubstitution, name="comment_user_fullname"
|
||||
)
|
||||
self.assertEqual(comment_user_fullname(), "Jim")
|
||||
|
||||
def testCommentUserEmailStringSubstitution(self):
|
||||
comment_user_email = getAdapter(self.document, IStringSubstitution,
|
||||
name=u'comment_user_email')
|
||||
self.assertEqual(comment_user_email(), u'jim@example.com')
|
||||
comment_user_email = getAdapter(
|
||||
self.document, IStringSubstitution, name="comment_user_email"
|
||||
)
|
||||
self.assertEqual(comment_user_email(), "jim@example.com")
|
||||
|
||||
|
||||
class ReplyContentRulesTest(unittest.TestCase):
|
||||
""" Test custom comments events
|
||||
"""
|
||||
"""Test custom comments events"""
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
# Setup sandbox
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
|
||||
self.document = self.portal['doc1']
|
||||
self.document = self.portal["doc1"]
|
||||
conversation = IConversation(self.document)
|
||||
replies = IReplies(conversation)
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'This is a comment'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "This is a comment"
|
||||
new_id = replies.addComment(comment)
|
||||
comment = self.document.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id),
|
||||
f"++conversation++default/{new_id}",
|
||||
)
|
||||
|
||||
re_comment = createObject('plone.Comment')
|
||||
re_comment.text = 'This is a reply'
|
||||
re_comment.author_username = 'julia'
|
||||
re_comment.author_name = 'Juliana'
|
||||
re_comment.author_email = 'julia@example.com'
|
||||
re_comment = createObject("plone.Comment")
|
||||
re_comment.text = "This is a reply"
|
||||
re_comment.author_username = "julia"
|
||||
re_comment.author_name = "Juliana"
|
||||
re_comment.author_email = "julia@example.com"
|
||||
|
||||
replies = IReplies(comment)
|
||||
replies.addComment(re_comment)
|
||||
@ -112,7 +118,7 @@ class ReplyContentRulesTest(unittest.TestCase):
|
||||
reply_id = getAdapter(
|
||||
self.document,
|
||||
IStringSubstitution,
|
||||
name=u'comment_id',
|
||||
name="comment_id",
|
||||
)
|
||||
self.assertIsInstance(reply_id(), int)
|
||||
|
||||
@ -120,30 +126,30 @@ class ReplyContentRulesTest(unittest.TestCase):
|
||||
reply_text = getAdapter(
|
||||
self.document,
|
||||
IStringSubstitution,
|
||||
name=u'comment_text',
|
||||
name="comment_text",
|
||||
)
|
||||
self.assertEqual(reply_text(), u'This is a reply')
|
||||
self.assertEqual(reply_text(), "This is a reply")
|
||||
|
||||
def testReplyUserIdStringSubstitution(self):
|
||||
reply_user_id = getAdapter(
|
||||
self.document,
|
||||
IStringSubstitution,
|
||||
name=u'comment_user_id',
|
||||
name="comment_user_id",
|
||||
)
|
||||
self.assertEqual(reply_user_id(), u'julia')
|
||||
self.assertEqual(reply_user_id(), "julia")
|
||||
|
||||
def testReplyUserFullNameStringSubstitution(self):
|
||||
reply_user_fullname = getAdapter(
|
||||
self.document,
|
||||
IStringSubstitution,
|
||||
name=u'comment_user_fullname',
|
||||
name="comment_user_fullname",
|
||||
)
|
||||
self.assertEqual(reply_user_fullname(), u'Juliana')
|
||||
self.assertEqual(reply_user_fullname(), "Juliana")
|
||||
|
||||
def testReplyUserEmailStringSubstitution(self):
|
||||
reply_user_email = getAdapter(
|
||||
self.document,
|
||||
IStringSubstitution,
|
||||
name=u'comment_user_email',
|
||||
name="comment_user_email",
|
||||
)
|
||||
self.assertEqual(reply_user_email(), u'julia@example.com')
|
||||
self.assertEqual(reply_user_email(), "julia@example.com")
|
||||
|
@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from plone.app.discussion.testing import ( # noqa
|
||||
PLONE_APP_DISCUSSION_INTEGRATION_TESTING,
|
||||
)
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from plone.registry import Registry
|
||||
@ -17,8 +18,8 @@ class RegistryTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.registry = Registry()
|
||||
self.registry.registerInterface(IDiscussionSettings)
|
||||
|
||||
@ -29,99 +30,100 @@ class RegistryTest(unittest.TestCase):
|
||||
def test_discussion_controlpanel_view(self):
|
||||
view = getMultiAdapter(
|
||||
(self.portal, self.portal.REQUEST),
|
||||
name='discussion-controlpanel',
|
||||
name="discussion-controlpanel",
|
||||
)
|
||||
self.assertTrue(view())
|
||||
|
||||
def test_discussion_in_controlpanel(self):
|
||||
# Check if discussion is in the control panel
|
||||
self.controlpanel = getToolByName(self.portal, 'portal_controlpanel')
|
||||
self.controlpanel = getToolByName(self.portal, "portal_controlpanel")
|
||||
self.assertTrue(
|
||||
'discussion' in [
|
||||
a.getAction(self)['id']
|
||||
for a in self.controlpanel.listActions()
|
||||
],
|
||||
"discussion"
|
||||
in [a.getAction(self)["id"] for a in self.controlpanel.listActions()],
|
||||
)
|
||||
|
||||
def test_globally_enabled(self):
|
||||
# Check globally_enabled record
|
||||
self.assertTrue('globally_enabled' in IDiscussionSettings)
|
||||
self.assertTrue("globally_enabled" in IDiscussionSettings)
|
||||
self.assertEqual(
|
||||
self.registry[
|
||||
'plone.app.discussion.interfaces.' +
|
||||
'IDiscussionSettings.globally_enabled'
|
||||
"plone.app.discussion.interfaces."
|
||||
+ "IDiscussionSettings.globally_enabled"
|
||||
],
|
||||
False,
|
||||
)
|
||||
|
||||
def test_anonymous_comments(self):
|
||||
# Check anonymous_comments record
|
||||
self.assertTrue('anonymous_comments' in IDiscussionSettings)
|
||||
self.assertTrue("anonymous_comments" in IDiscussionSettings)
|
||||
self.assertEqual(
|
||||
self.registry[
|
||||
'plone.app.discussion.interfaces.' +
|
||||
'IDiscussionSettings.anonymous_comments'
|
||||
"plone.app.discussion.interfaces."
|
||||
+ "IDiscussionSettings.anonymous_comments"
|
||||
],
|
||||
False,
|
||||
)
|
||||
|
||||
def test_moderation_enabled(self):
|
||||
# Check globally_enabled record
|
||||
self.assertTrue('moderation_enabled' in IDiscussionSettings)
|
||||
self.assertTrue("moderation_enabled" in IDiscussionSettings)
|
||||
self.assertEqual(
|
||||
self.registry[
|
||||
'plone.app.discussion.interfaces.' +
|
||||
'IDiscussionSettings.moderation_enabled'
|
||||
"plone.app.discussion.interfaces."
|
||||
+ "IDiscussionSettings.moderation_enabled"
|
||||
],
|
||||
False,
|
||||
)
|
||||
|
||||
def test_edit_comment_enabled(self):
|
||||
# Check edit_comment_enabled record
|
||||
self.assertTrue('edit_comment_enabled' in IDiscussionSettings)
|
||||
self.assertTrue("edit_comment_enabled" in IDiscussionSettings)
|
||||
self.assertEqual(
|
||||
self.registry['plone.app.discussion.interfaces.' +
|
||||
'IDiscussionSettings.edit_comment_enabled'],
|
||||
self.registry[
|
||||
"plone.app.discussion.interfaces."
|
||||
+ "IDiscussionSettings.edit_comment_enabled"
|
||||
],
|
||||
False,
|
||||
)
|
||||
|
||||
def test_delete_own_comment_enabled(self):
|
||||
# Check delete_own_comment_enabled record
|
||||
self.assertTrue('delete_own_comment_enabled' in IDiscussionSettings)
|
||||
self.assertTrue("delete_own_comment_enabled" in IDiscussionSettings)
|
||||
self.assertEqual(
|
||||
self.registry['plone.app.discussion.interfaces.' +
|
||||
'IDiscussionSettings.delete_own_comment_enabled'],
|
||||
self.registry[
|
||||
"plone.app.discussion.interfaces."
|
||||
+ "IDiscussionSettings.delete_own_comment_enabled"
|
||||
],
|
||||
False,
|
||||
)
|
||||
|
||||
def test_text_transform(self):
|
||||
self.assertTrue('text_transform' in IDiscussionSettings)
|
||||
self.assertTrue("text_transform" in IDiscussionSettings)
|
||||
self.assertEqual(
|
||||
self.registry[
|
||||
'plone.app.discussion.interfaces.' +
|
||||
'IDiscussionSettings.text_transform'
|
||||
"plone.app.discussion.interfaces."
|
||||
+ "IDiscussionSettings.text_transform"
|
||||
],
|
||||
'text/plain',
|
||||
"text/plain",
|
||||
)
|
||||
|
||||
def test_captcha(self):
|
||||
# Check globally_enabled record
|
||||
self.assertTrue('captcha' in IDiscussionSettings)
|
||||
self.assertTrue("captcha" in IDiscussionSettings)
|
||||
self.assertEqual(
|
||||
self.registry[
|
||||
'plone.app.discussion.interfaces.' +
|
||||
'IDiscussionSettings.captcha'
|
||||
"plone.app.discussion.interfaces." + "IDiscussionSettings.captcha"
|
||||
],
|
||||
'disabled',
|
||||
"disabled",
|
||||
)
|
||||
|
||||
def test_show_commenter_image(self):
|
||||
# Check show_commenter_image record
|
||||
self.assertTrue('show_commenter_image' in IDiscussionSettings)
|
||||
self.assertTrue("show_commenter_image" in IDiscussionSettings)
|
||||
self.assertEqual(
|
||||
self.registry[
|
||||
'plone.app.discussion.interfaces.' +
|
||||
'IDiscussionSettings.show_commenter_image'
|
||||
"plone.app.discussion.interfaces."
|
||||
+ "IDiscussionSettings.show_commenter_image"
|
||||
],
|
||||
True,
|
||||
)
|
||||
@ -129,12 +131,12 @@ class RegistryTest(unittest.TestCase):
|
||||
def test_moderator_notification_enabled(self):
|
||||
# Check show_commenter_image record
|
||||
self.assertTrue(
|
||||
'moderator_notification_enabled' in IDiscussionSettings,
|
||||
"moderator_notification_enabled" in IDiscussionSettings,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.registry[
|
||||
'plone.app.discussion.interfaces.' +
|
||||
'IDiscussionSettings.moderator_notification_enabled'
|
||||
"plone.app.discussion.interfaces."
|
||||
+ "IDiscussionSettings.moderator_notification_enabled"
|
||||
],
|
||||
False,
|
||||
)
|
||||
@ -154,22 +156,22 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
registry = queryUtility(IRegistry)
|
||||
self.settings = registry.forInterface(IDiscussionSettings, check=False)
|
||||
|
||||
def test_moderation_enabled_in_discussion_control_panel_changed(self):
|
||||
"""Make sure the 'Discussion Item' workflow is changed properly, when
|
||||
the 'comment_moderation' setting in the discussion control panel
|
||||
changes.
|
||||
the 'comment_moderation' setting in the discussion control panel
|
||||
changes.
|
||||
"""
|
||||
# By default the comment_one_state_workflow without moderation is
|
||||
# enabled
|
||||
self.assertEqual(
|
||||
('comment_one_state_workflow',),
|
||||
("comment_one_state_workflow",),
|
||||
self.portal.portal_workflow.getChainForPortalType(
|
||||
'Discussion Item',
|
||||
"Discussion Item",
|
||||
),
|
||||
)
|
||||
|
||||
@ -179,32 +181,32 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
|
||||
# Make sure the comment_review_workflow with moderation enabled is
|
||||
# enabled
|
||||
self.assertEqual(
|
||||
('comment_review_workflow',),
|
||||
("comment_review_workflow",),
|
||||
self.portal.portal_workflow.getChainForPortalType(
|
||||
'Discussion Item',
|
||||
"Discussion Item",
|
||||
),
|
||||
)
|
||||
# And back
|
||||
self.settings.moderation_enabled = False
|
||||
self.assertEqual(
|
||||
('comment_one_state_workflow',),
|
||||
("comment_one_state_workflow",),
|
||||
self.portal.portal_workflow.getChainForPortalType(
|
||||
'Discussion Item',
|
||||
"Discussion Item",
|
||||
),
|
||||
)
|
||||
|
||||
def test_change_workflow_in_types_control_panel(self):
|
||||
"""Make sure the setting in the discussion control panel is changed
|
||||
accordingly, when the workflow for the 'Discussion Item' changed in
|
||||
the types control panel.
|
||||
accordingly, when the workflow for the 'Discussion Item' changed in
|
||||
the types control panel.
|
||||
"""
|
||||
# By default, moderation is disabled
|
||||
self.settings.moderation_enabled = False
|
||||
|
||||
# Enable the 'comment_review_workflow' with moderation enabled
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
('comment_review_workflow',),
|
||||
("Discussion Item",),
|
||||
("comment_review_workflow",),
|
||||
)
|
||||
|
||||
# Make sure the moderation_enabled settings has changed
|
||||
@ -212,15 +214,15 @@ class ConfigurationChangedSubscriberTest(unittest.TestCase):
|
||||
|
||||
# Enable the 'comment_review_workflow' with moderation enabled
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
('comment_one_state_workflow',),
|
||||
("Discussion Item",),
|
||||
("comment_one_state_workflow",),
|
||||
)
|
||||
self.settings.moderation_enabled = True
|
||||
|
||||
# Enable a 'custom' discussion workflow
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
('intranet_workflow',),
|
||||
("Discussion Item",),
|
||||
("intranet_workflow",),
|
||||
)
|
||||
|
||||
# Setting has not changed. A Custom workflow disables the
|
||||
|
@ -1,17 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ..interfaces import IComment
|
||||
from ..interfaces import IConversation
|
||||
from ..interfaces import IDiscussionLayer
|
||||
from ..interfaces import IDiscussionSettings
|
||||
from ..interfaces import IReplies
|
||||
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
from Acquisition import aq_base
|
||||
from Acquisition import aq_parent
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from plone.app.discussion import interfaces
|
||||
from plone.app.discussion.interfaces import IComment
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.app.discussion.interfaces import IReplies
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from plone.app.vocabularies.types import BAD_TYPES
|
||||
from plone.dexterity.interfaces import IDexterityContent
|
||||
from plone.registry.interfaces import IRegistry
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
from zope import interface
|
||||
@ -19,31 +19,22 @@ from zope.annotation.interfaces import IAnnotations
|
||||
from zope.component import createObject
|
||||
from zope.component import queryUtility
|
||||
|
||||
import six
|
||||
import unittest
|
||||
|
||||
|
||||
try:
|
||||
from plone.dexterity.interfaces import IDexterityContent
|
||||
DEXTERITY = True
|
||||
except ImportError:
|
||||
DEXTERITY = False
|
||||
|
||||
|
||||
class ConversationTest(unittest.TestCase):
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
interface.alsoProvides(
|
||||
self.portal.REQUEST, interfaces.IDiscussionLayer)
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
interface.alsoProvides(self.portal.REQUEST, IDiscussionLayer)
|
||||
|
||||
self.typetool = self.portal.portal_types
|
||||
self.portal_discussion = getToolByName(
|
||||
self.portal,
|
||||
'portal_discussion',
|
||||
"portal_discussion",
|
||||
None,
|
||||
)
|
||||
# Allow discussion
|
||||
@ -52,7 +43,7 @@ class ConversationTest(unittest.TestCase):
|
||||
settings.globally_enabled = True
|
||||
|
||||
workflow = self.portal.portal_workflow
|
||||
workflow.doActionFor(self.portal.doc1, 'publish')
|
||||
workflow.doActionFor(self.portal.doc1, "publish")
|
||||
|
||||
def test_add_comment(self):
|
||||
# Create a conversation. In this case we doesn't assign it to an
|
||||
@ -62,8 +53,8 @@ class ConversationTest(unittest.TestCase):
|
||||
# Add a comment. Note: in real life, we always create comments via the
|
||||
# factory to allow different factories to be swapped in
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
new_id = conversation.addComment(comment)
|
||||
|
||||
@ -79,20 +70,19 @@ class ConversationTest(unittest.TestCase):
|
||||
self.assertEqual(len(tuple(conversation.getThreads())), 1)
|
||||
self.assertEqual(conversation.total_comments(), 1)
|
||||
self.assertTrue(
|
||||
conversation.last_comment_date - datetime.utcnow() <
|
||||
timedelta(seconds=1),
|
||||
conversation.last_comment_date - datetime.utcnow() < timedelta(seconds=1),
|
||||
)
|
||||
|
||||
def test_private_comment(self):
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.author_username = 'nobody'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.author_username = "nobody"
|
||||
conversation.addComment(comment)
|
||||
comment.manage_permission('View', roles=tuple())
|
||||
comment.manage_permission("View", roles=tuple())
|
||||
self.assertEqual(0, conversation.total_comments())
|
||||
self.assertEqual(None, conversation.last_comment_date)
|
||||
self.assertEqual(['nobody'], list(conversation.commentators))
|
||||
self.assertEqual(["nobody"], list(conversation.commentators))
|
||||
self.assertEqual([], list(conversation.public_commentators))
|
||||
|
||||
def test_delete_comment(self):
|
||||
@ -103,8 +93,8 @@ class ConversationTest(unittest.TestCase):
|
||||
# Add a comment. Note: in real life, we always create comments via the
|
||||
# factory to allow different factories to be swapped in
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
new_id = conversation.addComment(comment)
|
||||
|
||||
@ -139,23 +129,23 @@ class ConversationTest(unittest.TestCase):
|
||||
# +- Comment 2_1
|
||||
|
||||
# Create all comments
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
|
||||
comment1_1 = createObject('plone.Comment')
|
||||
comment1_1.text = 'Comment text'
|
||||
comment1_1 = createObject("plone.Comment")
|
||||
comment1_1.text = "Comment text"
|
||||
|
||||
comment1_1_1 = createObject('plone.Comment')
|
||||
comment1_1_1.text = 'Comment text'
|
||||
comment1_1_1 = createObject("plone.Comment")
|
||||
comment1_1_1.text = "Comment text"
|
||||
|
||||
comment1_2 = createObject('plone.Comment')
|
||||
comment1_2.text = 'Comment text'
|
||||
comment1_2 = createObject("plone.Comment")
|
||||
comment1_2.text = "Comment text"
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.text = 'Comment text'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.text = "Comment text"
|
||||
|
||||
comment2_1 = createObject('plone.Comment')
|
||||
comment2_1.text = 'Comment text'
|
||||
comment2_1 = createObject("plone.Comment")
|
||||
comment2_1.text = "Comment text"
|
||||
|
||||
# Create the nested comment structure
|
||||
new_id_1 = conversation.addComment(comment1)
|
||||
@ -175,21 +165,24 @@ class ConversationTest(unittest.TestCase):
|
||||
|
||||
del conversation[new_id_1]
|
||||
|
||||
self.assertEqual([
|
||||
{'comment': comment2, 'depth': 0, 'id': new_id_2},
|
||||
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1},
|
||||
], list(conversation.getThreads()))
|
||||
self.assertEqual(
|
||||
[
|
||||
{"comment": comment2, "depth": 0, "id": new_id_2},
|
||||
{"comment": comment2_1, "depth": 1, "id": new_id_2_1},
|
||||
],
|
||||
list(conversation.getThreads()),
|
||||
)
|
||||
|
||||
def test_delete_comment_when_content_object_is_deleted(self):
|
||||
# Make sure all comments of a content object are deleted when the
|
||||
# object itself is deleted.
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
conversation.addComment(comment)
|
||||
|
||||
# Delete the content object
|
||||
self.portal.manage_delObjects(['doc1'])
|
||||
self.portal.manage_delObjects(["doc1"])
|
||||
|
||||
# Make sure the comment has been deleted as well
|
||||
self.assertEqual(len(list(conversation.getComments())), 0)
|
||||
@ -198,8 +191,8 @@ class ConversationTest(unittest.TestCase):
|
||||
|
||||
def test_comments_enabled_on_doc_in_subfolder(self):
|
||||
typetool = self.portal.portal_types
|
||||
typetool.constructContent('Folder', self.portal, 'folder1')
|
||||
typetool.constructContent('Document', self.portal.folder1, 'doc2')
|
||||
typetool.constructContent("Folder", self.portal, "folder1")
|
||||
typetool.constructContent("Document", self.portal.folder1, "doc2")
|
||||
|
||||
folder = self.portal.folder1
|
||||
|
||||
@ -209,13 +202,13 @@ class ConversationTest(unittest.TestCase):
|
||||
self.assertFalse(aq_base(folder).allow_discussion)
|
||||
|
||||
doc = self.portal.folder1.doc2
|
||||
conversation = doc.restrictedTraverse('@@conversation_view')
|
||||
conversation = doc.restrictedTraverse("@@conversation_view")
|
||||
self.assertEqual(conversation.enabled(), False)
|
||||
|
||||
# We have to allow discussion on Document content type, since
|
||||
# otherwise allow_discussion will always return False
|
||||
portal_types = getToolByName(self.portal, 'portal_types')
|
||||
document_fti = getattr(portal_types, 'Document')
|
||||
portal_types = getToolByName(self.portal, "portal_types")
|
||||
document_fti = getattr(portal_types, "Document")
|
||||
document_fti.manage_changeProperties(allow_discussion=True)
|
||||
|
||||
self.assertEqual(conversation.enabled(), True)
|
||||
@ -223,13 +216,12 @@ class ConversationTest(unittest.TestCase):
|
||||
def test_disable_commenting_globally(self):
|
||||
|
||||
# Create a conversation.
|
||||
conversation = self.portal.doc1.restrictedTraverse(
|
||||
'@@conversation_view')
|
||||
conversation = self.portal.doc1.restrictedTraverse("@@conversation_view")
|
||||
|
||||
# We have to allow discussion on Document content type, since
|
||||
# otherwise allow_discussion will always return False
|
||||
portal_types = getToolByName(self.portal, 'portal_types')
|
||||
document_fti = getattr(portal_types, 'Document')
|
||||
portal_types = getToolByName(self.portal, "portal_types")
|
||||
document_fti = getattr(portal_types, "Document")
|
||||
document_fti.manage_changeProperties(allow_discussion=True)
|
||||
|
||||
# Check if conversation is enabled now
|
||||
@ -249,14 +241,14 @@ class ConversationTest(unittest.TestCase):
|
||||
|
||||
def test_allow_discussion_for_news_items(self):
|
||||
|
||||
self.typetool.constructContent('News Item', self.portal, 'newsitem')
|
||||
self.typetool.constructContent("News Item", self.portal, "newsitem")
|
||||
newsitem = self.portal.newsitem
|
||||
conversation = newsitem.restrictedTraverse('@@conversation_view')
|
||||
conversation = newsitem.restrictedTraverse("@@conversation_view")
|
||||
|
||||
# We have to allow discussion on Document content type, since
|
||||
# otherwise allow_discussion will always return False
|
||||
portal_types = getToolByName(self.portal, 'portal_types')
|
||||
document_fti = getattr(portal_types, 'News Item')
|
||||
portal_types = getToolByName(self.portal, "portal_types")
|
||||
document_fti = getattr(portal_types, "News Item")
|
||||
document_fti.manage_changeProperties(allow_discussion=True)
|
||||
|
||||
# Check if conversation is enabled now
|
||||
@ -278,23 +270,23 @@ class ConversationTest(unittest.TestCase):
|
||||
|
||||
# Create a conversation.
|
||||
conversation = self.portal.doc1.restrictedTraverse(
|
||||
'@@conversation_view',
|
||||
"@@conversation_view",
|
||||
)
|
||||
|
||||
# The Document content type is disabled by default
|
||||
self.assertEqual(conversation.enabled(), False)
|
||||
|
||||
# Allow discussion on Document content type
|
||||
portal_types = getToolByName(self.portal, 'portal_types')
|
||||
document_fti = getattr(portal_types, 'Document')
|
||||
portal_types = getToolByName(self.portal, "portal_types")
|
||||
document_fti = getattr(portal_types, "Document")
|
||||
document_fti.manage_changeProperties(allow_discussion=True)
|
||||
|
||||
# Check if conversation is enabled now
|
||||
self.assertEqual(conversation.enabled(), True)
|
||||
|
||||
# Disallow discussion on Document content type
|
||||
portal_types = getToolByName(self.portal, 'portal_types')
|
||||
document_fti = getattr(portal_types, 'Document')
|
||||
portal_types = getToolByName(self.portal, "portal_types")
|
||||
document_fti = getattr(portal_types, "Document")
|
||||
document_fti.manage_changeProperties(allow_discussion=False)
|
||||
|
||||
# Check if conversation is enabled now
|
||||
@ -306,17 +298,17 @@ class ConversationTest(unittest.TestCase):
|
||||
# plone.app.contenttypes does not have this restriction any longer.
|
||||
|
||||
# Create a folder
|
||||
self.typetool.constructContent('Folder', self.portal, 'f1')
|
||||
self.typetool.constructContent("Folder", self.portal, "f1")
|
||||
|
||||
# Usually we don't create a conversation on a folder
|
||||
conversation = self.portal.f1.restrictedTraverse('@@conversation_view')
|
||||
conversation = self.portal.f1.restrictedTraverse("@@conversation_view")
|
||||
|
||||
# Allow discussion for the folder
|
||||
self.portal.f1.allow_discussion = True
|
||||
|
||||
# Allow discussion on Folder content type
|
||||
portal_types = getToolByName(self.portal, 'portal_types')
|
||||
document_fti = getattr(portal_types, 'Folder')
|
||||
portal_types = getToolByName(self.portal, "portal_types")
|
||||
document_fti = getattr(portal_types, "Folder")
|
||||
document_fti.manage_changeProperties(allow_discussion=True)
|
||||
|
||||
self.assertTrue(conversation.enabled())
|
||||
@ -326,7 +318,7 @@ class ConversationTest(unittest.TestCase):
|
||||
|
||||
# Create a conversation.
|
||||
conversation = self.portal.doc1.restrictedTraverse(
|
||||
'@@conversation_view',
|
||||
"@@conversation_view",
|
||||
)
|
||||
|
||||
# Discussion is disallowed by default
|
||||
@ -351,13 +343,13 @@ class ConversationTest(unittest.TestCase):
|
||||
# Add a comment. Note: in real life, we always create comments via the
|
||||
# factory to allow different factories to be swapped in
|
||||
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
|
||||
new_id1 = conversation.addComment(comment1)
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.text = 'Comment text'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.text = "Comment text"
|
||||
|
||||
new_id2 = conversation.addComment(comment2)
|
||||
|
||||
@ -384,17 +376,17 @@ class ConversationTest(unittest.TestCase):
|
||||
self.assertTrue(comment2 in conversation.values())
|
||||
|
||||
# check if comment ids are in iterkeys
|
||||
self.assertTrue(new_id1 in six.iterkeys(conversation))
|
||||
self.assertTrue(new_id2 in six.iterkeys(conversation))
|
||||
self.assertFalse(123 in six.iterkeys(conversation))
|
||||
self.assertTrue(new_id1 in conversation.keys())
|
||||
self.assertTrue(new_id2 in conversation.keys())
|
||||
self.assertFalse(123 in conversation.keys())
|
||||
|
||||
# check if comment objects are in itervalues
|
||||
self.assertTrue(comment1 in six.itervalues(conversation))
|
||||
self.assertTrue(comment2 in six.itervalues(conversation))
|
||||
self.assertTrue(comment1 in conversation.values())
|
||||
self.assertTrue(comment2 in conversation.values())
|
||||
|
||||
# check if iteritems returns (key, comment object) pairs
|
||||
self.assertTrue((new_id1, comment1) in six.iteritems(conversation))
|
||||
self.assertTrue((new_id2, comment2) in six.iteritems(conversation))
|
||||
self.assertTrue((new_id1, comment1) in conversation.items())
|
||||
self.assertTrue((new_id2, comment2) in conversation.items())
|
||||
|
||||
# TODO test acquisition wrapping # noqa T000
|
||||
# self.assertTrue(aq_base(aq_parent(comment1)) is conversation)
|
||||
@ -408,14 +400,14 @@ class ConversationTest(unittest.TestCase):
|
||||
# comments via the factory to allow different factories to be
|
||||
# swapped in
|
||||
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.text = 'Comment text'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.text = "Comment text"
|
||||
|
||||
comment3 = createObject('plone.Comment')
|
||||
comment3.text = 'Comment text'
|
||||
comment3 = createObject("plone.Comment")
|
||||
comment3.text = "Comment text"
|
||||
|
||||
conversation.addComment(comment1)
|
||||
conversation.addComment(comment2)
|
||||
@ -437,49 +429,49 @@ class ConversationTest(unittest.TestCase):
|
||||
# Note: in real life, we always create
|
||||
# comments via the factory to allow different factories to be
|
||||
# swapped in
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1.author_username = 'Jim'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
comment1.author_username = "Jim"
|
||||
conversation.addComment(comment1)
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.text = 'Comment text'
|
||||
comment2.author_username = 'Joe'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.text = "Comment text"
|
||||
comment2.author_username = "Joe"
|
||||
conversation.addComment(comment2)
|
||||
|
||||
comment3 = createObject('plone.Comment')
|
||||
comment3.text = 'Comment text'
|
||||
comment3.author_username = 'Jack'
|
||||
comment3 = createObject("plone.Comment")
|
||||
comment3.text = "Comment text"
|
||||
comment3.author_username = "Jack"
|
||||
new_comment3_id = conversation.addComment(comment3)
|
||||
|
||||
comment4 = createObject('plone.Comment')
|
||||
comment4.text = 'Comment text'
|
||||
comment4.author_username = 'Jack'
|
||||
comment4 = createObject("plone.Comment")
|
||||
comment4.text = "Comment text"
|
||||
comment4.author_username = "Jack"
|
||||
new_comment4_id = conversation.addComment(comment4)
|
||||
|
||||
# check if all commentators are in the commentators list
|
||||
self.assertEqual(conversation.total_comments(), 4)
|
||||
self.assertTrue('Jim' in conversation.commentators)
|
||||
self.assertTrue('Joe' in conversation.commentators)
|
||||
self.assertTrue('Jack' in conversation.commentators)
|
||||
self.assertTrue("Jim" in conversation.commentators)
|
||||
self.assertTrue("Joe" in conversation.commentators)
|
||||
self.assertTrue("Jack" in conversation.commentators)
|
||||
|
||||
# remove the comment from Jack
|
||||
del conversation[new_comment3_id]
|
||||
|
||||
# check if Jack is still in the commentators list (since
|
||||
# he had added two comments)
|
||||
self.assertTrue('Jim' in conversation.commentators)
|
||||
self.assertTrue('Joe' in conversation.commentators)
|
||||
self.assertTrue('Jack' in conversation.commentators)
|
||||
self.assertTrue("Jim" in conversation.commentators)
|
||||
self.assertTrue("Joe" in conversation.commentators)
|
||||
self.assertTrue("Jack" in conversation.commentators)
|
||||
self.assertEqual(conversation.total_comments(), 3)
|
||||
|
||||
# remove the second comment from Jack
|
||||
del conversation[new_comment4_id]
|
||||
|
||||
# check if Jack has been removed from the commentators list
|
||||
self.assertTrue('Jim' in conversation.commentators)
|
||||
self.assertTrue('Joe' in conversation.commentators)
|
||||
self.assertFalse('Jack' in conversation.commentators)
|
||||
self.assertTrue("Jim" in conversation.commentators)
|
||||
self.assertTrue("Joe" in conversation.commentators)
|
||||
self.assertFalse("Jack" in conversation.commentators)
|
||||
self.assertEqual(conversation.total_comments(), 2)
|
||||
|
||||
def test_last_comment_date(self):
|
||||
@ -494,29 +486,29 @@ class ConversationTest(unittest.TestCase):
|
||||
# Note: in real life, we always create
|
||||
# comments via the factory to allow different factories to be
|
||||
# swapped in
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
comment1.creation_date = datetime.utcnow() - timedelta(4)
|
||||
conversation.addComment(comment1)
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.text = 'Comment text'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.text = "Comment text"
|
||||
comment2.creation_date = datetime.utcnow() - timedelta(2)
|
||||
new_comment2_id = conversation.addComment(comment2)
|
||||
|
||||
comment3 = createObject('plone.Comment')
|
||||
comment3.text = 'Comment text'
|
||||
comment3 = createObject("plone.Comment")
|
||||
comment3.text = "Comment text"
|
||||
comment3.creation_date = datetime.utcnow() - timedelta(1)
|
||||
new_comment3_id = conversation.addComment(comment3)
|
||||
|
||||
# check if the latest comment is exactly one day old
|
||||
self.assertTrue(
|
||||
conversation.last_comment_date < datetime.utcnow() -
|
||||
timedelta(hours=23, minutes=59, seconds=59),
|
||||
conversation.last_comment_date
|
||||
< datetime.utcnow() - timedelta(hours=23, minutes=59, seconds=59),
|
||||
)
|
||||
self.assertTrue(
|
||||
conversation.last_comment_date >
|
||||
datetime.utcnow() - timedelta(days=1, seconds=1),
|
||||
conversation.last_comment_date
|
||||
> datetime.utcnow() - timedelta(days=1, seconds=1),
|
||||
)
|
||||
|
||||
# remove the latest comment
|
||||
@ -525,12 +517,12 @@ class ConversationTest(unittest.TestCase):
|
||||
# check if the latest comment has been updated
|
||||
# the latest comment should be exactly two days old
|
||||
self.assertTrue(
|
||||
conversation.last_comment_date < datetime.utcnow() -
|
||||
timedelta(days=1, hours=23, minutes=59, seconds=59),
|
||||
conversation.last_comment_date
|
||||
< datetime.utcnow() - timedelta(days=1, hours=23, minutes=59, seconds=59),
|
||||
)
|
||||
self.assertTrue(
|
||||
conversation.last_comment_date > datetime.utcnow() -
|
||||
timedelta(days=2, seconds=1),
|
||||
conversation.last_comment_date
|
||||
> datetime.utcnow() - timedelta(days=2, seconds=1),
|
||||
)
|
||||
|
||||
# remove the latest comment again
|
||||
@ -539,12 +531,12 @@ class ConversationTest(unittest.TestCase):
|
||||
# check if the latest comment has been updated
|
||||
# the latest comment should be exactly four days old
|
||||
self.assertTrue(
|
||||
conversation.last_comment_date < datetime.utcnow() -
|
||||
timedelta(days=3, hours=23, minutes=59, seconds=59),
|
||||
conversation.last_comment_date
|
||||
< datetime.utcnow() - timedelta(days=3, hours=23, minutes=59, seconds=59),
|
||||
)
|
||||
self.assertTrue(
|
||||
conversation.last_comment_date > datetime.utcnow() -
|
||||
timedelta(days=4, seconds=2),
|
||||
conversation.last_comment_date
|
||||
> datetime.utcnow() - timedelta(days=4, seconds=2),
|
||||
)
|
||||
|
||||
def test_get_comments_full(self):
|
||||
@ -572,23 +564,23 @@ class ConversationTest(unittest.TestCase):
|
||||
# +- Comment 2_1
|
||||
|
||||
# Create all comments
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
|
||||
comment1_1 = createObject('plone.Comment')
|
||||
comment1_1.text = 'Comment text'
|
||||
comment1_1 = createObject("plone.Comment")
|
||||
comment1_1.text = "Comment text"
|
||||
|
||||
comment1_1_1 = createObject('plone.Comment')
|
||||
comment1_1_1.text = 'Comment text'
|
||||
comment1_1_1 = createObject("plone.Comment")
|
||||
comment1_1_1.text = "Comment text"
|
||||
|
||||
comment1_2 = createObject('plone.Comment')
|
||||
comment1_2.text = 'Comment text'
|
||||
comment1_2 = createObject("plone.Comment")
|
||||
comment1_2.text = "Comment text"
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.text = 'Comment text'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.text = "Comment text"
|
||||
|
||||
comment2_1 = createObject('plone.Comment')
|
||||
comment2_1.text = 'Comment text'
|
||||
comment2_1 = createObject("plone.Comment")
|
||||
comment2_1.text = "Comment text"
|
||||
|
||||
# Create the nested comment structure
|
||||
new_id_1 = conversation.addComment(comment1)
|
||||
@ -608,14 +600,17 @@ class ConversationTest(unittest.TestCase):
|
||||
|
||||
# Get threads
|
||||
|
||||
self.assertEqual([
|
||||
{'comment': comment1, 'depth': 0, 'id': new_id_1},
|
||||
{'comment': comment1_1, 'depth': 1, 'id': new_id_1_1},
|
||||
{'comment': comment1_1_1, 'depth': 2, 'id': new_id_1_1_1},
|
||||
{'comment': comment1_2, 'depth': 1, 'id': new_id_1_2},
|
||||
{'comment': comment2, 'depth': 0, 'id': new_id_2},
|
||||
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1},
|
||||
], list(conversation.getThreads()))
|
||||
self.assertEqual(
|
||||
[
|
||||
{"comment": comment1, "depth": 0, "id": new_id_1},
|
||||
{"comment": comment1_1, "depth": 1, "id": new_id_1_1},
|
||||
{"comment": comment1_1_1, "depth": 2, "id": new_id_1_1_1},
|
||||
{"comment": comment1_2, "depth": 1, "id": new_id_1_2},
|
||||
{"comment": comment2, "depth": 0, "id": new_id_2},
|
||||
{"comment": comment2_1, "depth": 1, "id": new_id_2_1},
|
||||
],
|
||||
list(conversation.getThreads()),
|
||||
)
|
||||
|
||||
def test_get_threads_batched(self):
|
||||
# TODO: test start, size, root and depth arguments to getThreads() # noqa T000
|
||||
@ -626,16 +621,16 @@ class ConversationTest(unittest.TestCase):
|
||||
# make sure we can traverse to conversations and get a URL and path
|
||||
|
||||
conversation = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default',
|
||||
"++conversation++default",
|
||||
)
|
||||
self.assertTrue(IConversation.providedBy(conversation))
|
||||
|
||||
self.assertEqual(
|
||||
('', 'plone', 'doc1', '++conversation++default'),
|
||||
("", "plone", "doc1", "++conversation++default"),
|
||||
conversation.getPhysicalPath(),
|
||||
)
|
||||
self.assertEqual(
|
||||
'http://nohost/plone/doc1/++conversation++default',
|
||||
"http://nohost/plone/doc1/++conversation++default",
|
||||
conversation.absolute_url(),
|
||||
)
|
||||
|
||||
@ -644,7 +639,7 @@ class ConversationTest(unittest.TestCase):
|
||||
# can't be converted to int
|
||||
|
||||
conversation = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/ThisCantBeRight',
|
||||
"++conversation++default/ThisCantBeRight",
|
||||
)
|
||||
self.assertEqual(conversation, None)
|
||||
|
||||
@ -658,17 +653,16 @@ class ConversationTest(unittest.TestCase):
|
||||
self.assertTrue(conversation.__parent__)
|
||||
self.assertTrue(aq_parent(conversation))
|
||||
|
||||
self.assertEqual(conversation.__parent__.getId(), 'doc1')
|
||||
self.assertEqual(conversation.__parent__.getId(), "doc1")
|
||||
|
||||
def test_discussion_item_not_in_bad_types(self):
|
||||
self.assertFalse('Discussion Item' in BAD_TYPES)
|
||||
self.assertFalse("Discussion Item" in BAD_TYPES)
|
||||
|
||||
def test_no_comment(self):
|
||||
IConversation(self.portal.doc1)
|
||||
# Make sure no conversation has been created
|
||||
self.assertTrue(
|
||||
'plone.app.discussion:conversation' not in
|
||||
IAnnotations(self.portal.doc1),
|
||||
"plone.app.discussion:conversation" not in IAnnotations(self.portal.doc1),
|
||||
)
|
||||
|
||||
|
||||
@ -677,21 +671,20 @@ class ConversationEnabledForDexterityTypesTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
interface.alsoProvides(
|
||||
self.portal.REQUEST,
|
||||
interfaces.IDiscussionLayer,
|
||||
IDiscussionLayer,
|
||||
)
|
||||
|
||||
if DEXTERITY:
|
||||
interface.alsoProvides(
|
||||
self.portal.doc1,
|
||||
IDexterityContent,
|
||||
)
|
||||
interface.alsoProvides(
|
||||
self.portal.doc1,
|
||||
IDexterityContent,
|
||||
)
|
||||
|
||||
def _makeOne(self, *args, **kw):
|
||||
return self.portal.doc1.restrictedTraverse('@@conversation_view')
|
||||
return self.portal.doc1.restrictedTraverse("@@conversation_view")
|
||||
|
||||
def _globally_enable_discussion(self, value):
|
||||
registry = queryUtility(IRegistry)
|
||||
@ -699,43 +692,38 @@ class ConversationEnabledForDexterityTypesTest(unittest.TestCase):
|
||||
settings.globally_enabled = value
|
||||
|
||||
def _enable_discussion_on_portal_type(self, portal_type, allow_discussion):
|
||||
portal_types = getToolByName(self.portal, 'portal_types')
|
||||
portal_types = getToolByName(self.portal, "portal_types")
|
||||
document_fti = getattr(portal_types, portal_type)
|
||||
document_fti.manage_changeProperties(allow_discussion=allow_discussion)
|
||||
|
||||
def test_conversation_is_not_enabled_by_default(self):
|
||||
if DEXTERITY:
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertFalse(conversation.enabled())
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertFalse(conversation.enabled())
|
||||
|
||||
def test_conversation_is_not_enabled_by_default_on_portal_type(self):
|
||||
if DEXTERITY:
|
||||
self._globally_enable_discussion(True)
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertFalse(conversation.enabled())
|
||||
self._globally_enable_discussion(True)
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertFalse(conversation.enabled())
|
||||
|
||||
def test_conversation_needs_to_be_enabled_globally_and_for_type(self):
|
||||
if DEXTERITY:
|
||||
self._globally_enable_discussion(True)
|
||||
self._enable_discussion_on_portal_type('Document', True)
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertTrue(conversation.enabled())
|
||||
self._globally_enable_discussion(True)
|
||||
self._enable_discussion_on_portal_type("Document", True)
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertTrue(conversation.enabled())
|
||||
|
||||
def test_disable_discussion(self):
|
||||
if DEXTERITY:
|
||||
self._globally_enable_discussion(True)
|
||||
self._enable_discussion_on_portal_type('Document', True)
|
||||
self.portal.doc1.allow_discussion = False
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertFalse(conversation.enabled())
|
||||
self._globally_enable_discussion(True)
|
||||
self._enable_discussion_on_portal_type("Document", True)
|
||||
self.portal.doc1.allow_discussion = False
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertFalse(conversation.enabled())
|
||||
|
||||
def test_enable_discussion(self):
|
||||
if DEXTERITY:
|
||||
self._globally_enable_discussion(True)
|
||||
self._enable_discussion_on_portal_type('Document', True)
|
||||
self.portal.doc1.allow_discussion = True
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertTrue(conversation.enabled())
|
||||
self._globally_enable_discussion(True)
|
||||
self._enable_discussion_on_portal_type("Document", True)
|
||||
self.portal.doc1.allow_discussion = True
|
||||
conversation = self._makeOne(self.portal.doc1)
|
||||
self.assertTrue(conversation.enabled())
|
||||
|
||||
|
||||
class RepliesTest(unittest.TestCase):
|
||||
@ -745,11 +733,11 @@ class RepliesTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
|
||||
workflow = self.portal.portal_workflow
|
||||
workflow.doActionFor(self.portal.doc1, 'publish')
|
||||
workflow.doActionFor(self.portal.doc1, "publish")
|
||||
|
||||
def test_add_comment(self):
|
||||
# Add comments to a ConversationReplies adapter
|
||||
@ -760,8 +748,8 @@ class RepliesTest(unittest.TestCase):
|
||||
|
||||
replies = IReplies(conversation)
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
new_id = replies.addComment(comment)
|
||||
|
||||
@ -787,8 +775,8 @@ class RepliesTest(unittest.TestCase):
|
||||
replies = IReplies(conversation)
|
||||
|
||||
# Add a comment.
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
new_id = replies.addComment(comment)
|
||||
|
||||
@ -826,39 +814,39 @@ class RepliesTest(unittest.TestCase):
|
||||
# +- Comment 2_1
|
||||
|
||||
# Create all comments
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment text'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment text"
|
||||
|
||||
comment1_1 = createObject('plone.Comment')
|
||||
comment1_1.text = 'Comment text'
|
||||
comment1_1 = createObject("plone.Comment")
|
||||
comment1_1.text = "Comment text"
|
||||
|
||||
comment1_1_1 = createObject('plone.Comment')
|
||||
comment1_1_1.text = 'Comment text'
|
||||
comment1_1_1 = createObject("plone.Comment")
|
||||
comment1_1_1.text = "Comment text"
|
||||
|
||||
comment1_2 = createObject('plone.Comment')
|
||||
comment1_2.text = 'Comment text'
|
||||
comment1_2 = createObject("plone.Comment")
|
||||
comment1_2.text = "Comment text"
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.text = 'Comment text'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.text = "Comment text"
|
||||
|
||||
comment2_1 = createObject('plone.Comment')
|
||||
comment2_1.text = 'Comment text'
|
||||
comment2_1 = createObject("plone.Comment")
|
||||
comment2_1.text = "Comment text"
|
||||
|
||||
# Create the nested comment structure
|
||||
new_id_1 = replies.addComment(comment1)
|
||||
comment1 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id_1),
|
||||
f"++conversation++default/{new_id_1}",
|
||||
)
|
||||
replies_to_comment1 = IReplies(comment1)
|
||||
new_id_2 = replies.addComment(comment2)
|
||||
comment2 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id_2),
|
||||
f"++conversation++default/{new_id_2}",
|
||||
)
|
||||
replies_to_comment2 = IReplies(comment2)
|
||||
|
||||
new_id_1_1 = replies_to_comment1.addComment(comment1_1)
|
||||
comment1_1 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id_1_1),
|
||||
f"++conversation++default/{new_id_1_1}",
|
||||
)
|
||||
replies_to_comment1_1 = IReplies(comment1_1)
|
||||
replies_to_comment1_1.addComment(comment1_1_1)
|
||||
|
@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IReplies
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from ..interfaces import IConversation
|
||||
from ..interfaces import IReplies
|
||||
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from Zope2.App import zcml
|
||||
@ -18,9 +17,9 @@ import unittest
|
||||
#
|
||||
|
||||
|
||||
class EventsRegistry(object):
|
||||
""" Fake registry to be used while testing discussion events
|
||||
"""
|
||||
class EventsRegistry:
|
||||
"""Fake registry to be used while testing discussion events"""
|
||||
|
||||
commentAdded = False
|
||||
commentModified = False
|
||||
commentRemoved = False
|
||||
@ -28,6 +27,7 @@ class EventsRegistry(object):
|
||||
replyModified = False
|
||||
replyRemoved = False
|
||||
|
||||
|
||||
#
|
||||
# Fake event handlers
|
||||
#
|
||||
@ -63,19 +63,19 @@ def reply_removed(doc, evt):
|
||||
|
||||
|
||||
class CommentEventsTest(unittest.TestCase):
|
||||
""" Test custom comments events
|
||||
"""
|
||||
"""Test custom comments events"""
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
|
||||
# Setup sandbox
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
self.registry = EventsRegistry
|
||||
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.document = self.portal['doc1']
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.document = self.portal["doc1"]
|
||||
|
||||
#
|
||||
# Subscribers
|
||||
@ -104,23 +104,23 @@ class CommentEventsTest(unittest.TestCase):
|
||||
|
||||
</configure>
|
||||
"""
|
||||
zcml.load_config('configure.zcml', Products.Five)
|
||||
zcml.load_config("configure.zcml", Products.Five)
|
||||
zcml.load_string(configure)
|
||||
|
||||
def test_addEvent(self):
|
||||
self.assertFalse(self.registry.commentAdded)
|
||||
comment = createObject('plone.Comment')
|
||||
comment = createObject("plone.Comment")
|
||||
conversation = IConversation(self.document)
|
||||
conversation.addComment(comment)
|
||||
self.assertTrue(self.registry.commentAdded)
|
||||
|
||||
def test_modifyEvent(self):
|
||||
self.assertFalse(self.registry.commentModified)
|
||||
comment = createObject('plone.Comment')
|
||||
comment = createObject("plone.Comment")
|
||||
conversation = IConversation(self.document)
|
||||
new_id = conversation.addComment(comment)
|
||||
comment = self.document.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id),
|
||||
f"++conversation++default/{new_id}",
|
||||
)
|
||||
comment.text = "foo"
|
||||
notify(ObjectModifiedEvent(comment))
|
||||
@ -128,7 +128,7 @@ class CommentEventsTest(unittest.TestCase):
|
||||
|
||||
def test_removedEvent(self):
|
||||
self.assertFalse(self.registry.commentRemoved)
|
||||
comment = createObject('plone.Comment')
|
||||
comment = createObject("plone.Comment")
|
||||
conversation = IConversation(self.document)
|
||||
cid = conversation.addComment(comment)
|
||||
del conversation[cid]
|
||||
@ -136,17 +136,17 @@ class CommentEventsTest(unittest.TestCase):
|
||||
|
||||
|
||||
class RepliesEventsTest(unittest.TestCase):
|
||||
""" Test custom replies events
|
||||
"""
|
||||
"""Test custom replies events"""
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
self.registry = EventsRegistry
|
||||
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.document = self.portal['doc1']
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.document = self.portal["doc1"]
|
||||
|
||||
#
|
||||
# Subscribers
|
||||
@ -175,7 +175,7 @@ class RepliesEventsTest(unittest.TestCase):
|
||||
|
||||
</configure>
|
||||
"""
|
||||
zcml.load_config('configure.zcml', Products.Five)
|
||||
zcml.load_config("configure.zcml", Products.Five)
|
||||
zcml.load_string(configure)
|
||||
|
||||
def test_addEvent(self):
|
||||
@ -184,15 +184,15 @@ class RepliesEventsTest(unittest.TestCase):
|
||||
conversation = IConversation(self.document)
|
||||
replies = IReplies(conversation)
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
new_id = replies.addComment(comment)
|
||||
comment = self.document.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id),
|
||||
f"++conversation++default/{new_id}",
|
||||
)
|
||||
|
||||
re_comment = createObject('plone.Comment')
|
||||
re_comment.text = 'Comment text'
|
||||
re_comment = createObject("plone.Comment")
|
||||
re_comment.text = "Comment text"
|
||||
|
||||
replies = IReplies(comment)
|
||||
replies.addComment(re_comment)
|
||||
@ -204,14 +204,14 @@ class RepliesEventsTest(unittest.TestCase):
|
||||
|
||||
conversation = IConversation(self.document)
|
||||
replies = IReplies(conversation)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment_id = replies.addComment(comment)
|
||||
comment = self.document.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(comment_id),
|
||||
f"++conversation++default/{comment_id}",
|
||||
)
|
||||
re_comment = createObject('plone.Comment')
|
||||
re_comment.text = 'Comment text'
|
||||
re_comment = createObject("plone.Comment")
|
||||
re_comment.text = "Comment text"
|
||||
replies = IReplies(comment)
|
||||
new_id = replies.addComment(re_comment)
|
||||
reply = replies[new_id]
|
||||
@ -225,15 +225,15 @@ class RepliesEventsTest(unittest.TestCase):
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
replies = IReplies(conversation)
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
new_id = replies.addComment(comment)
|
||||
comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id),
|
||||
f"++conversation++default/{new_id}",
|
||||
)
|
||||
|
||||
re_comment = createObject('plone.Comment')
|
||||
re_comment.text = 'Comment text'
|
||||
re_comment = createObject("plone.Comment")
|
||||
re_comment.text = "Comment text"
|
||||
replies = IReplies(comment)
|
||||
new_re_id = replies.addComment(re_comment)
|
||||
|
||||
|
@ -1,9 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Functional Doctests for plone.app.discussion.
|
||||
|
||||
These test are only triggered when Plone 4 (and plone.testing) is installed.
|
||||
"""
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING # noqa
|
||||
from ..testing import PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING # noqa
|
||||
from plone.testing import layered
|
||||
|
||||
import doctest
|
||||
@ -12,29 +11,29 @@ import unittest
|
||||
|
||||
|
||||
optionflags = (
|
||||
doctest.ELLIPSIS |
|
||||
doctest.NORMALIZE_WHITESPACE |
|
||||
doctest.REPORT_ONLY_FIRST_FAILURE
|
||||
doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_ONLY_FIRST_FAILURE
|
||||
)
|
||||
normal_testfiles = [
|
||||
'functional_test_comments.txt',
|
||||
'functional_test_comment_review_workflow.txt',
|
||||
"functional_test_comments.txt",
|
||||
"functional_test_comment_review_workflow.txt",
|
||||
]
|
||||
|
||||
|
||||
def test_suite():
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTests([
|
||||
layered(
|
||||
doctest.DocFileSuite(
|
||||
test,
|
||||
optionflags=optionflags,
|
||||
globs={
|
||||
'pprint': pprint.pprint,
|
||||
}
|
||||
),
|
||||
layer=PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING,
|
||||
)
|
||||
for test in normal_testfiles
|
||||
])
|
||||
suite.addTests(
|
||||
[
|
||||
layered(
|
||||
doctest.DocFileSuite(
|
||||
test,
|
||||
optionflags=optionflags,
|
||||
globs={
|
||||
"pprint": pprint.pprint,
|
||||
},
|
||||
),
|
||||
layer=PLONE_APP_DISCUSSION_FUNCTIONAL_TESTING,
|
||||
)
|
||||
for test in normal_testfiles
|
||||
]
|
||||
)
|
||||
return suite
|
||||
|
@ -1,11 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test for the plone.app.discussion indexers
|
||||
"""
|
||||
from DateTime import DateTime
|
||||
from .. import catalog
|
||||
from ..interfaces import IConversation
|
||||
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from datetime import datetime
|
||||
from plone.app.discussion import catalog
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from DateTime import DateTime
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from plone.indexer.delegate import DelegatingIndexerFactory
|
||||
@ -26,41 +25,40 @@ sed diam voluptua. At [...]"""
|
||||
|
||||
|
||||
class ConversationIndexersTest(unittest.TestCase):
|
||||
"""Conversation Indexer Tests
|
||||
"""
|
||||
"""Conversation Indexer Tests"""
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
|
||||
workflow = self.portal.portal_workflow
|
||||
workflow.doActionFor(self.portal.doc1, 'publish')
|
||||
workflow.doActionFor(self.portal.doc1, "publish")
|
||||
|
||||
# Create a conversation.
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.text = 'Comment Text'
|
||||
comment1.creator = 'jim'
|
||||
comment1.author_username = 'Jim'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.text = "Comment Text"
|
||||
comment1.creator = "jim"
|
||||
comment1.author_username = "Jim"
|
||||
comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12)
|
||||
comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12)
|
||||
self.new_id1 = conversation.addComment(comment1)
|
||||
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.text = 'Comment Text'
|
||||
comment2.creator = 'emma'
|
||||
comment2.author_username = 'Emma'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.text = "Comment Text"
|
||||
comment2.creator = "emma"
|
||||
comment2.author_username = "Emma"
|
||||
comment2.creation_date = datetime(2007, 12, 13, 4, 18, 12)
|
||||
comment2.modification_date = datetime(2007, 12, 13, 4, 18, 12)
|
||||
self.new_id2 = conversation.addComment(comment2)
|
||||
|
||||
comment3 = createObject('plone.Comment')
|
||||
comment3.text = 'Comment Text'
|
||||
comment3.creator = 'lukas'
|
||||
comment3.author_username = 'Lukas'
|
||||
comment3 = createObject("plone.Comment")
|
||||
comment3.text = "Comment Text"
|
||||
comment3.creator = "lukas"
|
||||
comment3.author_username = "Lukas"
|
||||
comment3.creation_date = datetime(2009, 4, 12, 11, 12, 12)
|
||||
comment3.modification_date = datetime(2009, 4, 12, 11, 12, 12)
|
||||
self.new_id3 = conversation.addComment(comment3)
|
||||
@ -68,10 +66,12 @@ class ConversationIndexersTest(unittest.TestCase):
|
||||
self.conversation = conversation
|
||||
|
||||
def test_conversation_total_comments(self):
|
||||
self.assertTrue(isinstance(
|
||||
catalog.total_comments,
|
||||
DelegatingIndexerFactory,
|
||||
))
|
||||
self.assertTrue(
|
||||
isinstance(
|
||||
catalog.total_comments,
|
||||
DelegatingIndexerFactory,
|
||||
)
|
||||
)
|
||||
self.assertEqual(catalog.total_comments(self.portal.doc1)(), 3)
|
||||
del self.conversation[self.new_id1]
|
||||
self.assertEqual(catalog.total_comments(self.portal.doc1)(), 2)
|
||||
@ -80,10 +80,12 @@ class ConversationIndexersTest(unittest.TestCase):
|
||||
self.assertEqual(catalog.total_comments(self.portal.doc1)(), 0)
|
||||
|
||||
def test_conversation_last_comment_date(self):
|
||||
self.assertTrue(isinstance(
|
||||
catalog.last_comment_date,
|
||||
DelegatingIndexerFactory,
|
||||
))
|
||||
self.assertTrue(
|
||||
isinstance(
|
||||
catalog.last_comment_date,
|
||||
DelegatingIndexerFactory,
|
||||
)
|
||||
)
|
||||
self.assertEqual(
|
||||
catalog.last_comment_date(self.portal.doc1)(),
|
||||
datetime(2009, 4, 12, 11, 12, 12),
|
||||
@ -110,8 +112,8 @@ class CommentIndexersTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
|
||||
# Create a conversation. In this case we doesn't assign it to an
|
||||
# object, as we just want to check the Conversation object API.
|
||||
@ -120,10 +122,10 @@ class CommentIndexersTest(unittest.TestCase):
|
||||
# Add a comment. Note: in real life, we always create comments via the
|
||||
# factory to allow different factories to be swapped in
|
||||
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Lorem ipsum dolor sit amet.'
|
||||
comment.creator = 'jim'
|
||||
comment.author_name = 'Jim'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Lorem ipsum dolor sit amet."
|
||||
comment.creator = "jim"
|
||||
comment.author_name = "Jim"
|
||||
comment.creation_date = datetime(2006, 9, 17, 14, 18, 12)
|
||||
comment.modification_date = datetime(2008, 3, 12, 7, 32, 52)
|
||||
|
||||
@ -132,60 +134,61 @@ class CommentIndexersTest(unittest.TestCase):
|
||||
self.conversation = conversation
|
||||
|
||||
def test_title(self):
|
||||
self.assertEqual(catalog.title(self.comment)(), 'Jim on Document 1')
|
||||
self.assertEqual(catalog.title(self.comment)(), "Jim on Document 1")
|
||||
self.assertTrue(isinstance(catalog.title, DelegatingIndexerFactory))
|
||||
|
||||
def test_description(self):
|
||||
self.assertEqual(
|
||||
catalog.description(self.comment)(),
|
||||
'Lorem ipsum dolor sit amet.',
|
||||
"Lorem ipsum dolor sit amet.",
|
||||
)
|
||||
self.assertTrue(
|
||||
isinstance(catalog.description, DelegatingIndexerFactory))
|
||||
self.assertTrue(isinstance(catalog.description, DelegatingIndexerFactory))
|
||||
|
||||
def test_description_long(self):
|
||||
# Create a 50 word comment and make sure the description returns
|
||||
# only the first 25 words
|
||||
comment_long = createObject('plone.Comment')
|
||||
comment_long.title = 'Long Comment'
|
||||
comment_long = createObject("plone.Comment")
|
||||
comment_long.title = "Long Comment"
|
||||
comment_long.text = LONG_TEXT
|
||||
|
||||
self.conversation.addComment(comment_long)
|
||||
self.assertEqual(
|
||||
catalog.description(comment_long)(),
|
||||
LONG_TEXT_CUT.replace('\n', ' '),
|
||||
LONG_TEXT_CUT.replace("\n", " "),
|
||||
)
|
||||
|
||||
def test_dates(self):
|
||||
# Test if created, modified, effective etc. are set correctly
|
||||
self.assertEqual(
|
||||
catalog.created(self.comment)(),
|
||||
DateTime(2006, 9, 17, 14, 18, 12, 'GMT'),
|
||||
DateTime(2006, 9, 17, 14, 18, 12, "GMT"),
|
||||
)
|
||||
self.assertEqual(
|
||||
catalog.effective(self.comment)(),
|
||||
DateTime(2006, 9, 17, 14, 18, 12, 'GMT'),
|
||||
DateTime(2006, 9, 17, 14, 18, 12, "GMT"),
|
||||
)
|
||||
self.assertEqual(
|
||||
catalog.modified(self.comment)(),
|
||||
DateTime(2008, 3, 12, 7, 32, 52, 'GMT'),
|
||||
DateTime(2008, 3, 12, 7, 32, 52, "GMT"),
|
||||
)
|
||||
|
||||
def test_searchable_text(self):
|
||||
# Test if searchable text is a concatenation of title and comment text
|
||||
self.assertEqual(
|
||||
catalog.searchable_text(self.comment)(),
|
||||
('Lorem ipsum dolor sit amet.'),
|
||||
("Lorem ipsum dolor sit amet."),
|
||||
)
|
||||
self.assertTrue(
|
||||
isinstance(
|
||||
catalog.searchable_text,
|
||||
DelegatingIndexerFactory,
|
||||
)
|
||||
)
|
||||
self.assertTrue(isinstance(
|
||||
catalog.searchable_text,
|
||||
DelegatingIndexerFactory,
|
||||
))
|
||||
|
||||
def test_creator(self):
|
||||
self.assertEqual(catalog.creator(self.comment)(), ('jim'))
|
||||
self.assertEqual(catalog.creator(self.comment)(), ("jim"))
|
||||
|
||||
def test_in_response_to(self):
|
||||
# make sure in_response_to returns the title or id of the content
|
||||
# object the comment was added to
|
||||
self.assertEqual(catalog.in_response_to(self.comment)(), 'Document 1')
|
||||
self.assertEqual(catalog.in_response_to(self.comment)(), "Document 1")
|
||||
|
@ -1,11 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.browser.moderation import BulkActionsView
|
||||
from plone.app.discussion.browser.moderation import DeleteComment
|
||||
from plone.app.discussion.browser.moderation import CommentTransition
|
||||
from plone.app.discussion.browser.moderation import View
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from ..browser.moderation import BulkActionsView
|
||||
from ..browser.moderation import CommentTransition
|
||||
from ..browser.moderation import DeleteComment
|
||||
from ..browser.moderation import View
|
||||
from ..interfaces import IConversation
|
||||
from ..interfaces import IDiscussionSettings
|
||||
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from plone.registry.interfaces import IRegistry
|
||||
@ -21,59 +20,57 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.app = self.layer['app']
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.wf = getToolByName(self.portal,
|
||||
'portal_workflow',
|
||||
None)
|
||||
self.app = self.layer["app"]
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.wf = getToolByName(self.portal, "portal_workflow", None)
|
||||
self.context = self.portal
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
'comment_review_workflow',
|
||||
("Discussion Item",),
|
||||
"comment_review_workflow",
|
||||
)
|
||||
self.wf_tool = self.portal.portal_workflow
|
||||
# Add a conversation with three comments
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.title = 'Comment 1'
|
||||
comment1.text = 'Comment text'
|
||||
comment1.Creator = 'Jim'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.title = "Comment 1"
|
||||
comment1.text = "Comment text"
|
||||
comment1.Creator = "Jim"
|
||||
new_id_1 = conversation.addComment(comment1)
|
||||
self.comment1 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id_1),
|
||||
f"++conversation++default/{new_id_1}",
|
||||
)
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.title = 'Comment 2'
|
||||
comment2.text = 'Comment text'
|
||||
comment2.Creator = 'Joe'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.title = "Comment 2"
|
||||
comment2.text = "Comment text"
|
||||
comment2.Creator = "Joe"
|
||||
new_id_2 = conversation.addComment(comment2)
|
||||
self.comment2 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id_2),
|
||||
f"++conversation++default/{new_id_2}",
|
||||
)
|
||||
comment3 = createObject('plone.Comment')
|
||||
comment3.title = 'Comment 3'
|
||||
comment3.text = 'Comment text'
|
||||
comment3.Creator = 'Emma'
|
||||
comment3 = createObject("plone.Comment")
|
||||
comment3.title = "Comment 3"
|
||||
comment3.text = "Comment text"
|
||||
comment3.Creator = "Emma"
|
||||
new_id_3 = conversation.addComment(comment3)
|
||||
self.comment3 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id_3),
|
||||
f"++conversation++default/{new_id_3}",
|
||||
)
|
||||
self.conversation = conversation
|
||||
|
||||
def test_default_bulkaction(self):
|
||||
# Make sure no error is raised when no bulk actions has been supplied
|
||||
self.request.set('form.select.BulkAction', '-1')
|
||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||
self.request.set("form.select.BulkAction", "-1")
|
||||
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
|
||||
|
||||
view = BulkActionsView(self.portal, self.request)
|
||||
|
||||
self.assertFalse(view())
|
||||
|
||||
def test_publish(self):
|
||||
self.request.set('form.select.BulkAction', 'publish')
|
||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||
self.request.set("form.select.BulkAction", "publish")
|
||||
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
|
||||
view = BulkActionsView(self.portal, self.request)
|
||||
|
||||
view()
|
||||
@ -81,16 +78,16 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
||||
# Count published comments
|
||||
published_comments = 0
|
||||
for r in self.conversation.getThreads():
|
||||
comment_obj = r['comment']
|
||||
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state')
|
||||
if workflow_status == 'published':
|
||||
comment_obj = r["comment"]
|
||||
workflow_status = self.wf.getInfoFor(comment_obj, "review_state")
|
||||
if workflow_status == "published":
|
||||
published_comments += 1
|
||||
# Make sure the comment has been published
|
||||
self.assertEqual(published_comments, 1)
|
||||
|
||||
def test_mark_as_spam(self):
|
||||
self.request.set('form.select.BulkAction', 'mark_as_spam')
|
||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||
self.request.set("form.select.BulkAction", "mark_as_spam")
|
||||
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
|
||||
|
||||
view = BulkActionsView(self.portal, self.request)
|
||||
|
||||
@ -99,9 +96,9 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
||||
# Count spam comments
|
||||
spam_comments = 0
|
||||
for r in self.conversation.getThreads():
|
||||
comment_obj = r['comment']
|
||||
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state')
|
||||
if workflow_status == 'spam':
|
||||
comment_obj = r["comment"]
|
||||
workflow_status = self.wf.getInfoFor(comment_obj, "review_state")
|
||||
if workflow_status == "spam":
|
||||
spam_comments += 1
|
||||
# Make sure the comment has been marked as spam
|
||||
self.assertEqual(spam_comments, 1)
|
||||
@ -110,9 +107,14 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
||||
# Initially we have three comments
|
||||
self.assertEqual(len(self.conversation.objectIds()), 3)
|
||||
# Delete two comments with bulk actions
|
||||
self.request.set('form.select.BulkAction', 'delete')
|
||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath()),
|
||||
'/'.join(self.comment3.getPhysicalPath())])
|
||||
self.request.set("form.select.BulkAction", "delete")
|
||||
self.request.set(
|
||||
"paths",
|
||||
[
|
||||
"/".join(self.comment1.getPhysicalPath()),
|
||||
"/".join(self.comment3.getPhysicalPath()),
|
||||
],
|
||||
)
|
||||
view = BulkActionsView(self.app, self.request)
|
||||
|
||||
view()
|
||||
|
@ -1,11 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.browser.moderation import BulkActionsView
|
||||
from plone.app.discussion.browser.moderation import DeleteComment
|
||||
from plone.app.discussion.browser.moderation import CommentTransition
|
||||
from plone.app.discussion.browser.moderation import View
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from ..browser.moderation import BulkActionsView
|
||||
from ..browser.moderation import CommentTransition
|
||||
from ..browser.moderation import DeleteComment
|
||||
from ..browser.moderation import View
|
||||
from ..interfaces import IConversation
|
||||
from ..interfaces import IDiscussionSettings
|
||||
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from plone.registry.interfaces import IRegistry
|
||||
@ -21,37 +20,37 @@ class ModerationViewTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.app = self.layer['app']
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal_discussion = getToolByName(self.portal,
|
||||
'portal_discussion',
|
||||
None)
|
||||
self.membership_tool = getToolByName(self.portal,
|
||||
'portal_membership')
|
||||
self.app = self.layer["app"]
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.portal_discussion = getToolByName(self.portal, "portal_discussion", None)
|
||||
self.membership_tool = getToolByName(self.portal, "portal_membership")
|
||||
self.memberdata = self.portal.portal_memberdata
|
||||
request = self.app.REQUEST
|
||||
context = getattr(self.portal, 'doc1')
|
||||
context = getattr(self.portal, "doc1")
|
||||
self.view = View(context, request)
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',), 'comment_review_workflow')
|
||||
("Discussion Item",), "comment_review_workflow"
|
||||
)
|
||||
self.wf_tool = self.portal.portal_workflow
|
||||
|
||||
def test_moderation_enabled(self):
|
||||
"""Make sure that moderation_enabled returns true if the comment
|
||||
workflow implements a 'pending' state.
|
||||
workflow implements a 'pending' state.
|
||||
"""
|
||||
# If workflow is not set, enabled must return False
|
||||
self.wf_tool.setChainForPortalTypes(('Discussion Item',), ())
|
||||
self.wf_tool.setChainForPortalTypes(("Discussion Item",), ())
|
||||
self.assertEqual(self.view.moderation_enabled(), False)
|
||||
# The comment_one_state_workflow does not have a 'pending' state
|
||||
self.wf_tool.setChainForPortalTypes(('Discussion Item',),
|
||||
('comment_one_state_workflow,'))
|
||||
self.wf_tool.setChainForPortalTypes(
|
||||
("Discussion Item",), ("comment_one_state_workflow,")
|
||||
)
|
||||
self.assertEqual(self.view.moderation_enabled(), False)
|
||||
# The comment_review_workflow does have a 'pending' state
|
||||
self.wf_tool.setChainForPortalTypes(('Discussion Item',),
|
||||
('comment_review_workflow,'))
|
||||
self.wf_tool.setChainForPortalTypes(
|
||||
("Discussion Item",), ("comment_review_workflow,")
|
||||
)
|
||||
self.assertEqual(self.view.moderation_enabled(), True)
|
||||
|
||||
|
||||
@ -60,59 +59,57 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.app = self.layer['app']
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.wf = getToolByName(self.portal,
|
||||
'portal_workflow',
|
||||
None)
|
||||
self.app = self.layer["app"]
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.wf = getToolByName(self.portal, "portal_workflow", None)
|
||||
self.context = self.portal
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
'comment_review_workflow',
|
||||
("Discussion Item",),
|
||||
"comment_review_workflow",
|
||||
)
|
||||
self.wf_tool = self.portal.portal_workflow
|
||||
# Add a conversation with three comments
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
comment1 = createObject('plone.Comment')
|
||||
comment1.title = 'Comment 1'
|
||||
comment1.text = 'Comment text'
|
||||
comment1.Creator = 'Jim'
|
||||
comment1 = createObject("plone.Comment")
|
||||
comment1.title = "Comment 1"
|
||||
comment1.text = "Comment text"
|
||||
comment1.Creator = "Jim"
|
||||
new_id_1 = conversation.addComment(comment1)
|
||||
self.comment1 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id_1),
|
||||
f"++conversation++default/{new_id_1}",
|
||||
)
|
||||
comment2 = createObject('plone.Comment')
|
||||
comment2.title = 'Comment 2'
|
||||
comment2.text = 'Comment text'
|
||||
comment2.Creator = 'Joe'
|
||||
comment2 = createObject("plone.Comment")
|
||||
comment2.title = "Comment 2"
|
||||
comment2.text = "Comment text"
|
||||
comment2.Creator = "Joe"
|
||||
new_id_2 = conversation.addComment(comment2)
|
||||
self.comment2 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id_2),
|
||||
f"++conversation++default/{new_id_2}",
|
||||
)
|
||||
comment3 = createObject('plone.Comment')
|
||||
comment3.title = 'Comment 3'
|
||||
comment3.text = 'Comment text'
|
||||
comment3.Creator = 'Emma'
|
||||
comment3 = createObject("plone.Comment")
|
||||
comment3.title = "Comment 3"
|
||||
comment3.text = "Comment text"
|
||||
comment3.Creator = "Emma"
|
||||
new_id_3 = conversation.addComment(comment3)
|
||||
self.comment3 = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(new_id_3),
|
||||
f"++conversation++default/{new_id_3}",
|
||||
)
|
||||
self.conversation = conversation
|
||||
|
||||
def test_default_bulkaction(self):
|
||||
# Make sure no error is raised when no bulk actions has been supplied
|
||||
self.request.set('form.select.BulkAction', '-1')
|
||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||
self.request.set("form.select.BulkAction", "-1")
|
||||
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
|
||||
|
||||
view = BulkActionsView(self.portal, self.request)
|
||||
|
||||
self.assertFalse(view())
|
||||
|
||||
def test_publish(self):
|
||||
self.request.set('form.select.BulkAction', 'publish')
|
||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath())])
|
||||
self.request.set("form.select.BulkAction", "publish")
|
||||
self.request.set("paths", ["/".join(self.comment1.getPhysicalPath())])
|
||||
view = BulkActionsView(self.portal, self.request)
|
||||
|
||||
view()
|
||||
@ -120,9 +117,9 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
||||
# Count published comments
|
||||
published_comments = 0
|
||||
for r in self.conversation.getThreads():
|
||||
comment_obj = r['comment']
|
||||
workflow_status = self.wf.getInfoFor(comment_obj, 'review_state')
|
||||
if workflow_status == 'published':
|
||||
comment_obj = r["comment"]
|
||||
workflow_status = self.wf.getInfoFor(comment_obj, "review_state")
|
||||
if workflow_status == "published":
|
||||
published_comments += 1
|
||||
# Make sure the comment has been published
|
||||
self.assertEqual(published_comments, 1)
|
||||
@ -131,9 +128,14 @@ class ModerationBulkActionsViewTest(unittest.TestCase):
|
||||
# Initially we have three comments
|
||||
self.assertEqual(len(self.conversation.objectIds()), 3)
|
||||
# Delete two comments with bulk actions
|
||||
self.request.set('form.select.BulkAction', 'delete')
|
||||
self.request.set('paths', ['/'.join(self.comment1.getPhysicalPath()),
|
||||
'/'.join(self.comment3.getPhysicalPath())])
|
||||
self.request.set("form.select.BulkAction", "delete")
|
||||
self.request.set(
|
||||
"paths",
|
||||
[
|
||||
"/".join(self.comment1.getPhysicalPath()),
|
||||
"/".join(self.comment3.getPhysicalPath()),
|
||||
],
|
||||
)
|
||||
view = BulkActionsView(self.app, self.request)
|
||||
|
||||
view()
|
||||
@ -151,41 +153,41 @@ class RedirectionTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Update settings.
|
||||
self.portal = self.layer['portal']
|
||||
self.request = self.layer['request']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
self.request = self.layer["request"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
# applyProfile(self.portal, 'plone.app.discussion:default')
|
||||
registry = queryUtility(IRegistry)
|
||||
settings = registry.forInterface(IDiscussionSettings)
|
||||
settings.globally_enabled = True
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
('comment_review_workflow',),
|
||||
("Discussion Item",),
|
||||
("comment_review_workflow",),
|
||||
)
|
||||
# Create page plus comment.
|
||||
self.portal.invokeFactory(
|
||||
id='page',
|
||||
title='Page 1',
|
||||
type_name='Document',
|
||||
id="page",
|
||||
title="Page 1",
|
||||
type_name="Document",
|
||||
)
|
||||
self.page = self.portal.page
|
||||
self.conversation = IConversation(self.page)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
self.comment_id = self.conversation.addComment(comment)
|
||||
self.comment = list(self.conversation.getComments())[0]
|
||||
|
||||
def test_regression(self):
|
||||
page_url = self.page.absolute_url()
|
||||
self.request['HTTP_REFERER'] = page_url
|
||||
self.request["HTTP_REFERER"] = page_url
|
||||
for Klass in (DeleteComment, CommentTransition):
|
||||
view = Klass(self.comment, self.request)
|
||||
view.__parent__ = self.comment
|
||||
self.assertEqual(page_url, view())
|
||||
|
||||
def test_valid_next_url(self):
|
||||
self.request['HTTP_REFERER'] = 'http://attacker.com'
|
||||
self.request["HTTP_REFERER"] = "http://attacker.com"
|
||||
for Klass in (DeleteComment, CommentTransition):
|
||||
view = Klass(self.comment, self.request)
|
||||
view.__parent__ = self.comment
|
||||
self.assertNotEqual('http://attacker.com', view())
|
||||
self.assertNotEqual("http://attacker.com", view())
|
||||
|
@ -1,13 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ..interfaces import IConversation
|
||||
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
from Acquisition import aq_base
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from persistent.list import PersistentList
|
||||
from plone.app.testing import setRoles
|
||||
from plone.app.testing import TEST_USER_ID
|
||||
from plone.base.interfaces import IMailSchema
|
||||
from plone.registry.interfaces import IRegistry
|
||||
from Products.CMFPlone.interfaces import IMailSchema
|
||||
from Products.CMFPlone.tests.utils import MockMailHost
|
||||
from Products.MailHost.interfaces import IMailHost
|
||||
from Products.MailHost.MailHost import _mungeHeaders
|
||||
from Products.MailHost.MailHost import MailBase
|
||||
from zope.component import createObject
|
||||
from zope.component import getSiteManager
|
||||
from zope.component import getUtility
|
||||
@ -16,50 +17,85 @@ from zope.component import queryUtility
|
||||
import unittest
|
||||
|
||||
|
||||
class MockMailHost(MailBase):
|
||||
"""A MailHost that collects messages instead of sending them."""
|
||||
|
||||
def __init__(self, id):
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
self.messages = PersistentList()
|
||||
|
||||
def _send(self, mfrom, mto, messageText, immediate=False):
|
||||
"""Send the message"""
|
||||
self.messages.append(messageText)
|
||||
|
||||
def send(
|
||||
self,
|
||||
messageText,
|
||||
mto=None,
|
||||
mfrom=None,
|
||||
subject=None,
|
||||
encode=None,
|
||||
immediate=False,
|
||||
charset=None,
|
||||
msg_type=None,
|
||||
):
|
||||
"""send *messageText* modified by the other parameters.
|
||||
|
||||
*messageText* can either be an ``email.message.Message``
|
||||
or a string.
|
||||
Note that Products.MailHost 4.10 had changes here.
|
||||
"""
|
||||
msg, mto, mfrom = _mungeHeaders(
|
||||
messageText, mto, mfrom, subject, charset, msg_type, encode
|
||||
)
|
||||
self.messages.append(msg)
|
||||
|
||||
|
||||
class TestUserNotificationUnit(unittest.TestCase):
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
# Set up a mock mailhost
|
||||
self.portal._original_MailHost = self.portal.MailHost
|
||||
self.portal.MailHost = mailhost = MockMailHost('MailHost')
|
||||
self.portal.MailHost = mailhost = MockMailHost("MailHost")
|
||||
sm = getSiteManager(context=self.portal)
|
||||
sm.unregisterUtility(provided=IMailHost)
|
||||
sm.registerUtility(mailhost, provided=IMailHost)
|
||||
# We need to fake a valid mail setup
|
||||
registry = getUtility(IRegistry)
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
|
||||
mail_settings.email_from_address = 'portal@plone.test'
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
|
||||
mail_settings.email_from_address = "portal@plone.test"
|
||||
self.mailhost = self.portal.MailHost
|
||||
# Enable user notification setting
|
||||
registry = queryUtility(IRegistry)
|
||||
registry['plone.app.discussion.interfaces.IDiscussionSettings' +
|
||||
'.user_notification_enabled'] = True
|
||||
# Archetypes content types store data as utf-8 encoded strings
|
||||
# The missing u in front of a string is therefor not missing
|
||||
self.portal.doc1.title = 'Kölle Alaaf' # What is 'Fasching'?
|
||||
registry[
|
||||
"plone.app.discussion.interfaces.IDiscussionSettings"
|
||||
+ ".user_notification_enabled"
|
||||
] = True
|
||||
self.portal.doc1.title = "Kölle Alaaf" # What is 'Fasching'?
|
||||
self.conversation = IConversation(self.portal.doc1)
|
||||
|
||||
def beforeTearDown(self):
|
||||
self.portal.MailHost = self.portal._original_MailHost
|
||||
sm = getSiteManager(context=self.portal)
|
||||
sm.unregisterUtility(provided=IMailHost)
|
||||
sm.registerUtility(aq_base(self.portal._original_MailHost),
|
||||
provided=IMailHost)
|
||||
sm.registerUtility(aq_base(self.portal._original_MailHost), provided=IMailHost)
|
||||
|
||||
def test_notify_user(self):
|
||||
# Add a comment with user notification enabled. Add another comment
|
||||
# and make sure an email is send to the user of the first comment.
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment.user_notification = True
|
||||
comment.author_email = 'john@plone.test'
|
||||
comment.author_email = "john@plone.test"
|
||||
self.conversation.addComment(comment)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
comment_id = self.conversation.addComment(comment)
|
||||
|
||||
@ -67,52 +103,46 @@ class TestUserNotificationUnit(unittest.TestCase):
|
||||
self.assertTrue(self.mailhost.messages[0])
|
||||
msg = self.mailhost.messages[0]
|
||||
msg = msg.decode("utf-8")
|
||||
self.assertIn('To: john@plone.test', msg)
|
||||
self.assertIn('From: portal@plone.test', msg)
|
||||
self.assertIn("To: john@plone.test", msg)
|
||||
self.assertIn("From: portal@plone.test", msg)
|
||||
# We expect the headers to be properly header encoded (7-bit):
|
||||
self.assertIn(
|
||||
'Subject: =?utf-8?q?A_comment_has_been_posted=2E?=',
|
||||
msg)
|
||||
self.assertIn("Subject: =?utf-8?q?A_comment_has_been_posted=2E?=", msg)
|
||||
# The output should be encoded in a reasonable manner
|
||||
# (in this case quoted-printable).
|
||||
# Depending on which Python version and which Products.MailHost version,
|
||||
# you may get lines separated by '\n' or '\r\n' in here.
|
||||
msg = msg.replace('\r\n', '\n')
|
||||
self.assertIn(
|
||||
'A comment on "K=C3=B6lle Alaaf" has been posted here:',
|
||||
msg)
|
||||
self.assertIn(
|
||||
'http://nohost/plone/d=\noc1/view#{0}'.format(comment_id),
|
||||
msg)
|
||||
self.assertIn('Comment text', msg)
|
||||
self.assertNotIn('Approve comment', msg)
|
||||
self.assertNotIn('Delete comment', msg)
|
||||
msg = msg.replace("\r\n", "\n")
|
||||
self.assertIn('A comment on "K=C3=B6lle Alaaf" has been posted here:', msg)
|
||||
self.assertIn(f"http://nohost/plone/d=\noc1/view#{comment_id}", msg)
|
||||
self.assertIn("Comment text", msg)
|
||||
self.assertNotIn("Approve comment", msg)
|
||||
self.assertNotIn("Delete comment", msg)
|
||||
|
||||
def test_do_not_notify_user_when_notification_is_disabled(self):
|
||||
registry = queryUtility(IRegistry)
|
||||
registry[
|
||||
'plone.app.discussion.interfaces.IDiscussionSettings.' +
|
||||
'user_notification_enabled'
|
||||
"plone.app.discussion.interfaces.IDiscussionSettings."
|
||||
+ "user_notification_enabled"
|
||||
] = False
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment.user_notification = True
|
||||
comment.author_email = 'john@plone.test'
|
||||
comment.author_email = "john@plone.test"
|
||||
self.conversation.addComment(comment)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
self.conversation.addComment(comment)
|
||||
|
||||
self.assertEqual(len(self.mailhost.messages), 0)
|
||||
|
||||
def test_do_not_notify_user_when_email_address_is_given(self):
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment.user_notification = True
|
||||
self.conversation.addComment(comment)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
self.conversation.addComment(comment)
|
||||
|
||||
@ -122,15 +152,15 @@ class TestUserNotificationUnit(unittest.TestCase):
|
||||
# Set sender mail address to none and make sure no email is send to
|
||||
# the moderator.
|
||||
registry = getUtility(IRegistry)
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
|
||||
mail_settings.email_from_address = None
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment.user_notification = True
|
||||
comment.author_email = 'john@plone.test'
|
||||
comment.author_email = "john@plone.test"
|
||||
self.conversation.addComment(comment)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
self.conversation.addComment(comment)
|
||||
self.assertEqual(len(self.mailhost.messages), 0)
|
||||
@ -139,15 +169,15 @@ class TestUserNotificationUnit(unittest.TestCase):
|
||||
# When a user has added two comments in a conversation and has
|
||||
# both times requested email notification, do not send him two
|
||||
# emails when another comment has been added.
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment.user_notification = True
|
||||
comment.author_email = 'john@plone.test'
|
||||
comment.author_email = "john@plone.test"
|
||||
self.conversation.addComment(comment)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment.user_notification = True
|
||||
comment.author_email = 'john@plone.test'
|
||||
comment.author_email = "john@plone.test"
|
||||
|
||||
self.conversation.addComment(comment)
|
||||
|
||||
@ -163,48 +193,45 @@ class TestModeratorNotificationUnit(unittest.TestCase):
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
# Set up a mock mailhost
|
||||
self.portal._original_MailHost = self.portal.MailHost
|
||||
self.portal.MailHost = mailhost = MockMailHost('MailHost')
|
||||
self.portal.MailHost = mailhost = MockMailHost("MailHost")
|
||||
sm = getSiteManager(context=self.portal)
|
||||
sm.unregisterUtility(provided=IMailHost)
|
||||
sm.registerUtility(mailhost, provided=IMailHost)
|
||||
# We need to fake a valid mail setup
|
||||
registry = getUtility(IRegistry)
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
|
||||
mail_settings.email_from_address = 'portal@plone.test'
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
|
||||
mail_settings.email_from_address = "portal@plone.test"
|
||||
self.mailhost = self.portal.MailHost
|
||||
# Enable comment moderation
|
||||
self.portal.portal_types['Document'].allow_discussion = True
|
||||
self.portal.portal_types["Document"].allow_discussion = True
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
('comment_review_workflow',),
|
||||
("Discussion Item",),
|
||||
("comment_review_workflow",),
|
||||
)
|
||||
# Enable moderator notification setting
|
||||
registry = queryUtility(IRegistry)
|
||||
registry[
|
||||
'plone.app.discussion.interfaces.IDiscussionSettings.' +
|
||||
'moderator_notification_enabled'
|
||||
"plone.app.discussion.interfaces.IDiscussionSettings."
|
||||
+ "moderator_notification_enabled"
|
||||
] = True
|
||||
# Archetypes content types store data as utf-8 encoded strings
|
||||
# The missing u in front of a string is therefor not missing
|
||||
self.portal.doc1.title = 'Kölle Alaaf' # What is 'Fasching'?
|
||||
self.portal.doc1.title = "Kölle Alaaf" # What is 'Fasching'?
|
||||
self.conversation = IConversation(self.portal.doc1)
|
||||
|
||||
def beforeTearDown(self):
|
||||
self.portal.MailHost = self.portal._original_MailHost
|
||||
sm = getSiteManager(context=self.portal)
|
||||
sm.unregisterUtility(provided=IMailHost)
|
||||
sm.registerUtility(aq_base(self.portal._original_MailHost),
|
||||
provided=IMailHost)
|
||||
sm.registerUtility(aq_base(self.portal._original_MailHost), provided=IMailHost)
|
||||
|
||||
def test_notify_moderator(self):
|
||||
"""Add a comment and make sure an email is send to the moderator."""
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment.author_email = 'john@plone.test'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment.author_email = "john@plone.test"
|
||||
|
||||
comment_id = self.conversation.addComment(comment)
|
||||
|
||||
@ -212,54 +239,41 @@ class TestModeratorNotificationUnit(unittest.TestCase):
|
||||
self.assertTrue(self.mailhost.messages[0])
|
||||
msg = self.mailhost.messages[0]
|
||||
msg = msg.decode("utf-8")
|
||||
self.assertTrue('To: portal@plone.test' in msg)
|
||||
self.assertTrue('From: portal@plone.test' in msg)
|
||||
self.assertTrue("To: portal@plone.test" in msg)
|
||||
self.assertTrue("From: portal@plone.test" in msg)
|
||||
# We expect the headers to be properly header encoded (7-bit):
|
||||
self.assertTrue(
|
||||
'Subject: =?utf-8?q?A_comment_has_been_posted=2E?='
|
||||
in msg)
|
||||
self.assertTrue("Subject: =?utf-8?q?A_comment_has_been_posted=2E?=" in msg)
|
||||
# The output should be encoded in a reasonable manner
|
||||
# (in this case quoted-printable):
|
||||
self.assertTrue(
|
||||
'A comment on "K=C3=B6lle Alaaf" has been posted'
|
||||
in msg
|
||||
)
|
||||
self.assertIn(
|
||||
'http://nohost/plone/doc1/view#{0}'.format(comment_id),
|
||||
msg
|
||||
)
|
||||
self.assertIn(
|
||||
comment.author_email,
|
||||
msg
|
||||
)
|
||||
self.assertIn(
|
||||
comment.text,
|
||||
msg
|
||||
)
|
||||
self.assertTrue('A comment on "K=C3=B6lle Alaaf" has been posted' in msg)
|
||||
self.assertIn(f"http://nohost/plone/doc1/view#{comment_id}", msg)
|
||||
self.assertIn(comment.author_email, msg)
|
||||
self.assertIn(comment.text, msg)
|
||||
|
||||
def test_notify_moderator_specific_address(self):
|
||||
# A moderator email address can be specified in the control panel.
|
||||
registry = queryUtility(IRegistry)
|
||||
registry['plone.app.discussion.interfaces.IDiscussionSettings' +
|
||||
'.moderator_email'] = 'test@example.com'
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
registry[
|
||||
"plone.app.discussion.interfaces.IDiscussionSettings" + ".moderator_email"
|
||||
] = "test@example.com"
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
self.conversation.addComment(comment)
|
||||
|
||||
self.assertEqual(len(self.mailhost.messages), 1)
|
||||
msg = self.mailhost.messages[0]
|
||||
msg = msg.decode("utf-8")
|
||||
self.assertTrue('To: test@example.com' in msg)
|
||||
self.assertTrue("To: test@example.com" in msg)
|
||||
|
||||
def test_do_not_notify_moderator_when_no_sender_is_available(self):
|
||||
# Set sender mail address to nonw and make sure no email is send to the
|
||||
# moderator.
|
||||
registry = getUtility(IRegistry)
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix='plone')
|
||||
mail_settings = registry.forInterface(IMailSchema, prefix="plone")
|
||||
mail_settings.email_from_address = None
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
self.conversation.addComment(comment)
|
||||
|
||||
@ -269,10 +283,12 @@ class TestModeratorNotificationUnit(unittest.TestCase):
|
||||
# Disable moderator notification setting and make sure no email is send
|
||||
# to the moderator.
|
||||
registry = queryUtility(IRegistry)
|
||||
registry['plone.app.discussion.interfaces.IDiscussionSettings.' +
|
||||
'moderator_notification_enabled'] = False
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
registry[
|
||||
"plone.app.discussion.interfaces.IDiscussionSettings."
|
||||
+ "moderator_notification_enabled"
|
||||
] = False
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
|
||||
self.conversation.addComment(comment)
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_ROBOT_TESTING
|
||||
from ..testing import PLONE_APP_DISCUSSION_ROBOT_TESTING
|
||||
from plone.app.testing import ROBOT_TEST_LEVEL
|
||||
from plone.testing import layered
|
||||
|
||||
@ -11,19 +10,21 @@ import unittest
|
||||
def test_suite():
|
||||
suite = unittest.TestSuite()
|
||||
current_dir = os.path.abspath(os.path.dirname(__file__))
|
||||
robot_dir = os.path.join(current_dir, 'robot')
|
||||
robot_dir = os.path.join(current_dir, "robot")
|
||||
robot_tests = [
|
||||
os.path.join('robot', doc) for doc in
|
||||
os.listdir(robot_dir) if doc.endswith('.robot') and
|
||||
doc.startswith('test_')
|
||||
os.path.join("robot", doc)
|
||||
for doc in os.listdir(robot_dir)
|
||||
if doc.endswith(".robot") and doc.startswith("test_")
|
||||
]
|
||||
for robot_test in robot_tests:
|
||||
robottestsuite = robotsuite.RobotTestSuite(robot_test)
|
||||
robottestsuite.level = ROBOT_TEST_LEVEL
|
||||
suite.addTests([
|
||||
layered(
|
||||
robottestsuite,
|
||||
layer=PLONE_APP_DISCUSSION_ROBOT_TESTING,
|
||||
),
|
||||
])
|
||||
suite.addTests(
|
||||
[
|
||||
layered(
|
||||
robottestsuite,
|
||||
layer=PLONE_APP_DISCUSSION_ROBOT_TESTING,
|
||||
),
|
||||
]
|
||||
)
|
||||
return suite
|
||||
|
@ -1,10 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Test plone.app.discussion workflow and permissions.
|
||||
"""
|
||||
from ..interfaces import IConversation
|
||||
from ..interfaces import IDiscussionLayer
|
||||
from ..testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
from AccessControl import Unauthorized
|
||||
from plone.app.discussion.interfaces import IConversation
|
||||
from plone.app.discussion.interfaces import IDiscussionLayer
|
||||
from plone.app.discussion.testing import PLONE_APP_DISCUSSION_INTEGRATION_TESTING # noqa
|
||||
from plone.app.testing import login
|
||||
from plone.app.testing import logout
|
||||
from plone.app.testing import setRoles
|
||||
@ -19,48 +18,51 @@ import unittest
|
||||
|
||||
|
||||
class WorkflowSetupTest(unittest.TestCase):
|
||||
"""Make sure the workflows are set up properly.
|
||||
"""
|
||||
"""Make sure the workflows are set up properly."""
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal.invokeFactory('Folder', 'test-folder')
|
||||
self.folder = self.portal['test-folder']
|
||||
self.portal.portal_types['Document'].allow_discussion = True
|
||||
self.folder.invokeFactory('Document', 'doc1')
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.portal.invokeFactory("Folder", "test-folder")
|
||||
self.folder = self.portal["test-folder"]
|
||||
self.portal.portal_types["Document"].allow_discussion = True
|
||||
self.folder.invokeFactory("Document", "doc1")
|
||||
self.doc = self.folder.doc1
|
||||
|
||||
def test_workflows_installed(self):
|
||||
"""Make sure both comment workflows have been installed properly.
|
||||
"""
|
||||
self.assertTrue('comment_one_state_workflow' in
|
||||
self.portal.portal_workflow.objectIds())
|
||||
self.assertTrue('comment_review_workflow' in
|
||||
self.portal.portal_workflow.objectIds())
|
||||
"""Make sure both comment workflows have been installed properly."""
|
||||
self.assertTrue(
|
||||
"comment_one_state_workflow" in self.portal.portal_workflow.objectIds()
|
||||
)
|
||||
self.assertTrue(
|
||||
"comment_review_workflow" in self.portal.portal_workflow.objectIds()
|
||||
)
|
||||
|
||||
def test_default_workflow(self):
|
||||
"""Make sure one_state_workflow is the default workflow.
|
||||
"""
|
||||
"""Make sure one_state_workflow is the default workflow."""
|
||||
self.assertEqual(
|
||||
('comment_one_state_workflow',),
|
||||
("comment_one_state_workflow",),
|
||||
self.portal.portal_workflow.getChainForPortalType(
|
||||
'Discussion Item',
|
||||
"Discussion Item",
|
||||
),
|
||||
)
|
||||
|
||||
def test_review_comments_permission(self):
|
||||
# 'Review comments' in self.portal.permissionsOfRole('Admin')
|
||||
|
||||
setRoles(self.portal, TEST_USER_ID, ['Reviewer'])
|
||||
self.assertTrue(self.portal.portal_membership.checkPermission(
|
||||
'Review comments', self.folder), self.folder)
|
||||
setRoles(self.portal, TEST_USER_ID, ['Member'])
|
||||
setRoles(self.portal, TEST_USER_ID, ["Reviewer"])
|
||||
self.assertTrue(
|
||||
self.portal.portal_membership.checkPermission(
|
||||
"Review comments", self.folder
|
||||
),
|
||||
self.folder,
|
||||
)
|
||||
setRoles(self.portal, TEST_USER_ID, ["Member"])
|
||||
self.assertFalse(
|
||||
self.portal.portal_membership.checkPermission(
|
||||
'Review comments',
|
||||
"Review comments",
|
||||
self.folder,
|
||||
),
|
||||
self.folder,
|
||||
@ -71,31 +73,30 @@ class WorkflowSetupTest(unittest.TestCase):
|
||||
|
||||
|
||||
class PermissionsSetupTest(unittest.TestCase):
|
||||
"""Make sure the permissions are set up properly.
|
||||
"""
|
||||
"""Make sure the permissions are set up properly."""
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
mtool = self.portal.portal_membership
|
||||
self.checkPermission = mtool.checkPermission
|
||||
|
||||
def test_reply_to_item_permission_assigned(self):
|
||||
"""Make sure the 'Reply to item' permission is properly assigned.
|
||||
By default this permission is assigned to 'Member' and 'Manager'.
|
||||
plone.app.discussion assigns this permission to 'Authenticated' as
|
||||
well to emulate the behavior of the old commenting system.
|
||||
By default this permission is assigned to 'Member' and 'Manager'.
|
||||
plone.app.discussion assigns this permission to 'Authenticated' as
|
||||
well to emulate the behavior of the old commenting system.
|
||||
"""
|
||||
ReplyToItemPerm = 'Reply to item'
|
||||
ReplyToItemPerm = "Reply to item"
|
||||
# should be allowed as Member
|
||||
self.assertTrue(self.checkPermission(ReplyToItemPerm, self.portal))
|
||||
# should be allowed as Authenticated
|
||||
setRoles(self.portal, TEST_USER_ID, ['Authenticated'])
|
||||
setRoles(self.portal, TEST_USER_ID, ["Authenticated"])
|
||||
self.assertTrue(self.checkPermission(ReplyToItemPerm, self.portal))
|
||||
# should be allowed as Manager
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.assertTrue(self.checkPermission(ReplyToItemPerm, self.portal))
|
||||
# should not be allowed as anonymous
|
||||
logout()
|
||||
@ -103,70 +104,66 @@ class PermissionsSetupTest(unittest.TestCase):
|
||||
|
||||
|
||||
class CommentOneStateWorkflowTest(unittest.TestCase):
|
||||
"""Test the comment_one_state_workflow that ships with plone.app.discussion.
|
||||
"""
|
||||
"""Test the comment_one_state_workflow that ships with plone.app.discussion."""
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal.invokeFactory('Folder', 'test-folder')
|
||||
self.folder = self.portal['test-folder']
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.portal.invokeFactory("Folder", "test-folder")
|
||||
self.folder = self.portal["test-folder"]
|
||||
self.catalog = self.portal.portal_catalog
|
||||
self.workflow = self.portal.portal_workflow
|
||||
self.folder.invokeFactory('Document', 'doc1')
|
||||
self.folder.invokeFactory("Document", "doc1")
|
||||
self.doc = self.folder.doc1
|
||||
|
||||
# Add a comment
|
||||
conversation = IConversation(self.folder.doc1)
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
cid = conversation.addComment(comment)
|
||||
|
||||
self.comment = self.folder.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(cid),
|
||||
f"++conversation++default/{cid}",
|
||||
)
|
||||
|
||||
self.portal.acl_users._doAddUser('member', 'secret', ['Member'], [])
|
||||
self.portal.acl_users._doAddUser(
|
||||
'reviewer', 'secret', ['Reviewer'], [])
|
||||
self.portal.acl_users._doAddUser('manager', 'secret', ['Manager'], [])
|
||||
self.portal.acl_users._doAddUser('editor', ' secret', ['Editor'], [])
|
||||
self.portal.acl_users._doAddUser('reader', 'secret', ['Reader'], [])
|
||||
self.portal.acl_users._doAddUser("member", "secret", ["Member"], [])
|
||||
self.portal.acl_users._doAddUser("reviewer", "secret", ["Reviewer"], [])
|
||||
self.portal.acl_users._doAddUser("manager", "secret", ["Manager"], [])
|
||||
self.portal.acl_users._doAddUser("editor", " secret", ["Editor"], [])
|
||||
self.portal.acl_users._doAddUser("reader", "secret", ["Reader"], [])
|
||||
|
||||
def test_initial_workflow_state(self):
|
||||
"""Make sure the initial workflow state of a comment is 'private'.
|
||||
"""
|
||||
"""Make sure the initial workflow state of a comment is 'private'."""
|
||||
self.assertEqual(
|
||||
self.workflow.getInfoFor(self.doc, 'review_state'),
|
||||
'private',
|
||||
self.workflow.getInfoFor(self.doc, "review_state"),
|
||||
"private",
|
||||
)
|
||||
|
||||
def test_view_comments(self):
|
||||
"""Make sure published comments can be viewed by everyone.
|
||||
"""
|
||||
"""Make sure published comments can be viewed by everyone."""
|
||||
# Owner is allowed
|
||||
# self.login(default_user)
|
||||
# self.assertTrue(checkPerm(View, self.doc))
|
||||
# Member is allowed
|
||||
login(self.portal, TEST_USER_NAME)
|
||||
workflow = self.portal.portal_workflow
|
||||
workflow.doActionFor(self.doc, 'publish')
|
||||
workflow.doActionFor(self.doc, "publish")
|
||||
|
||||
login(self.portal, 'member')
|
||||
login(self.portal, "member")
|
||||
self.assertTrue(checkPerm(View, self.comment))
|
||||
# Reviewer is allowed
|
||||
login(self.portal, 'reviewer')
|
||||
login(self.portal, "reviewer")
|
||||
self.assertTrue(checkPerm(View, self.comment))
|
||||
# Anonymous is allowed
|
||||
logout()
|
||||
self.assertTrue(checkPerm(View, self.comment))
|
||||
# Editor is allowed
|
||||
login(self.portal, 'editor')
|
||||
login(self.portal, "editor")
|
||||
self.assertTrue(checkPerm(View, self.comment))
|
||||
# Reader is allowed
|
||||
login(self.portal, 'reader')
|
||||
login(self.portal, "reader")
|
||||
self.assertTrue(checkPerm(View, self.comment))
|
||||
|
||||
def test_comment_on_private_content_not_visible_to_world(self):
|
||||
@ -175,8 +172,9 @@ class CommentOneStateWorkflowTest(unittest.TestCase):
|
||||
|
||||
def test_migration(self):
|
||||
from plone.app.discussion.upgrades import upgrade_comment_workflows
|
||||
|
||||
# Fake permission according to earlier one_comment_workflow.
|
||||
self.comment._View_Permission = ('Anonymous',)
|
||||
self.comment._View_Permission = ("Anonymous",)
|
||||
# Anonymous can see the comment.
|
||||
logout()
|
||||
self.assertTrue(checkPerm(View, self.comment))
|
||||
@ -185,8 +183,8 @@ class CommentOneStateWorkflowTest(unittest.TestCase):
|
||||
upgrade_comment_workflows(self.portal.portal_setup)
|
||||
# The workflow chain is still what we want.
|
||||
self.assertEqual(
|
||||
self.portal.portal_workflow.getChainFor('Discussion Item'),
|
||||
('comment_one_state_workflow',),
|
||||
self.portal.portal_workflow.getChainFor("Discussion Item"),
|
||||
("comment_one_state_workflow",),
|
||||
)
|
||||
# A Manager can still see the comment.
|
||||
self.assertTrue(checkPerm(View, self.comment))
|
||||
@ -196,112 +194,112 @@ class CommentOneStateWorkflowTest(unittest.TestCase):
|
||||
|
||||
|
||||
class CommentReviewWorkflowTest(unittest.TestCase):
|
||||
"""Test the comment_review_workflow that ships with plone.app.discussion.
|
||||
"""
|
||||
"""Test the comment_review_workflow that ships with plone.app.discussion."""
|
||||
|
||||
layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING
|
||||
|
||||
def setUp(self):
|
||||
self.portal = self.layer['portal']
|
||||
setRoles(self.portal, TEST_USER_ID, ['Manager'])
|
||||
self.portal.invokeFactory('Folder', 'test-folder')
|
||||
self.folder = self.portal['test-folder']
|
||||
self.portal = self.layer["portal"]
|
||||
setRoles(self.portal, TEST_USER_ID, ["Manager"])
|
||||
self.portal.invokeFactory("Folder", "test-folder")
|
||||
self.folder = self.portal["test-folder"]
|
||||
|
||||
# Allow discussion on the Document content type
|
||||
self.portal.portal_types['Document'].allow_discussion = True
|
||||
self.portal.portal_types["Document"].allow_discussion = True
|
||||
# Set workflow for Discussion item to review workflow
|
||||
self.portal.portal_workflow.setChainForPortalTypes(
|
||||
('Discussion Item',),
|
||||
('comment_review_workflow',),
|
||||
("Discussion Item",),
|
||||
("comment_review_workflow",),
|
||||
)
|
||||
|
||||
# Create a conversation for this Document
|
||||
conversation = IConversation(self.portal.doc1)
|
||||
|
||||
# Add a comment.
|
||||
comment = createObject('plone.Comment')
|
||||
comment.text = 'Comment text'
|
||||
comment = createObject("plone.Comment")
|
||||
comment.text = "Comment text"
|
||||
comment_id = conversation.addComment(comment)
|
||||
comment = self.portal.doc1.restrictedTraverse(
|
||||
'++conversation++default/{0}'.format(comment_id),
|
||||
f"++conversation++default/{comment_id}",
|
||||
)
|
||||
|
||||
self.conversation = conversation
|
||||
self.comment_id = comment_id
|
||||
self.comment = comment
|
||||
|
||||
setRoles(self.portal, TEST_USER_ID, ['Reviewer'])
|
||||
setRoles(self.portal, TEST_USER_ID, ["Reviewer"])
|
||||
alsoProvides(self.portal.REQUEST, IDiscussionLayer)
|
||||
|
||||
def test_delete(self):
|
||||
self.portal.REQUEST.form['comment_id'] = self.comment_id
|
||||
view = self.comment.restrictedTraverse('@@moderate-delete-comment')
|
||||
self.portal.REQUEST.form["comment_id"] = self.comment_id
|
||||
view = self.comment.restrictedTraverse("@@moderate-delete-comment")
|
||||
view()
|
||||
self.assertFalse(self.comment_id in self.conversation.objectIds())
|
||||
|
||||
def test_delete_as_anonymous(self):
|
||||
# Make sure that anonymous users can not delete comments
|
||||
logout()
|
||||
self.portal.REQUEST.form['comment_id'] = self.comment_id
|
||||
self.portal.REQUEST.form["comment_id"] = self.comment_id
|
||||
self.assertRaises(
|
||||
Unauthorized,
|
||||
self.comment.restrictedTraverse,
|
||||
'@@moderate-delete-comment',
|
||||
"@@moderate-delete-comment",
|
||||
)
|
||||
self.assertTrue(self.comment_id in self.conversation.objectIds())
|
||||
|
||||
def test_delete_as_user(self):
|
||||
# Make sure that members can not delete comments
|
||||
logout()
|
||||
setRoles(self.portal, TEST_USER_ID, ['Member'])
|
||||
self.portal.REQUEST.form['comment_id'] = self.comment_id
|
||||
setRoles(self.portal, TEST_USER_ID, ["Member"])
|
||||
self.portal.REQUEST.form["comment_id"] = self.comment_id
|
||||
self.assertRaises(
|
||||
Unauthorized,
|
||||
self.comment.restrictedTraverse,
|
||||
'@@moderate-delete-comment',
|
||||
"@@moderate-delete-comment",
|
||||
)
|
||||
self.assertTrue(self.comment_id in self.conversation.objectIds())
|
||||
|
||||
def test_publish(self):
|
||||
self.portal.REQUEST.form['comment_id'] = self.comment_id
|
||||
self.portal.REQUEST.form['workflow_action'] = 'publish'
|
||||
self.portal.REQUEST.form["comment_id"] = self.comment_id
|
||||
self.portal.REQUEST.form["workflow_action"] = "publish"
|
||||
self.assertEqual(
|
||||
'pending',
|
||||
"pending",
|
||||
self.portal.portal_workflow.getInfoFor(
|
||||
self.comment,
|
||||
'review_state',
|
||||
"review_state",
|
||||
),
|
||||
)
|
||||
view = self.comment.restrictedTraverse('@@transmit-comment')
|
||||
view = self.comment.restrictedTraverse("@@transmit-comment")
|
||||
view()
|
||||
self.assertEqual(
|
||||
'published',
|
||||
"published",
|
||||
self.portal.portal_workflow.getInfoFor(
|
||||
self.comment,
|
||||
'review_state',
|
||||
"review_state",
|
||||
),
|
||||
)
|
||||
|
||||
def test_publish_as_anonymous(self):
|
||||
logout()
|
||||
self.portal.REQUEST.form['comment_id'] = self.comment_id
|
||||
self.portal.REQUEST.form['workflow_action'] = 'publish'
|
||||
self.portal.REQUEST.form["comment_id"] = self.comment_id
|
||||
self.portal.REQUEST.form["workflow_action"] = "publish"
|
||||
self.assertEqual(
|
||||
'pending', self.portal.portal_workflow.getInfoFor(
|
||||
"pending",
|
||||
self.portal.portal_workflow.getInfoFor(
|
||||
self.comment,
|
||||
'review_state',
|
||||
"review_state",
|
||||
),
|
||||
)
|
||||
self.assertRaises(
|
||||
Unauthorized,
|
||||
self.comment.restrictedTraverse,
|
||||
'@@transmit-comment',
|
||||
"@@transmit-comment",
|
||||
)
|
||||
self.assertEqual(
|
||||
'pending',
|
||||
"pending",
|
||||
self.portal.portal_workflow.getInfoFor(
|
||||
self.comment,
|
||||
'review_state',
|
||||
"review_state",
|
||||
),
|
||||
)
|
||||
|
||||
@ -312,15 +310,16 @@ class CommentReviewWorkflowTest(unittest.TestCase):
|
||||
# publish comment and check again
|
||||
login(self.portal, TEST_USER_NAME)
|
||||
workflow = self.portal.portal_workflow
|
||||
workflow.doActionFor(self.comment, 'publish')
|
||||
workflow.doActionFor(self.comment, "publish")
|
||||
|
||||
logout()
|
||||
self.assertFalse(checkPerm(View, self.comment))
|
||||
|
||||
def test_migration(self):
|
||||
from plone.app.discussion.upgrades import upgrade_comment_workflows
|
||||
|
||||
# Fake permission according to earlier comment_review_workflow.
|
||||
self.comment._View_Permission = ('Anonymous',)
|
||||
self.comment._View_Permission = ("Anonymous",)
|
||||
# Anonymous can see the comment.
|
||||
logout()
|
||||
self.assertTrue(checkPerm(View, self.comment))
|
||||
@ -329,8 +328,9 @@ class CommentReviewWorkflowTest(unittest.TestCase):
|
||||
upgrade_comment_workflows(self.portal.portal_setup)
|
||||
# The workflow chain is still what we want.
|
||||
self.assertEqual(
|
||||
self.portal.portal_workflow.getChainFor('Discussion Item'),
|
||||
('comment_review_workflow',))
|
||||
self.portal.portal_workflow.getChainFor("Discussion Item"),
|
||||
("comment_review_workflow",),
|
||||
)
|
||||
# A Manager can still see the comment.
|
||||
self.assertTrue(checkPerm(View, self.comment))
|
||||
# Anonymous cannot see the comment.
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The portal_discussion tool, usually accessed via
|
||||
queryUtility(ICommentingTool). The default implementation delegates to the
|
||||
standard portal_catalog for indexing comments.
|
||||
@ -17,62 +16,60 @@ from zope.component import queryUtility
|
||||
@interface.implementer(ICommentingTool)
|
||||
class CommentingTool(UniqueObject, SimpleItem):
|
||||
|
||||
meta_type = 'plone.app.discussion tool'
|
||||
id = 'portal_discussion'
|
||||
meta_type = "plone.app.discussion tool"
|
||||
id = "portal_discussion"
|
||||
|
||||
def reindexObject(self, object):
|
||||
# Reindex in catalog.
|
||||
catalog = getToolByName(self, 'portal_catalog')
|
||||
catalog = getToolByName(self, "portal_catalog")
|
||||
return catalog.reindexObject(object)
|
||||
|
||||
indexObject = reindexObject
|
||||
|
||||
def unindexObject(self, object):
|
||||
# Remove from catalog.
|
||||
catalog = getToolByName(self, 'portal_catalog')
|
||||
catalog = getToolByName(self, "portal_catalog")
|
||||
return catalog.unindexObject(object)
|
||||
|
||||
def uniqueValuesFor(self, name):
|
||||
# return unique values for FieldIndex name
|
||||
catalog = getToolByName(self, 'portal_catalog')
|
||||
catalog = getToolByName(self, "portal_catalog")
|
||||
return catalog.uniqueValuesFor(name)
|
||||
|
||||
def searchResults(self, REQUEST=None, **kw):
|
||||
# Calls ZCatalog.searchResults with extra arguments that
|
||||
# limit the results to what the user is allowed to see.
|
||||
catalog = getToolByName(self, 'portal_catalog')
|
||||
catalog = getToolByName(self, "portal_catalog")
|
||||
object_provides = [IComment.__identifier__]
|
||||
|
||||
if 'object_provides' in kw:
|
||||
kw_provides = kw['object_provides']
|
||||
if "object_provides" in kw:
|
||||
kw_provides = kw["object_provides"]
|
||||
if isinstance(str, kw_provides):
|
||||
object_provides.append(kw_provides)
|
||||
else:
|
||||
object_provides.extend(kw_provides)
|
||||
|
||||
if REQUEST is not None and 'object_provides' in REQUEST.form:
|
||||
rq_provides = REQUEST.form['object_provides']
|
||||
del REQUEST.form['object_provides']
|
||||
if REQUEST is not None and "object_provides" in REQUEST.form:
|
||||
rq_provides = REQUEST.form["object_provides"]
|
||||
del REQUEST.form["object_provides"]
|
||||
if isinstance(str, rq_provides):
|
||||
object_provides.append(rq_provides)
|
||||
else:
|
||||
object_provides.extend(rq_provides)
|
||||
|
||||
kw['object_provides'] = object_provides
|
||||
kw["object_provides"] = object_provides
|
||||
return catalog.searchResults(REQUEST, **kw)
|
||||
|
||||
|
||||
def index_object(obj, event):
|
||||
"""Index the object when added to the conversation
|
||||
"""
|
||||
"""Index the object when added to the conversation"""
|
||||
tool = queryUtility(ICommentingTool)
|
||||
if tool is not None:
|
||||
tool.indexObject(obj)
|
||||
|
||||
|
||||
def unindex_object(obj, event):
|
||||
"""Unindex the object when removed
|
||||
"""
|
||||
"""Unindex the object when removed"""
|
||||
tool = queryUtility(ICommentingTool)
|
||||
if tool is not None:
|
||||
tool.unindexObject(obj)
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.interfaces import IDiscussionSettings
|
||||
from plone.registry.interfaces import IRegistry
|
||||
from Products.CMFCore.utils import getToolByName
|
||||
@ -7,8 +6,8 @@ from zope.component import getUtility
|
||||
import logging
|
||||
|
||||
|
||||
default_profile = 'profile-plone.app.discussion:default'
|
||||
logger = logging.getLogger('plone.app.discussion')
|
||||
default_profile = "profile-plone.app.discussion:default"
|
||||
logger = logging.getLogger("plone.app.discussion")
|
||||
|
||||
|
||||
def update_registry(context):
|
||||
@ -17,7 +16,7 @@ def update_registry(context):
|
||||
|
||||
|
||||
def update_rolemap(context):
|
||||
context.runImportStepFromProfile(default_profile, 'rolemap')
|
||||
context.runImportStepFromProfile(default_profile, "rolemap")
|
||||
|
||||
|
||||
def upgrade_comment_workflows_retain_current_workflow(context):
|
||||
@ -25,16 +24,16 @@ def upgrade_comment_workflows_retain_current_workflow(context):
|
||||
# import step will change it to comment_one_state_workflow. This is good.
|
||||
# If it was anything else, we should restore this. So get the original
|
||||
# chain.
|
||||
portal_type = 'Discussion Item'
|
||||
wf_tool = getToolByName(context, 'portal_workflow')
|
||||
portal_type = "Discussion Item"
|
||||
wf_tool = getToolByName(context, "portal_workflow")
|
||||
orig_chain = list(wf_tool.getChainFor(portal_type))
|
||||
|
||||
# Run the workflow step. This sets the chain to
|
||||
# comment_one_state_workflow.
|
||||
context.runImportStepFromProfile(default_profile, 'workflow')
|
||||
context.runImportStepFromProfile(default_profile, "workflow")
|
||||
|
||||
# Restore original workflow chain if needed.
|
||||
old_workflow = 'one_state_workflow'
|
||||
old_workflow = "one_state_workflow"
|
||||
if old_workflow not in orig_chain:
|
||||
# Restore the chain. Probably comment_review_workflow.
|
||||
wf_tool.setChainForPortalTypes([portal_type], orig_chain)
|
||||
@ -43,7 +42,7 @@ def upgrade_comment_workflows_retain_current_workflow(context):
|
||||
if old_workflow in orig_chain:
|
||||
# Replace with new one.
|
||||
idx = orig_chain.index(old_workflow)
|
||||
orig_chain[idx] = 'comment_one_state_workflow'
|
||||
orig_chain[idx] = "comment_one_state_workflow"
|
||||
# Restore the chain.
|
||||
wf_tool.setChainForPortalTypes([portal_type], orig_chain)
|
||||
|
||||
@ -51,9 +50,9 @@ def upgrade_comment_workflows_retain_current_workflow(context):
|
||||
def upgrade_comment_workflows_apply_rolemapping(context):
|
||||
# Now go over the comments, update their role mappings, and reindex the
|
||||
# allowedRolesAndUsers index.
|
||||
portal_type = 'Discussion Item'
|
||||
catalog = getToolByName(context, 'portal_catalog')
|
||||
wf_tool = getToolByName(context, 'portal_workflow')
|
||||
portal_type = "Discussion Item"
|
||||
catalog = getToolByName(context, "portal_catalog")
|
||||
wf_tool = getToolByName(context, "portal_workflow")
|
||||
new_chain = list(wf_tool.getChainFor(portal_type))
|
||||
workflows = [wf_tool.getWorkflowById(wf_id) for wf_id in new_chain]
|
||||
for brain in catalog.unrestrictedSearchResults(portal_type=portal_type):
|
||||
@ -63,7 +62,7 @@ def upgrade_comment_workflows_apply_rolemapping(context):
|
||||
wf.updateRoleMappingsFor(comment)
|
||||
comment.reindexObjectSecurity()
|
||||
except (AttributeError, KeyError):
|
||||
logger.info('Could not reindex comment {0}'.format(brain.getURL()))
|
||||
logger.info(f"Could not reindex comment {brain.getURL()}")
|
||||
|
||||
|
||||
def upgrade_comment_workflows(context):
|
||||
@ -72,7 +71,7 @@ def upgrade_comment_workflows(context):
|
||||
|
||||
|
||||
def add_js_to_plone_legacy(context):
|
||||
context.runImportStepFromProfile(default_profile, 'plone.app.registry')
|
||||
context.runImportStepFromProfile(default_profile, "plone.app.registry")
|
||||
|
||||
|
||||
def extend_review_workflow(context):
|
||||
|
@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from plone.app.discussion.interfaces import _
|
||||
from zope.schema.vocabulary import SimpleTerm
|
||||
from zope.schema.vocabulary import SimpleVocabulary
|
||||
@ -7,6 +6,7 @@ from zope.schema.vocabulary import SimpleVocabulary
|
||||
HAS_CAPTCHA = False
|
||||
try:
|
||||
import plone.formwidget.captcha # noqa
|
||||
|
||||
HAS_CAPTCHA = True # pragma: no cover
|
||||
except ImportError:
|
||||
pass
|
||||
@ -14,6 +14,7 @@ except ImportError:
|
||||
HAS_RECAPTCHA = False
|
||||
try:
|
||||
import plone.formwidget.recaptcha # noqa
|
||||
|
||||
HAS_RECAPTCHA = True # pragma: no cover
|
||||
except ImportError:
|
||||
pass
|
||||
@ -21,6 +22,7 @@ except ImportError:
|
||||
HAS_AKISMET = False
|
||||
try:
|
||||
import collective.akismet # noqa
|
||||
|
||||
HAS_AKISMET = True # pragma: no cover
|
||||
except ImportError:
|
||||
pass
|
||||
@ -28,73 +30,48 @@ except ImportError:
|
||||
HAS_NOROBOTS = False
|
||||
try:
|
||||
import collective.z3cform.norobots # noqa
|
||||
|
||||
HAS_NOROBOTS = True # pragma: no cover
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def captcha_vocabulary(context):
|
||||
"""Vocabulary with all available captcha implementations.
|
||||
"""
|
||||
"""Vocabulary with all available captcha implementations."""
|
||||
terms = []
|
||||
terms.append(
|
||||
SimpleTerm(
|
||||
value='disabled',
|
||||
token='disabled',
|
||||
title=_(u'Disabled')))
|
||||
terms.append(SimpleTerm(value="disabled", token="disabled", title=_("Disabled")))
|
||||
|
||||
if HAS_CAPTCHA: # pragma: no cover
|
||||
terms.append(
|
||||
SimpleTerm(
|
||||
value='captcha',
|
||||
token='captcha',
|
||||
title='Captcha'))
|
||||
terms.append(SimpleTerm(value="captcha", token="captcha", title="Captcha"))
|
||||
|
||||
if HAS_RECAPTCHA: # pragma: no cover
|
||||
terms.append(
|
||||
SimpleTerm(
|
||||
value='recaptcha',
|
||||
token='recaptcha',
|
||||
title='ReCaptcha'))
|
||||
SimpleTerm(value="recaptcha", token="recaptcha", title="ReCaptcha")
|
||||
)
|
||||
|
||||
if HAS_AKISMET: # pragma: no cover
|
||||
terms.append(
|
||||
SimpleTerm(
|
||||
value='akismet',
|
||||
token='akismet',
|
||||
title='Akismet'))
|
||||
terms.append(SimpleTerm(value="akismet", token="akismet", title="Akismet"))
|
||||
|
||||
if HAS_NOROBOTS: # pragma: no cover
|
||||
terms.append(
|
||||
SimpleTerm(
|
||||
value='norobots',
|
||||
token='norobots',
|
||||
title='Norobots'))
|
||||
terms.append(SimpleTerm(value="norobots", token="norobots", title="Norobots"))
|
||||
return SimpleVocabulary(terms)
|
||||
|
||||
|
||||
def text_transform_vocabulary(context):
|
||||
"""Vocabulary with all available portal_transform transformations.
|
||||
"""
|
||||
"""Vocabulary with all available portal_transform transformations."""
|
||||
terms = []
|
||||
terms.append(SimpleTerm(value="text/plain", token="text/plain", title="Plain text"))
|
||||
terms.append(SimpleTerm(value="text/html", token="text/html", title="HTML"))
|
||||
terms.append(
|
||||
SimpleTerm(
|
||||
value='text/plain',
|
||||
token='text/plain',
|
||||
title='Plain text'))
|
||||
value="text/x-web-markdown", token="text/x-web-markdown", title="Markdown"
|
||||
)
|
||||
)
|
||||
terms.append(
|
||||
SimpleTerm(
|
||||
value='text/html',
|
||||
token='text/html',
|
||||
title='HTML'))
|
||||
terms.append(
|
||||
SimpleTerm(
|
||||
value='text/x-web-markdown',
|
||||
token='text/x-web-markdown',
|
||||
title='Markdown'))
|
||||
terms.append(
|
||||
SimpleTerm(
|
||||
value='text/x-web-intelligent',
|
||||
token='text/x-web-intelligent',
|
||||
title='Intelligent text'))
|
||||
value="text/x-web-intelligent",
|
||||
token="text/x-web-intelligent",
|
||||
title="Intelligent text",
|
||||
)
|
||||
)
|
||||
return SimpleVocabulary(terms)
|
||||
|
@ -11,3 +11,10 @@ universal = 1
|
||||
|
||||
[zest.releaser]
|
||||
create-wheel = yes
|
||||
|
||||
[isort]
|
||||
# black compatible Plone isort rules:
|
||||
profile = black
|
||||
force_alphabetical_sort = True
|
||||
force_single_line = True
|
||||
lines_after_imports = 2
|
||||
|
113
setup.py
113
setup.py
@ -1,73 +1,62 @@
|
||||
# encoding: utf-8
|
||||
|
||||
from setuptools import find_packages
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
version = '4.0.0a7.dev0'
|
||||
version = "4.0.0a7.dev0"
|
||||
|
||||
install_requires = [
|
||||
'setuptools',
|
||||
'plone.app.layout',
|
||||
'plone.app.registry',
|
||||
'plone.app.uuid',
|
||||
'plone.app.z3cform',
|
||||
'plone.indexer',
|
||||
'plone.registry',
|
||||
'plone.z3cform',
|
||||
'six',
|
||||
'ZODB3',
|
||||
'zope.interface',
|
||||
'zope.component',
|
||||
'zope.annotation',
|
||||
'zope.event',
|
||||
'zope.container',
|
||||
'zope.lifecycleevent',
|
||||
'zope.site',
|
||||
'z3c.form>=2.3.3',
|
||||
"setuptools",
|
||||
"plone.app.layout",
|
||||
"plone.app.registry",
|
||||
"plone.app.uuid",
|
||||
"plone.app.z3cform",
|
||||
"plone.base",
|
||||
"plone.indexer",
|
||||
"plone.z3cform",
|
||||
"z3c.form>=2.3.3",
|
||||
]
|
||||
|
||||
setup(name='plone.app.discussion',
|
||||
version=version,
|
||||
description='Enhanced discussion support for Plone',
|
||||
long_description=open('README.rst').read() + '\n' +
|
||||
open('CHANGES.rst').read(),
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Plone",
|
||||
"Framework :: Plone :: 6.0",
|
||||
"Framework :: Plone :: Core",
|
||||
"Framework :: Zope :: 5",
|
||||
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
keywords='plone discussion',
|
||||
author='Timo Stollenwerk - Plone Foundation',
|
||||
author_email='plone-developers@lists.sourceforge.net',
|
||||
url='https://pypi.org/project/plone.app.discussion',
|
||||
license='GPL',
|
||||
packages=find_packages(),
|
||||
namespace_packages=['plone', 'plone.app'],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
'test': [
|
||||
'plone.app.testing',
|
||||
'plone.stringinterp',
|
||||
'plone.contentrules',
|
||||
'plone.app.contentrules',
|
||||
'plone.app.contenttypes[test]',
|
||||
'plone.app.robotframework',
|
||||
],
|
||||
},
|
||||
entry_points="""
|
||||
setup(
|
||||
name="plone.app.discussion",
|
||||
version=version,
|
||||
description="Enhanced discussion support for Plone",
|
||||
long_description=open("README.rst").read() + "\n" + open("CHANGES.rst").read(),
|
||||
classifiers=[
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Framework :: Plone",
|
||||
"Framework :: Plone :: 6.0",
|
||||
"Framework :: Plone :: Core",
|
||||
"Framework :: Zope :: 5",
|
||||
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
],
|
||||
keywords="plone discussion",
|
||||
author="Timo Stollenwerk - Plone Foundation",
|
||||
author_email="plone-developers@lists.sourceforge.net",
|
||||
url="https://pypi.org/project/plone.app.discussion",
|
||||
license="GPL",
|
||||
packages=find_packages(),
|
||||
namespace_packages=["plone", "plone.app"],
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
install_requires=install_requires,
|
||||
extras_require={
|
||||
"test": [
|
||||
"plone.app.testing",
|
||||
"plone.stringinterp",
|
||||
"plone.contentrules",
|
||||
"plone.app.contentrules",
|
||||
"plone.app.contenttypes[test]",
|
||||
"plone.app.robotframework",
|
||||
],
|
||||
},
|
||||
entry_points="""
|
||||
[z3c.autoinclude.plugin]
|
||||
target = plone
|
||||
""",
|
||||
)
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user