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,
# 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.
#sys.path.append(os.path.abspath('.'))
# sys.path.append(os.path.abspath('.'))
# -- General configuration ----------------------------------------------------
@ -95,7 +95,7 @@ pygments_style = 'sphinx'
#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
# Sphinx are currently 'default' and 'sphinxdoc'.
@ -169,7 +169,7 @@ html_static_path = ['_static']
htmlhelp_basename = 'ploneappdiscussiondoc'
# -- Options for LaTeX output --------------------------------------------------
# -- Options for LaTeX output --------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
@ -180,8 +180,8 @@ htmlhelp_basename = 'ploneappdiscussiondoc'
# 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', u'plone.app.discussion Documentation',
u'Timo Stollenwerk', 'manual'),
]
# 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 zope import interface
from zope.annotation import factory
from zope.component import adapts
from zope.component import adapter
from zope.component import queryUtility
from zope.interface import Interface
from zope.publisher.interfaces.browser import IDefaultBrowserLayer
@adapter(Comment)
@interface.implementer(ICaptcha)
class Captcha(Persistent):
"""Captcha input field.
"""
adapts(Comment)
captcha = u""
captcha = u''
Captcha = factory(Captcha)
# context, request, form
@adapter(Interface, IDefaultBrowserLayer, CommentForm)
class CaptchaExtender(extensible.FormExtender):
"""Extends the comment form with a Captcha. This Captcha extender is only
registered when a plugin is installed that provides the
"plone.app.discussion-captcha" feature.
"""
# context, request, form
adapts(Interface, IDefaultBrowserLayer, CommentForm)
fields = Fields(ICaptcha)
@ -65,5 +66,3 @@ class CaptchaExtender(extensible.FormExtender):
self.form.fields['captcha'].widgetFactory = NorobotsFieldWidget
else:
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 Acquisition import aq_inner
from Acquisition import aq_parent
@ -112,6 +112,5 @@ class EditCommentForm(CommentForm):
type='info')
return self._redirect(target=self.context.absolute_url())
EditComment = wrap_form(EditCommentForm)
# EOF
EditComment = wrap_form(EditCommentForm)

View File

@ -106,11 +106,11 @@
</td>
<td>
<span tal:content="python:item.author_name or item.Creator">Name</span>
<tal:email tal:condition="email"><br /><a
<tal:email tal:condition="email"><br /><a
tal:attributes="href string:mailto:$email"
tal:content="email">Email</a></tal:email>
</td>
<td tal:content="python:toLocalizedTime(item.ModificationDate,
<td tal:content="python:toLocalizedTime(item.ModificationDate,
long_format=1)" />
<td>
<a tal:attributes="href item/getURL"

View File

