This commit is contained in:
Katja Süss 2017-07-28 17:58:35 +02:00 committed by ksuess
parent fb7c68d5e5
commit 78abff152d
16 changed files with 140 additions and 77 deletions

View File

@ -17,7 +17,7 @@ import os
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here. # documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.append(os.path.abspath('.')) # sys.path.append(os.path.abspath('.'))
# -- General configuration ---------------------------------------------------- # -- General configuration ----------------------------------------------------
@ -95,7 +95,7 @@ pygments_style = 'sphinx'
#modindex_common_prefix = [] #modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with # The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'. # Sphinx are currently 'default' and 'sphinxdoc'.
@ -169,7 +169,7 @@ html_static_path = ['_static']
htmlhelp_basename = 'ploneappdiscussiondoc' htmlhelp_basename = 'ploneappdiscussiondoc'
# -- Options for LaTeX output -------------------------------------------------- # -- Options for LaTeX output --------------------------------------------
# The paper size ('letter' or 'a4'). # The paper size ('letter' or 'a4').
#latex_paper_size = 'letter' #latex_paper_size = 'letter'
@ -180,8 +180,8 @@ htmlhelp_basename = 'ploneappdiscussiondoc'
# Grouping the document tree into LaTeX files. List of tuples # Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]). # (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [ latex_documents = [
('index', 'ploneappdiscussion.tex', u'plone.app.discussion Documentation', ('index', 'ploneappdiscussion.tex', u'plone.app.discussion Documentation',
u'Timo Stollenwerk', 'manual'), u'Timo Stollenwerk', 'manual'),
] ]
# The name of an image file (relative to this directory) to place at the top of # The name of an image file (relative to this directory) to place at the top of

View File

@ -12,29 +12,30 @@ from z3c.form import interfaces
from z3c.form.field import Fields from z3c.form.field import Fields
from zope import interface from zope import interface
from zope.annotation import factory from zope.annotation import factory
from zope.component import adapts from zope.component import adapter
from zope.component import queryUtility from zope.component import queryUtility
from zope.interface import Interface from zope.interface import Interface
from zope.publisher.interfaces.browser import IDefaultBrowserLayer from zope.publisher.interfaces.browser import IDefaultBrowserLayer
@adapter(Comment)
@interface.implementer(ICaptcha) @interface.implementer(ICaptcha)
class Captcha(Persistent): class Captcha(Persistent):
"""Captcha input field. """Captcha input field.
""" """
adapts(Comment) captcha = u''
captcha = u""
Captcha = factory(Captcha) Captcha = factory(Captcha)
# context, request, form
@adapter(Interface, IDefaultBrowserLayer, CommentForm)
class CaptchaExtender(extensible.FormExtender): class CaptchaExtender(extensible.FormExtender):
"""Extends the comment form with a Captcha. This Captcha extender is only """Extends the comment form with a Captcha. This Captcha extender is only
registered when a plugin is installed that provides the registered when a plugin is installed that provides the
"plone.app.discussion-captcha" feature. "plone.app.discussion-captcha" feature.
""" """
# context, request, form
adapts(Interface, IDefaultBrowserLayer, CommentForm)
fields = Fields(ICaptcha) fields = Fields(ICaptcha)
@ -65,5 +66,3 @@ class CaptchaExtender(extensible.FormExtender):
self.form.fields['captcha'].widgetFactory = NorobotsFieldWidget self.form.fields['captcha'].widgetFactory = NorobotsFieldWidget
else: else:
self.form.fields['captcha'].mode = interfaces.HIDDEN_MODE self.form.fields['captcha'].mode = interfaces.HIDDEN_MODE

View File

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # coding: utf-8
from AccessControl import getSecurityManager from AccessControl import getSecurityManager
from Acquisition import aq_inner from Acquisition import aq_inner
from Acquisition import aq_parent from Acquisition import aq_parent
@ -112,6 +112,5 @@ class EditCommentForm(CommentForm):
type='info') type='info')
return self._redirect(target=self.context.absolute_url()) return self._redirect(target=self.context.absolute_url())
EditComment = wrap_form(EditCommentForm)
# EOF EditComment = wrap_form(EditCommentForm)

View File

