Merge pull request #126 from plone/moderation

Show email in moderation view
This commit is contained in:
Gil Forcada Codinachs 2017-07-29 23:22:28 +02:00 committed by GitHub
commit a313350e35
18 changed files with 152 additions and 82 deletions

View File

@ -14,6 +14,8 @@ New features:
Bug fixes:
- Show email in moderation view [ksuess]
- Remove plone.app.robotframework extras (reload and ride).
They are not needed and they are not Python 3 compatible.
[gforcada]

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

@ -47,7 +47,7 @@ comment form with the "website" field::
from zope import schema
from zope.annotation import factory
from zope.component import adapts
from zope.component import adapter
from zope.interface import Interface
from zope.publisher.interfaces.browser import IDefaultBrowserLayer
@ -61,9 +61,9 @@ comment form with the "website" field::
website = schema.TextLine(title=u"Website", required=False)
# Persistent class that implements the ICommentExtenderFields interface
@adapter(Comment)
class CommentExtenderFields(Persistent):
interface.implements(ICommentExtenderFields)
adapts(Comment)
website = u""
# CommentExtenderFields factory
@ -71,9 +71,8 @@ comment form with the "website" field::
# Extending the comment form with the fields defined in the
# ICommentExtenderFields interface.
@adapter(Interface, IDefaultBrowserLayer, CommentForm)
class CommentExtender(extensible.FormExtender):
adapts(Interface, IDefaultBrowserLayer, CommentForm)
fields = Fields(ICommentExtenderFields)
def __init__(self, context, request, form):

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

@ -88,7 +88,8 @@
<tbody>
<tal:block repeat="item batch">
<tr class="commentrow"
tal:define="even repeat/item/even"
tal:define="even repeat/item/even;
email python:getattr(item.getObject(), 'author_email');"
tal:attributes="class python: even and 'odd' or 'even'">
<td class="notDraggable">
<input type="checkbox"
@ -103,8 +104,16 @@
<input type="hidden" name="selected_obj_paths:list" value="#"
tal:attributes="value item/getURL" />
</td>
<td tal:content="python:item.author_name or item.Creator" />
<td tal:content="python:toLocalizedTime(item.ModificationDate, long_format=1)" />
<td>
<span tal:content="python:item.author_name or item.Creator">Name</span>
<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, long_format=1)"/>
<td>
<a tal:attributes="href item/getURL"
tal:content="item/in_response_to" />

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
@ -14,6 +14,7 @@ from zope.traversing.interfaces import TraversalError
@implementer(ITraversable)
@adapter(Interface, IBrowserRequest)
class ConversationNamespace(object):
"""Allow traversal into a conversation via a ++conversation++name
namespace. The name is the name of an adapter from context to
@ -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
@ -33,8 +33,8 @@ except ImportError:
@implementer(IValidator)
@adapter(Interface, IDiscussionLayer, Interface, IField, Interface)
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,7 @@ 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 +98,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
@ -326,12 +325,12 @@ else:
@implementer(IReplies)
@adapter(Conversation) # relies on implementation details
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
@ -402,6 +401,7 @@ class ConversationReplies(object):
@implementer(IReplies)
@adapter(Comment)
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
""",
)