@ -4,7 +4,7 @@ IDiscussion container for the context, from which traversal will continue
into an actual comment object.
"""
from plone.app.discussion.interfaces import IConversation
from zope.component import adapts
from zope.component import adapter
from zope.component import queryAdapter
from zope.interface import implementer
from zope.interface import Interface
@ -13,6 +13,7 @@ from zope.traversing.interfaces import ITraversable
from zope.traversing.interfaces import TraversalError
@adapter(Interface, IBrowserRequest)
@implementer(ITraversable)
class ConversationNamespace(object):
"""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
does not allow traversal to namespaces with an empty string name.
"""
adapts(Interface, IBrowserRequest)
def __init__(self, context, request=None):
self.context = context

View File

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

View File

@ -77,8 +77,10 @@ def creator(object):
@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 += ' [...]'
return text
@ -99,37 +101,43 @@ def in_response_to(object):
@indexer(IComment)
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')
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',
)
@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')
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',
)
@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')
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',
)
# Override the conversation indexers for comments

View File

@ -42,7 +42,8 @@ import logging
COMMENT_TITLE = _(
u'comment_title',
default=u'${author_name} on ${content}')
default=u'${author_name} on ${content}',
)
MAIL_NOTIFICATION_MESSAGE = _(
u'mail_notification_message',
@ -50,7 +51,8 @@ MAIL_NOTIFICATION_MESSAGE = _(
u'has been posted here: ${link}\n\n'
u'---\n'
u'${text}\n'
u'---\n')
u'---\n',
)
MAIL_NOTIFICATION_MESSAGE_MODERATOR = _(
u'mail_notification_message_moderator',
@ -60,7 +62,8 @@ MAIL_NOTIFICATION_MESSAGE_MODERATOR = _(
u'${text}\n'
u'---\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')

View File

@ -30,7 +30,6 @@ from Products.CMFPlone.interfaces import IHideFromBreadcrumbs
from zope.annotation.interfaces import IAnnotatable
from zope.annotation.interfaces import IAnnotations
from zope.component import adapter
from zope.component import adapts
from zope.container.contained import ContainerModifiedEvent
from zope.event import notify
from zope.interface import implementer
@ -325,13 +324,13 @@ else:
return conversationAdapterFactory(content)
@adapter(Conversation) # relies on implementation details
@implementer(IReplies)
class ConversationReplies(object):
"""An IReplies adapter for conversations.
This makes it easy to work with top-level comments.
"""
adapts(Conversation) # relies on implementation details
def __init__(self, context):
self.conversation = context
@ -401,6 +400,7 @@ class ConversationReplies(object):
return self.conversation._children.get(self.comment_id, LLSet())
@adapter(Comment)
@implementer(IReplies)
class CommentReplies(ConversationReplies):
"""An IReplies adapter for comments.
@ -412,8 +412,6 @@ class CommentReplies(ConversationReplies):
# most likely, anyone writing a different type of Conversation will also
# have a different type of Comment
adapts(Comment)
def __init__(self, context):
self.comment = context
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.common.mapping import IIterableMapping
def isEmail(value):
portal = getUtility(ISiteRoot)
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
class IConversation(IIterableMapping):
"""A conversation about a content object.
@ -160,7 +162,10 @@ class IComment(Interface):
# 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_email = schema.TextLine(title=_(u'Email'),
required=False,
constraint=isEmail,
)
title = schema.TextLine(title=_(u'label_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 Products.CMFCore.utils import getToolByName
from zope.component import queryUtility
from zope.configuration import xmlconfig
try:
@ -40,9 +39,9 @@ class PloneAppDiscussion(PloneSandboxLayer):
def setUpZope(self, app, configurationContext):
# Load ZCML
import plone.app.discussion
xmlconfig.file('configure.zcml',
plone.app.discussion,
context=configurationContext)
self.loadZCML(package=plone.app.discussion,
context=configurationContext,
)
def setUpPloneSite(self, portal):
# Install into Plone site using portal_setup

View File

@ -182,7 +182,7 @@ flaw? Though, the comment is published properly.
>>> browser.handleErrors = False
>>> 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)
>>> '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)
[<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]
self.assertEqual([
{'comment': comment2, 'depth': 0, 'id': new_id_2},
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1},
{'comment': comment2, 'depth': 0, 'id': new_id_2},
{'comment': comment2_1, 'depth': 1, 'id': new_id_2_1},
], list(conversation.getThreads()))
def test_delete_comment_when_content_object_is_deleted(self):
@ -608,12 +608,12 @@ 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, '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},
{'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):

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.testing import setRoles
from plone.app.testing import TEST_USER_ID
from zope.component import createObject
from Zope2.App import zcml
from zope.component import createObject
import Products.Five
import unittest

View File

@ -60,4 +60,4 @@ def upgrade_comment_workflows(context):
wf.updateRoleMappingsFor(comment)
comment.reindexObjectSecurity()
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 setup
version = '3.0.3.dev0'
install_requires = [
@ -26,15 +29,15 @@ install_requires = [
setup(name='plone.app.discussion',
version=version,
description="Enhanced discussion support for Plone",
long_description=open("README.rst").read() + "\n" +
open("CHANGES.rst").read(),
description='Enhanced discussion support for Plone',
long_description=open('README.rst').read() + '\n' +
open('CHANGES.rst').read(),
classifiers=[
"Framework :: Plone",
"Framework :: Plone :: 5.0",
"Framework :: Plone :: 5.1",
"Programming Language :: Python",
"Programming Language :: Python :: 2.7",
'Framework :: Plone',
'Framework :: Plone :: 5.0',
'Framework :: Plone :: 5.1',
'Programming Language :: Python',
'Programming Language :: Python :: 2.7',
],
keywords='plone discussion',
author='Timo Stollenwerk - Plone Foundation',
@ -54,11 +57,10 @@ setup(name='plone.app.discussion',
'plone.app.contentrules',
'plone.app.contenttypes[test]',
'plone.app.robotframework',
]
],
},
entry_points="""
[z3c.autoinclude.plugin]
target = plone
""",
)