@ -4,7 +4,7 @@ IDiscussion container for the context, from which traversal will continue
into an actual comment object. into an actual comment object.
""" """
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IConversation
from zope.component import adapts from zope.component import adapter
from zope.component import queryAdapter from zope.component import queryAdapter
from zope.interface import implementer from zope.interface import implementer
from zope.interface import Interface from zope.interface import Interface
@ -13,6 +13,7 @@ from zope.traversing.interfaces import ITraversable
from zope.traversing.interfaces import TraversalError from zope.traversing.interfaces import TraversalError
@adapter(Interface, IBrowserRequest)
@implementer(ITraversable) @implementer(ITraversable)
class ConversationNamespace(object): class ConversationNamespace(object):
"""Allow traversal into a conversation via a ++conversation++name """Allow traversal into a conversation via a ++conversation++name
@ -21,7 +22,6 @@ class ConversationNamespace(object):
(unnamed) adapter. This is to work around a bug in OFS.Traversable which (unnamed) adapter. This is to work around a bug in OFS.Traversable which
does not allow traversal to namespaces with an empty string name. does not allow traversal to namespaces with an empty string name.
""" """
adapts(Interface, IBrowserRequest)
def __init__(self, context, request=None): def __init__(self, context, request=None):
self.context = context self.context = context

View File

@ -8,7 +8,7 @@ from plone.app.discussion.interfaces import IDiscussionSettings
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
from z3c.form import validator from z3c.form import validator
from z3c.form.interfaces import IValidator from z3c.form.interfaces import IValidator
from zope.component import adapts from zope.component import adapter
from zope.component import getMultiAdapter from zope.component import getMultiAdapter
from zope.component import queryUtility from zope.component import queryUtility
from zope.interface import implementer from zope.interface import implementer
@ -32,9 +32,9 @@ except ImportError:
pass pass
@adapter(Interface, IDiscussionLayer, Interface, IField, Interface)
@implementer(IValidator) @implementer(IValidator)
class CaptchaValidator(validator.SimpleFieldValidator): class CaptchaValidator(validator.SimpleFieldValidator):
adapts(Interface, IDiscussionLayer, Interface, IField, Interface)
# Object, Request, Form, Field, Widget, # Object, Request, Form, Field, Widget,
# We adapt the CaptchaValidator class to all form fields (IField) # We adapt the CaptchaValidator class to all form fields (IField)

View File

@ -77,8 +77,10 @@ def creator(object):
@indexer(IComment) @indexer(IComment)
def description(object): def description(object):
# Return the first 25 words of the comment text and append ' [...]' # Return the first 25 words of the comment text and append ' [...]'
text = join(object.getText( text = join(
targetMimetype='text/plain').split()[:MAX_DESCRIPTION]) object.getText(targetMimetype='text/plain')
.split()[:MAX_DESCRIPTION]
)
if len(object.getText().split()) > 25: if len(object.getText().split()) > 25:
text += ' [...]' text += ' [...]'
return text return text
@ -99,37 +101,43 @@ def in_response_to(object):
@indexer(IComment) @indexer(IComment)
def effective(object): def effective(object):
# the catalog index needs Zope DateTime instead of Python datetime # the catalog index needs Zope DateTime instead of Python datetime
return DateTime(object.creation_date.year, return DateTime(
object.creation_date.month, object.creation_date.year,
object.creation_date.day, object.creation_date.month,
object.creation_date.hour, object.creation_date.day,
object.creation_date.minute, object.creation_date.hour,
object.creation_date.second, object.creation_date.minute,
'GMT') object.creation_date.second,
'GMT',
)
@indexer(IComment) @indexer(IComment)
def created(object): def created(object):
# the catalog index needs Zope DateTime instead of Python datetime # the catalog index needs Zope DateTime instead of Python datetime
return DateTime(object.creation_date.year, return DateTime(
object.creation_date.month, object.creation_date.year,
object.creation_date.day, object.creation_date.month,
object.creation_date.hour, object.creation_date.day,
object.creation_date.minute, object.creation_date.hour,
object.creation_date.second, object.creation_date.minute,
'GMT') object.creation_date.second,
'GMT',
)
@indexer(IComment) @indexer(IComment)
def modified(object): def modified(object):
# the catalog index needs Zope DateTime instead of Python datetime # the catalog index needs Zope DateTime instead of Python datetime
return DateTime(object.modification_date.year, return DateTime(
object.modification_date.month, object.modification_date.year,
object.modification_date.day, object.modification_date.month,
object.modification_date.hour, object.modification_date.day,
object.modification_date.minute, object.modification_date.hour,
object.modification_date.second, object.modification_date.minute,
'GMT') object.modification_date.second,
'GMT',
)
# Override the conversation indexers for comments # Override the conversation indexers for comments

