Merge pull request #126 from plone/moderation
Show email in moderation view
This commit is contained in:
commit
a313350e35
@ -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]
|
||||
|
@ -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'
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,
|
||||
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')
|
||||
'GMT',
|
||||
)
|
||||
|
||||
|
||||
@indexer(IComment)
|
||||
def created(object):
|
||||
# the catalog index needs Zope DateTime instead of Python datetime
|
||||
return DateTime(object.creation_date.year,
|
||||
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')
|
||||
'GMT',
|
||||
)
|
||||
|
||||
|
||||
@indexer(IComment)
|
||||
def modified(object):
|
||||
# the catalog index needs Zope DateTime instead of Python datetime
|
||||
return DateTime(object.modification_date.year,
|
||||
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')
|
||||
'GMT',
|
||||
)
|
||||
|
||||
|
||||
# Override the conversation indexers for comments
|
||||
|
@ -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')
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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'))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()))
|
||||
|
22
setup.py
22
setup.py
@ -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
|
||||
""",
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user