Whitespace.

This commit is contained in:
Timo Stollenwerk 2012-01-09 16:31:52 +01:00
parent c5864ca9f7
commit 1f01a71aac
7 changed files with 84 additions and 84 deletions

View File

@ -1,7 +1,7 @@
[buildout] [buildout]
extends = extends =
http://svn.plone.org/svn/collective/buildout/plonetest/plone-4.1.x.cfg http://svn.plone.org/svn/collective/buildout/plonetest/plone-4.1.x.cfg
package-name = plone.app.discussion package-name = plone.app.discussion
package-directory = plone/app/discussion package-directory = plone/app/discussion
parts += instance parts += instance

View File

@ -2,7 +2,7 @@
extends = extends =
buildout.cfg buildout.cfg
extensions = buildout.eggtractor mr.developer extensions = buildout.eggtractor mr.developer
tractor-src-directory = tractor-src-directory =
. .
src src
auto-checkout = auto-checkout =
@ -15,7 +15,7 @@ auto-checkout =
parts += parts +=
omelette omelette
releaser releaser
pocompile pocompile
zopepy zopepy
sphinxbuilder sphinxbuilder
sphinxupload sphinxupload

View File

@ -19,19 +19,19 @@ plone.app.discussion.
permissions. permissions.
**Discussion items are light weight objects** **Discussion items are light weight objects**
Discussion item objects are as light weight as possible. Ideally, a Discussion item objects are as light weight as possible. Ideally, a
discussion item should be as lightweight as a catalog brain. This may mean discussion item should be as lightweight as a catalog brain. This may mean
that we forego convenience base classes and re-implement certain interfaces. that we forego convenience base classes and re-implement certain interfaces.
Comments should not provide the full set of dublin core metadata, though Comments should not provide the full set of dublin core metadata, though
custom indexers can be used to provide values for standard catalog indexes. custom indexers can be used to provide values for standard catalog indexes.
**Optimise for retrival speed** **Optimise for retrival speed**
HTML filtering and other processing should happen on save, not on render, HTML filtering and other processing should happen on save, not on render,
to make rendering quick. to make rendering quick.
**Settings are stored using plone.registry** **Settings are stored using plone.registry**
Any global setting should be stored in plone.registry records. Any global setting should be stored in plone.registry records.
**Forms are constructed using extensible z3c.form forms** **Forms are constructed using extensible z3c.form forms**
This allows plugins (such as spam protection algorithms) to provide This allows plugins (such as spam protection algorithms) to provide
additional validation. It also allows integrators to write add-ons that add additional validation. It also allows integrators to write add-ons that add
@ -45,15 +45,15 @@ plone.app.discussion.
not stored threaded, the dict interface should act as if they are, i.e. not stored threaded, the dict interface should act as if they are, i.e.
calling items() on a comment should return the replies to that comment calling items() on a comment should return the replies to that comment
(in order). (in order).
**Discussion items are retrieved in reverse creation date order** **Discussion items are retrieved in reverse creation date order**
Discussion items do not need to support explicit ordering. They should Discussion items do not need to support explicit ordering. They should
always be retrieved in reverse creation date order (most recent for). always be retrieved in reverse creation date order (most recent for).
They can be stored with keys so that this is always true. They can be stored with keys so that this is always true.
**Discussion items do not need readable ids** **Discussion items do not need readable ids**
Ids can be based on the creation date. Ids can be based on the creation date.
**Discussion items send events** **Discussion items send events**
The usual zope.lifecycleevent and zope.container events are fired when The usual zope.lifecycleevent and zope.container events are fired when
discussion items are added, removed, or modified. discussion items are added, removed, or modified.

View File