View File

@ -42,7 +42,8 @@ import logging
COMMENT_TITLE = _( COMMENT_TITLE = _(
u'comment_title', u'comment_title',
default=u'${author_name} on ${content}') default=u'${author_name} on ${content}',
)
MAIL_NOTIFICATION_MESSAGE = _( MAIL_NOTIFICATION_MESSAGE = _(
u'mail_notification_message', u'mail_notification_message',
@ -50,7 +51,8 @@ MAIL_NOTIFICATION_MESSAGE = _(
u'has been posted here: ${link}\n\n' u'has been posted here: ${link}\n\n'
u'---\n' u'---\n'
u'${text}\n' u'${text}\n'
u'---\n') u'---\n',
)
MAIL_NOTIFICATION_MESSAGE_MODERATOR = _( MAIL_NOTIFICATION_MESSAGE_MODERATOR = _(
u'mail_notification_message_moderator', u'mail_notification_message_moderator',
@ -60,7 +62,8 @@ MAIL_NOTIFICATION_MESSAGE_MODERATOR = _(
u'${text}\n' u'${text}\n'
u'---\n\n' u'---\n\n'
u'Approve comment:\n${link_approve}\n\n' u'Approve comment:\n${link_approve}\n\n'
u'Delete comment:\n${link_delete}\n') u'Delete comment:\n${link_delete}\n',
)
logger = logging.getLogger('plone.app.discussion') logger = logging.getLogger('plone.app.discussion')

View File

@ -30,7 +30,6 @@ from Products.CMFPlone.interfaces import IHideFromBreadcrumbs
from zope.annotation.interfaces import IAnnotatable from zope.annotation.interfaces import IAnnotatable
from zope.annotation.interfaces import IAnnotations from zope.annotation.interfaces import IAnnotations
from zope.component import adapter from zope.component import adapter
from zope.component import adapts
from zope.container.contained import ContainerModifiedEvent from zope.container.contained import ContainerModifiedEvent
from zope.event import notify from zope.event import notify
from zope.interface import implementer from zope.interface import implementer
@ -325,13 +324,13 @@ else:
return conversationAdapterFactory(content) return conversationAdapterFactory(content)
@adapter(Conversation) # relies on implementation details
@implementer(IReplies) @implementer(IReplies)
class ConversationReplies(object): class ConversationReplies(object):
"""An IReplies adapter for conversations. """An IReplies adapter for conversations.
This makes it easy to work with top-level comments. This makes it easy to work with top-level comments.
""" """
adapts(Conversation) # relies on implementation details
def __init__(self, context): def __init__(self, context):
self.conversation = context self.conversation = context
@ -401,6 +400,7 @@ class ConversationReplies(object):
return self.conversation._children.get(self.comment_id, LLSet()) return self.conversation._children.get(self.comment_id, LLSet())
@adapter(Comment)
@implementer(IReplies) @implementer(IReplies)
class CommentReplies(ConversationReplies): class CommentReplies(ConversationReplies):
"""An IReplies adapter for comments. """An IReplies adapter for comments.
@ -412,8 +412,6 @@ class CommentReplies(ConversationReplies):
# most likely, anyone writing a different type of Conversation will also # most likely, anyone writing a different type of Conversation will also
# have a different type of Comment # have a different type of Comment
adapts(Comment)
def __init__(self, context): def __init__(self, context):
self.comment = context self.comment = context
self.conversation = aq_parent(self.comment) self.conversation = aq_parent(self.comment)

View File

@ -11,13 +11,15 @@ from zope.interface import Interface
from zope.interface import Invalid from zope.interface import Invalid
from zope.interface.common.mapping import IIterableMapping from zope.interface.common.mapping import IIterableMapping
def isEmail(value): def isEmail(value):
portal = getUtility(ISiteRoot) portal = getUtility(ISiteRoot)
reg_tool = getToolByName(portal, 'portal_registration') reg_tool = getToolByName(portal, 'portal_registration')
if not (value and reg_tool.isValidEmail(value)): if not (value and reg_tool.isValidEmail(value)):
raise Invalid(_("Invalid email address.")) raise Invalid(_('Invalid email address.'))
return True return True
class IConversation(IIterableMapping): class IConversation(IIterableMapping):
"""A conversation about a content object. """A conversation about a content object.
@ -160,7 +162,10 @@ class IComment(Interface):
# for anonymous comments only, set to None for logged in comments # for anonymous comments only, set to None for logged in comments
author_name = schema.TextLine(title=_(u'Name'), required=False) author_name = schema.TextLine(title=_(u'Name'), required=False)
author_email = schema.TextLine(title=_(u'Email'), required=False, constraint=isEmail) author_email = schema.TextLine(title=_(u'Email'),
required=False,
constraint=isEmail,
)
title = schema.TextLine(title=_(u'label_subject', title = schema.TextLine(title=_(u'label_subject',
default=u'Subject')) default=u'Subject'))

View File

@ -11,7 +11,6 @@ from plone.app.testing import TEST_USER_ID
from plone.registry.interfaces import IRegistry from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from zope.component import queryUtility from zope.component import queryUtility
from zope.configuration import xmlconfig
try: try:
@ -40,9 +39,9 @@ class PloneAppDiscussion(PloneSandboxLayer):
def setUpZope(self, app, configurationContext): def setUpZope(self, app, configurationContext):
# Load ZCML # Load ZCML
import plone.app.discussion import plone.app.discussion
xmlconfig.file('configure.zcml', self.loadZCML(package=plone.app.discussion,
plone.app.discussion, context=configurationContext,
context=configurationContext) )
def setUpPloneSite(self, portal): def setUpPloneSite(self, portal):
# Install into Plone site using portal_setup # Install into Plone site using portal_setup

View File

@ -182,7 +182,7 @@ flaw? Though, the comment is published properly.
>>> browser.handleErrors = False >>> browser.handleErrors = False
>>> browser.raiseHttpErrors = True >>> browser.raiseHttpErrors = True
Make sure anonyous users see the approved comment, but not the unapproved ones. Make sure anonymous users see the approved comment, but not the unapproved ones.
>>> unprivileged_browser.open(urldoc) >>> unprivileged_browser.open(urldoc)
>>> 'First anonymous comment' in unprivileged_browser.contents >>> 'First anonymous comment' in unprivileged_browser.contents
@ -230,3 +230,53 @@ Make sure the catalog has been updated properly.
>>> portal.portal_catalog.searchResults(id='doc', total_comments=0) >>> portal.portal_catalog.searchResults(id='doc', total_comments=0)
[<Products...] [<Products...]
Moderation view
---------------
Enable anonymous comment with email.
>>> browser.open(portal_url + '/logout')
>>> browser.open(portal_url + '/login_form')
>>> browser.getControl(name='__ac_name').value = 'admin'
>>> browser.getControl(name='__ac_password').value = 'secret'
>>> browser.getControl(name='submit').click()
>>> browser.open(portal_url+'/@@discussion-controlpanel')
>>> browser.getControl(name='form.widgets.anonymous_comments:list').value = 'selected'
>>> browser.getControl(name='form.widgets.anonymous_email_enabled:list').value = 'selected'
>>> browser.getControl(name='form.buttons.save').click()
>>> browser.open(portal_url + '/logout')
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_email').value = 'john@acme.com'
>>> unprivileged_browser.getControl(name='form.buttons.comment').click()
Check that the form has been properly submitted.
>>> unprivileged_browser.url
'http://nohost/plone/doc/document_view'
>>> 'Your comment awaits moderator approval.' in unprivileged_browser.contents
True
Change to Moderation view.
>>> browser.open(urldoc)
>>> browser.getLink("Moderate comments").click()
The new comment is shown in moderation view with authors name and email.
>>> browser.url
'http://nohost/plone/@@moderate-comments'
>>> 'John' in browser.contents
True
>>> 'john@acme.com' in browser.contents
True

View File

@ -175,8 +175,8 @@ class ConversationTest(unittest.TestCase):
del conversation[new_id_1] del conversation[new_id_1]
self.assertEqual([ self.assertEqual([
{'comment': comment2, 'depth': 0, 'id': new_id_2}, {'comment': comment2, 'depth': 0, 'id': new_id_2},
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1}, {'comment': comment2_1, 'depth': 1, 'id': new_id_2_1},
], list(conversation.getThreads())) ], list(conversation.getThreads()))
def test_delete_comment_when_content_object_is_deleted(self): def test_delete_comment_when_content_object_is_deleted(self):
@ -608,12 +608,12 @@ class ConversationTest(unittest.TestCase):
# Get threads # Get threads
self.assertEqual([ self.assertEqual([
{'comment': comment1, 'depth': 0, 'id': new_id_1}, {'comment': comment1, 'depth': 0, 'id': new_id_1},
{'comment': comment1_1, 'depth': 1, 'id': new_id_1_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_1_1, 'depth': 2, 'id': new_id_1_1_1},
{'comment': comment1_2, 'depth': 1, 'id': new_id_1_2}, {'comment': comment1_2, 'depth': 1, 'id': new_id_1_2},
{'comment': comment2, 'depth': 0, 'id': new_id_2}, {'comment': comment2, 'depth': 0, 'id': new_id_2},
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1}, {'comment': comment2_1, 'depth': 1, 'id': new_id_2_1},
], list(conversation.getThreads())) ], list(conversation.getThreads()))
def test_get_threads_batched(self): def test_get_threads_batched(self):

View File

@ -4,8 +4,8 @@ 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 # noqa
from plone.app.testing import setRoles from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID from plone.app.testing import TEST_USER_ID
from zope.component import createObject
from Zope2.App import zcml from Zope2.App import zcml
from zope.component import createObject
import Products.Five import Products.Five
import unittest import unittest

View File

@ -60,4 +60,4 @@ def upgrade_comment_workflows(context):
wf.updateRoleMappingsFor(comment) wf.updateRoleMappingsFor(comment)
comment.reindexObjectSecurity() comment.reindexObjectSecurity()
except (AttributeError, KeyError): except (AttributeError, KeyError):
logger.info('Could not reindex comment %s' % brain.getURL()) logger.info('Could not reindex comment {0}'.format(brain.getURL()))

View File

@ -1,6 +1,9 @@
# encoding: utf-8
from setuptools import find_packages from setuptools import find_packages
from setuptools import setup from setuptools import setup
version = '3.0.3.dev0' version = '3.0.3.dev0'
install_requires = [ install_requires = [
@ -26,15 +29,15 @@ install_requires = [
setup(name='plone.app.discussion', setup(name='plone.app.discussion',
version=version, version=version,
description="Enhanced discussion support for Plone", description='Enhanced discussion support for Plone',
long_description=open("README.rst").read() + "\n" + long_description=open('README.rst').read() + '\n' +
open("CHANGES.rst").read(), open('CHANGES.rst').read(),
classifiers=[ classifiers=[
"Framework :: Plone", 'Framework :: Plone',
"Framework :: Plone :: 5.0", 'Framework :: Plone :: 5.0',
"Framework :: Plone :: 5.1", 'Framework :: Plone :: 5.1',
"Programming Language :: Python", 'Programming Language :: Python',
"Programming Language :: Python :: 2.7", 'Programming Language :: Python :: 2.7',
], ],
keywords='plone discussion', keywords='plone discussion',
author='Timo Stollenwerk - Plone Foundation', author='Timo Stollenwerk - Plone Foundation',
@ -54,11 +57,10 @@ setup(name='plone.app.discussion',
'plone.app.contentrules', 'plone.app.contentrules',
'plone.app.contenttypes[test]', 'plone.app.contenttypes[test]',
'plone.app.robotframework', 'plone.app.robotframework',
] ],
}, },
entry_points=""" entry_points="""
[z3c.autoinclude.plugin] [z3c.autoinclude.plugin]
target = plone target = plone
""", """,
) )