@ -71,67 +71,67 @@ logger = logging.getLogger("plone.app.discussion")
class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable, class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
RoleManager, Owned, Implicit, Persistent): RoleManager, Owned, Implicit, Persistent):
"""A comment. """A comment.
This object attempts to be as lightweight as possible. We implement a This object attempts to be as lightweight as possible. We implement a
number of standard methods instead of subclassing, to have total control number of standard methods instead of subclassing, to have total control
over what goes into the object. over what goes into the object.
""" """
implements(IComment) implements(IComment)
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 # This needs to be kept in sync with types/Discussion_Item.xml title
fti_title = 'Comment' fti_title = 'Comment'
__parent__ = None __parent__ = None
comment_id = None # long comment_id = None # long
in_reply_to = None # long in_reply_to = None # long
title = u"" title = u""
mime_type = None mime_type = None
text = u"" text = u""
creator = None creator = None
creation_date = None creation_date = None
modification_date = None modification_date = None
author_username = None author_username = None
author_name = None author_name = None
author_email = None author_email = None
user_notification = None user_notification = None
# Note: we want to use zope.component.createObject() to instantiate # Note: we want to use zope.component.createObject() to instantiate
# comments as far as possible. comment_id and __parent__ are set via # comments as far as possible. comment_id and __parent__ are set via
# IConversation.addComment(). # IConversation.addComment().
def __init__(self): def __init__(self):
self.creation_date = self.modification_date = datetime.utcnow() self.creation_date = self.modification_date = datetime.utcnow()
@property @property
def __name__(self): def __name__(self):
return self.comment_id and unicode(self.comment_id) or None return self.comment_id and unicode(self.comment_id) or None
@property @property
def id(self): def id(self):
return self.comment_id and str(self.comment_id) or None return self.comment_id and str(self.comment_id) or None
def getId(self): def getId(self):
"""The id of the comment, as a string. """The id of the comment, as a string.
""" """
return self.id return self.id
def getText(self, targetMimetype=None): def getText(self, targetMimetype=None):
"""The body text of a comment. """The body text of a comment.
""" """
transforms = getToolByName(self, 'portal_transforms') transforms = getToolByName(self, 'portal_transforms')
if targetMimetype is None: 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: if sourceMimetype is None:
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
@ -146,21 +146,21 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
text, text,
context=self, context=self,
mimetype=sourceMimetype).getData() mimetype=sourceMimetype).getData()
def Title(self): def Title(self):
"""The title of the comment. """The title of the comment.
""" """
if self.title: if self.title:
return self.title return self.title
if not self.creator: if not self.creator:
creator = translate(Message(_(u"label_anonymous", creator = translate(Message(_(u"label_anonymous",
default=u"Anonymous"))) default=u"Anonymous")))
else: else:
creator = self.creator creator = self.creator
creator = creator creator = creator
# Fetch the content object (the parent of the comment is the # Fetch the content object (the parent of the comment is the
# conversation, the parent of the conversation is the content object). # conversation, the parent of the conversation is the content object).
content = aq_base(self.__parent__.__parent__) content = aq_base(self.__parent__.__parent__)
@ -169,26 +169,26 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable,
mapping={'creator': creator, mapping={'creator': creator,
'content': safe_unicode(content.Title())})) 'content': safe_unicode(content.Title())}))
return title return title
def Creator(self): def Creator(self):
"""The name of the person who wrote the comment. """The name of the person who wrote the comment.
""" """
return self.creator return self.creator
def Type(self): def Type(self):
"""The Discussion Item content type. """The Discussion Item content type.
""" """
return self.fti_title return self.fti_title
# CMF's event handlers assume any IDynamicType has these :( # CMF's event handlers assume any IDynamicType has these :(
def opaqueItems(self): # pragma: no cover def opaqueItems(self): # pragma: no cover
return [] return []
def opaqueIds(self): # pragma: no cover def opaqueIds(self): # pragma: no cover
return [] return []
def opaqueValues(self): # pragma: no cover def opaqueValues(self): # pragma: no cover
return [] return []
CommentFactory = Factory(Comment) CommentFactory = Factory(Comment)
@ -242,24 +242,24 @@ def notify_content_object_moved(obj, event):
for comment in conversation.getComments(): for comment in conversation.getComments():
aq_base(comment).__parent__.__parent__.__parent__ = event.newParent aq_base(comment).__parent__.__parent__.__parent__ = event.newParent
catalog.reindexObject(aq_base(comment)) catalog.reindexObject(aq_base(comment))
def notify_user(obj, event): def notify_user(obj, event):
"""Tell users when a comment has been added. """Tell users when a comment has been added.
This method composes and sends emails to all users that have added a This method composes and sends emails to all users that have added a
comment to this conversation and enabled user notification. comment to this conversation and enabled user notification.
This requires the user_notification setting to be enabled in the This requires the user_notification setting to be enabled in the
discussion control panel. discussion control panel.
""" """
# Check if user notification is enabled # Check if user notification is enabled
registry = queryUtility(IRegistry) registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
if not settings.user_notification_enabled: if not settings.user_notification_enabled:
return return
# Get informations that are necessary to send an email # Get informations that are necessary to send an email
mail_host = getToolByName(obj, 'MailHost') mail_host = getToolByName(obj, 'MailHost')
portal_url = getToolByName(obj, 'portal_url') portal_url = getToolByName(obj, 'portal_url')
@ -282,16 +282,16 @@ def notify_user(obj, event):
if (obj != comment and if (obj != comment and
comment.user_notification and comment.author_email): comment.user_notification and comment.author_email):
emails.add(comment.author_email) emails.add(comment.author_email)
if not emails: if not emails:
return return
subject = translate(_(u"A comment has been posted."), subject = translate(_(u"A comment has been posted."),
context=obj.REQUEST) context=obj.REQUEST)
message = translate(Message( message = translate(Message(
MAIL_NOTIFICATION_MESSAGE, MAIL_NOTIFICATION_MESSAGE,
mapping={'title': safe_unicode(content_object.title), mapping={'title': safe_unicode(content_object.title),
'link': content_object.absolute_url() + 'link': content_object.absolute_url() +
'/view#' + obj.id, '/view#' + obj.id,
'text': obj.text}), 'text': obj.text}),
context=obj.REQUEST) context=obj.REQUEST)
@ -312,13 +312,13 @@ def notify_user(obj, event):
def notify_moderator(obj, event): def notify_moderator(obj, event):
"""Tell the moderator when a comment needs attention. """Tell the moderator when a comment needs attention.
This method sends an email to the moderator if comment moderation a new This method sends an email to the moderator if comment moderation a new
comment has been added that needs to be approved. comment has been added that needs to be approved.
The moderator_notification setting has to be enabled in the discussion The moderator_notification setting has to be enabled in the discussion
control panel. control panel.
Configure the moderator e-mail address 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, If no moderator is configured but moderator notifications are turned on,
the site admin email (from the mail control panel) will be used. the site admin email (from the mail control panel) will be used.
@ -328,25 +328,25 @@ def notify_moderator(obj, event):
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
if not settings.moderator_notification_enabled: if not settings.moderator_notification_enabled:
return return
# Get informations that are necessary to send an email # Get informations that are necessary to send an email
mail_host = getToolByName(obj, 'MailHost') mail_host = getToolByName(obj, 'MailHost')
portal_url = getToolByName(obj, 'portal_url') portal_url = getToolByName(obj, 'portal_url')
portal = portal_url.getPortalObject() portal = portal_url.getPortalObject()
sender = portal.getProperty('email_from_address') sender = portal.getProperty('email_from_address')
if settings.moderator_email: if settings.moderator_email:
mto = settings.moderator_email mto = settings.moderator_email
else: else:
mto = sender mto = sender
# Check if a sender address is available # Check if a sender address is available
if not sender: if not sender:
return return
conversation = aq_parent(obj) conversation = aq_parent(obj)
content_object = aq_parent(conversation) content_object = aq_parent(conversation)
# Compose email # Compose email
subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) subject = translate(_(u"A comment has been posted."), context=obj.REQUEST)
message = translate(Message(MAIL_NOTIFICATION_MESSAGE_MODERATOR, message = translate(Message(MAIL_NOTIFICATION_MESSAGE_MODERATOR,
@ -358,7 +358,7 @@ def notify_moderator(obj, event):
'link_delete': obj.absolute_url() + '/@@moderate-delete-comment', 'link_delete': obj.absolute_url() + '/@@moderate-delete-comment',
}), }),
context=obj.REQUEST) context=obj.REQUEST)
# Send email # Send email
try: try:
mail_host.send(message, mto, sender, subject, charset='utf-8') mail_host.send(message, mto, sender, subject, charset='utf-8')

View File

@ -17,7 +17,7 @@ id and allow traversal). Hence, traversing to obj/++conversation++/123 retrieves
the comment with id 123. the comment with id 123.
Comments ids are assigned in order, so a comment with id N was posted before Comments ids are assigned in order, so a comment with id N was posted before
a comment with id N + 1. However, it is not guaranteed that ids will be a comment with id N + 1. However, it is not guaranteed that ids will be
incremental. Ids must be positive integers - 0 or negative numbers are not incremental. Ids must be positive integers - 0 or negative numbers are not
allowed. allowed.
@ -42,13 +42,13 @@ Factories
Comments should always be created via the 'Discussion Item' IFactory utility. Comments should always be created via the 'Discussion Item' IFactory utility.
Conversations should always be obtained via the IConversation adapter (even Conversations should always be obtained via the IConversation adapter (even
the ++conversation++ namespace should use this). This makes it possible to the ++conversation++ namespace should use this). This makes it possible to
replace conversations and comments transparently. replace conversations and comments transparently.
The Comment class The Comment class
----------------- -----------------
The inheritance tree for DiscussionItem is shown below. Classes we want to The inheritance tree for DiscussionItem is shown below. Classes we want to
mix in and interface we want to implement in the Comment class are marked mix in and interface we want to implement in the Comment class are marked
with [x]. with [x].
@ -60,17 +60,17 @@ with [x].
[ ] DynamicType = [ ] IDynamicType [ ] DynamicType = [ ] IDynamicType
[ ] CMFCatalogAware = [ ] <no interface> [ ] CMFCatalogAware = [ ] <no interface>
[ ] SimpleItem = [ ] ISimpleItem [ ] SimpleItem = [ ] ISimpleItem
[ ] Item [ ] [ ] Item [ ]
[?] Base = [ ] <no interface> [?] Base = [ ] <no interface>
[ ] Resource = [ ] <no interface> [ ] Resource = [ ] <no interface>
[ ] CopySource = [ ] ICopySource [ ] CopySource = [ ] ICopySource
[ ] Tabs = [ ] <no interface> [ ] Tabs = [ ] <no interface>
[x] Traversable = [ ] ITraversable [x] Traversable = [ ] ITraversable
[ ] Element = [ ] <no interface> [ ] Element = [ ] <no interface>
[x] Owned = [ ] IOwned [x] Owned = [ ] IOwned
[ ] UndoSupport = [ ] IUndoSupport [ ] UndoSupport = [ ] IUndoSupport
[ ] Persistent [ ] [ ] Persistent [ ]
[ ] Implicit [ ] [ ] Implicit [ ]
[x] RoleManager = [ ] IRoleManager [x] RoleManager = [ ] IRoleManager
[ ] RoleManager = [ ] IPermissionMappingSupport [ ] RoleManager = [ ] IPermissionMappingSupport
[ ] DefaultDublinCoreImpl = [ ] IDublinCore [ ] DefaultDublinCoreImpl = [ ] IDublinCore
@ -86,7 +86,7 @@ Thus, we want:
- we do not want implicit acquisition - we do not want implicit acquisition
* Owned, to be able to track ownership * Owned, to be able to track ownership
* RoleManager, to support permissions and local roles * RoleManager, to support permissions and local roles
We also want to use a number of custom indexers for most of the standard We also want to use a number of custom indexers for most of the standard
metadata such as creator, effective date etc. metadata such as creator, effective date etc.
@ -121,7 +121,7 @@ In addition, we'll need a 'Moderator' role and a moderation permission,
* Moderate comment * Moderate comment
* Bypass moderation * Bypass moderation
To control whether Anonymous can post comments, we manage the 'Reply to Item' To control whether Anonymous can post comments, we manage the 'Reply to Item'
permission. To control whether moderation is required for various roles, we permission. To control whether moderation is required for various roles, we
could manage the 'Bypass moderation' permission. could manage the 'Bypass moderation' permission.
@ -136,7 +136,7 @@ These could work in a workflow like this:
+----- {auto-publish} -----+ +----- {auto-publish} -----+
| | | |
+----- {auto-moderate} ----+ +----- {auto-moderate} ----+
The 'posted' state is the initial state. 'published' is the state where the The 'posted' state is the initial state. 'published' is the state where the
comment is visible to non-reviewers. comment is visible to non-reviewers.
@ -150,22 +150,22 @@ the 'Bypass moderation' permission.
The 'auto-moderate' transition would be another automatic transition protected The 'auto-moderate' transition would be another automatic transition protected
by an expression (e.g. calling a view) that returns True if the user is on by an expression (e.g. calling a view) that returns True if the user is on
an auto-moderation 'white-list', e.g. by email address or username. an auto-moderation 'white-list', e.g. by email address or username.
Forms and UI Forms and UI
------------ ------------
The basic commenting display/reply form is placed in a viewlet. The basic commenting display/reply form is placed in a viewlet.
The reply form is dynamically created right under the comment when the user hits The reply form is dynamically created right under the comment when the user hits
the reply button. To do so, we copy the standard comment form with a jQuery the reply button. To do so, we copy the standard comment form with a jQuery
function. This function sets the form's hidden in_reply_to field to the id of function. This function sets the form's hidden in_reply_to field to the id of
the comment the user wants to reply to. This also makes it possible to use the comment the user wants to reply to. This also makes it possible to use
z3c.form validation for the reply forms, because we can uniquely identify the z3c.form validation for the reply forms, because we can uniquely identify the
reply form request and return the reply form with validation errors. reply form request and return the reply form with validation errors.
Since we rely on JavaScript for the reply form creation, the reply button is Since we rely on JavaScript for the reply form creation, the reply button is
removed for non JavaScript enabled browsers. removed for non JavaScript enabled browsers.
The comment form uses z3c.form and plone.z3cform's ExtensibleForm support. This The comment form uses z3c.form and plone.z3cform's ExtensibleForm support. This
makes it possible to plug in additional fields declaratively, e.g. to include makes it possible to plug in additional fields declaratively, e.g. to include
SPAM protection. SPAM protection.

View File

@ -112,7 +112,7 @@ class IDiscussionSettings(Interface):
required=False, required=False,
default=False, default=False,
) )
moderator_email = schema.ASCIILine( moderator_email = schema.ASCIILine(
title = _(u'label_moderator_email', default=u'Moderator Email Address'), title = _(u'label_moderator_email', default=u'Moderator Email Address'),
description = _(u'help_moderator_email', description = _(u'help_moderator_email',

View File

@ -1,7 +1,7 @@
<configure <configure
xmlns="http://namespaces.zope.org/zope" xmlns="http://namespaces.zope.org/zope"
xmlns:genericsetup="http://namespaces.zope.org/genericsetup"> xmlns:genericsetup="http://namespaces.zope.org/genericsetup">
<genericsetup:upgradeStep <genericsetup:upgradeStep
source="*" source="*"
destination="100" destination="100"
@ -10,5 +10,5 @@
profile="plone.app.discussion:default" profile="plone.app.discussion:default"
handler=".upgrades.update_registry" handler=".upgrades.update_registry"
/> />
</configure> </configure>