check in some skeletal code + notes, nothing concrete or working yet

svn path=/plone.app.discussion/trunk/; revision=26892
This commit is contained in:
Martin Aspeli
2009-05-11 16:52:16 +00:00
commit 6381b14763
28 changed files with 757 additions and 0 deletions
+169
View File
@@ -0,0 +1,169 @@
=================================
plone.app.discussion design notes
=================================
This document contains design notes for plone.app.discussion.
The Comment class
-----------------
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
with [x].
[ ] DiscussionItem
[ ] Document
[ ] PortalContent = [ ] IContentish
[ ] DynamicType = [ ] IDynamicType
[ ] CMFCatalogAware = [ ] <no interface>
[ ] SimpleItem = [ ] ISimpleItem
[ ] Item [ ]
[?] Base = [ ] <no interface>
[ ] Resource = [ ] <no interface>
[ ] CopySource = [ ] ICopySource
[ ] Tabs = [ ] <no interface>
[x] Traversable = [ ] ITraversable
[ ] Element = [ ] <no interface>
[x] Owned = [ ] IOwned
[ ] UndoSupport = [ ] IUndoSupport
[ ] Persistent [ ]
[ ] Implicit [ ]
[x] RoleManager = [ ] IRoleManager
[ ] RoleManager = [ ] IPermissionMappingSupport
[ ] DefaultDublinCoreImpl = [ ] IDublinCore
[ ] ICatalogableDublinCore
[ ] IMutableDublinCore
[ ] PropertyManager = [ ] IPropertyManager
Thus, we want:
* Traversable, to get absolute_url() and friends
- this requires a good acquisition chain at all times
* Acquisition.Explicit, to support acquisition
- we do not want implicit acquisition
* Owned, to be able to track ownership
* RoleManager, to support permissions and local roles
We also want to use a number of custom indexers for most of the standard
metadata such as creator, effective date etc.
Finally, we'll need event handlers to perform the actual indexing.
The Discussable class
---------------------
To obtain comments for a content item, we adapt the content to IDiscussable.
This has a 'replies' BTree.
To support __parent__ pointer integrity and stable URLs, we will implement
IDiscussable as a persistent object stored in an annotation. Comments directly
in reply to the content item have a __parent__ pointing to this, which in turn
has a __parent__ pointing at the content item.
The IDiscussable interface also maintains information about the total number
of comments and the unique set of commenters' usernames. These are indexed in
the catalog, allowing queries like "recently commented-upon content items",
"my comments", or "all comments by user X". These values need to be stored and
maintained by event handlers, not calculated on the fly.
See collective.discussionplus for inspiration.
Traversal and acquisition
--------------------------
A comment may have a URL such as:
http://localhost:8080/site/content/++comments++/1/2/3
For this traversal to work, we have:
- a namespace traversal adapter for ++comments++ that looks up an
IDiscussable adapter on the context and returns this
- an IPublishTraverse adapter for IHasReplies (inherited by IDiscussable
and IComment), which looks up values in the 'replies' dictionary and
acquisition-wraps them.
- the IDiscussable adapter needs to have an id of ++comment++
XXX: unrestrictedTraverse() does not take IPublishTraverse adapters into
account. This may mean we need to implement __getitem__ on comments instead
of/in addition to using a custom IPublishTraverse for IHasReplies.
Discussion settings
-------------------
Discussion can be enabled per-type and per-instance, via values in the FTI
(allow_discussion) and on the object. These will remain unchanged. The
IDiscussable object's 'enabled' property should consult these.
Global settings should be managed using plone.registry. A control panel
can be generated from this as well, using the helper class in
plone.app.registry.
Note that some settings, notably those to do with permissions and workflow,
will need to be wired up as custom form fields with custom data mangers
or similar.
Workflow and permissions
------------------------
Where possible, we should use existing permissions:
* View
* Reply to Item
* Modify Portal Content
* Request Review
In addition, we'll need a 'Moderator' role and a moderation permission,
* Moderate comment
* Bypass moderation
To control whether Anonymous can post comments, we manage the 'Reply to Item'
permission. To control whether moderation is required for various roles, we
could manage the 'Bypass moderation' permission.
These could work in a workflow like this:
* --> [posted] -- {publish} --> [published]--> *
| ^
| |
+----- {auto-publish} -----+
| |
+----- {auto-moderate} ----+
The 'posted' state is the initial state. 'published' is the state where the
comment is visible to non-reviewers.
The 'publish' transition would be protected by the 'Moderate comment'
permission. We could have states and transition for 'rejected', etc, but it
is probably just as good to delete comments that are rejected.
The 'auto-publish' transition would be an automatic transition protected by
the 'Bypass moderation' permission.
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
an auto-moderation 'white-list', e.g. by email address or username.
Forms and UI
------------
The basic commenting display/reply form should be placed in a viewlet.
Ideally, the reply form should be inline, perhaps revealed with JavaScript
if enabled. This allows full contextualisation of replies. The current
solution, with a separate form that shows some context, is brittle and
over-complicated.
If we support quoting of comments in replies, we can load the text to quote
using JavaScript as well.
As a fall-back for non-JavaScript enabled browsers, it is probably OK not to
support quoting and/or viewing of context, e.g. the user is taken to a standalone
'comment reply' form.
All actual forms should be handled using z3c.form and plone.z3cform's
ExtensibleForm support. This makes it possible to plug in additional fields
declaratively, e.g. to include spam protection.
+77
View File
@@ -0,0 +1,77 @@
=============================================
plone.app.discussion architectural principles
=============================================
This document outlines architectural principles used in the design of
plone.app.discussion.
Discussion items have a portal_type
This makes it easier to search for them and manage them using existing
CMF and Plone UI constructs.
Discussion items are cataloged
It should be possible to search for discussion items like any other
type of content.
Discussion items are subject to workflow and permission
Moderation, anonymous commenting, and auto approve/reject should be
handled using workflow states, automatic and manual transitions, and
permissions.
Discussion items are light weight objects
All discussion item objects should be as light weight as possible.
Ideally, a discussion item should be as lightweight as a catalog brain.
This may mean that we forego convenience base classes and re-implement
certain interfaces. Comments should not provide the full set of dublin
core metadata, though custom indexers can be used to provide values for
standard catalog indexes.
Optimise for retrival speed
HTML filtering and other processing should happen on save, not on render,
to make rendering quick.
Settings are stored using plone.registry
Any global setting should be stored in plone.registry records
Forms are constructed using extensible z3c.form forms
This allows plugins (such as spam protection algorithms) to provide
additional validation
Discussion items are stored in a BTree container
This allows faster lookup and manipulation
Discussion items are accessed using a dict-like interface
This makes iteration and manipulation more natural. Even if comments are
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
(in order).
Discussion items are retrieved in reverse creation date order
Discussion items do not need to support explicit ordering. They should
always be retrieved in reverse creation date order (most recent for).
They can be stored with keys so that this is always true.
Discussion items do not need readable ids
Ids can be based on the creation date.
Discussion items send events
The usual zope.lifecycleevent and zope.app.container events should be
fired when discussion items are added, removed, or modified.
Outstanding questions
---------------------
* Should comments use the 'talkback' URL structure currently used in CMF,
or a ++comments++ namespace?
* Should discussion items be stored in one container, maintaining parent
pointers as references or attributes, or in a nested structure?
- it may be possible to use the navtree algorithm to turn a search of
comments into a nested structure
* How can we ensure that discussion items are not removed from the catalog
when the user initiates a "clear and rebuild"?
* Can we find a way to avoid having to explicitly migrate all existing
comments?
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,7 @@
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="plone.app.discussion">
</configure>
@@ -0,0 +1,5 @@
"""Implement an IPublishTraverse adapter for discussion items that allows
traversal into the 'replies' dictionary, as well as the ++comments++ traversal
namespace.
"""
+6
View File
@@ -0,0 +1,6 @@
"""Catalog indexers, using plone.indexer. These will populate standard catalog
indexes with values based on the IComment interface.
Also provide event handlers to actually catalog the comments.
"""
+74
View File
@@ -0,0 +1,74 @@
"""Discussion items and replies
"""
from zope.interface import implements, alsoProvides
from BTrees.OOBTree import OOBTree
from Acquisition import Explicit
from OFS.Traversable import Traversable
from AccessControl.Role import RoleManager
from AccessControl.Owned import Owned
from plone.app.discussion.interfaces import IReplies, IComment
def Replies():
"""Create a new replies object. Acts like a constructor, but actually
returns a BTree marked with an interface. We do this because subclassing
an OOBTree does not work properly.
"""
replies = OOBTree()
alsoProvides(replies, IReplies)
return replies
class Comment(Explicit, Traversable, RoleManager, Owned):
"""A comment.
This object attempts to be as lightweight as possible. We implement a
number of standard methods instead of subclassing, to have total control
over what goes into the object.
"""
implements(IComment)
meta_type = portal_type = 'Discussion Item'
__parent__ = None
__name__ = None
ancestor = None
title = u""
mime_type = "text/plain"
text = u""
creator = None
creation_date = None
modification_date = None
author_username = None
author_name = None
author_email = None
replies = None
def __init__(self, id, ancestor, parent, **kw):
self.__name__ = id
self.__parent__ = parent
self.ancestor = ancestor
for k, v in kw:
setattr(self, k, v)
replies = Replies()
# convenience functions
@property
def id(self):
return self.__name__
def getId(self):
return self.__name__
+6
View File
@@ -0,0 +1,6 @@
<configure
xmlns="http://namespaces.zope.org/zope"
i18n_domain="plone.app.discussion">
</configure>
+3
View File
@@ -0,0 +1,3 @@
"""Default implementation of the IDiscussable adapter.
"""
+68
View File
@@ -0,0 +1,68 @@
from zope.interface import Interface
from zope.interface.common.mapping import IIterableMapping, IWriteMapping
from zope import schema
from zope.i18nmessageid import MessageFactory
_ = MessageFactory('plone.app.discussion')
class IReplies(IIterableMapping, IWriteMapping):
"""A set of related comments
This acts as a mapping (dict) with string keys and values being other
discussion items in reply to this discussion item.
"""
class IHasReplies(Interface):
"""Common interface for objects that have replies.
"""
replies = schema.Object(title=_(u"Replies"), schema=IReplies)
class IComment(IHasReplies):
"""A comment
"""
portal_type = schema.ASCIILine(title=_(u"Portal type"), default="Discussion Item")
__parent__ = schema.Object(title=_(u"In reply to"), description=_(u"Another comment or a content item"), schema=Interface)
__name__ = schema.TextLine(title=_(u"Name"))
ancestor = schema.Object(title=_(u"The original content object the comment is for"), schema=Interface)
title = schema.TextLine(title=_(u"Subject"))
mime_type = schema.ASCIILine(title=_(u"MIME type"), default="text/plain")
text = schema.Text(title=_(u"Comment text"))
creator = schema.TextLine(title=_(u"Author name (for display)"))
creation_date = schema.Date(title=_(u"Creation date"))
modification_date = schema.Date(title=_(u"Modification date"))
# for logged in comments - set to None for anonymous
author_username = schema.TextLine(title=_(u"Author username"), required=False)
# for anonymous comments only, set to None for logged in comments
author_name = schema.TextLine(title=_(u"Author name"), required=False)
author_email = schema.TextLine(title=_(u"Author email address"), required=False)
class IDiscussable(IHasReplies):
"""Adapt a content item to this interface to determine whether discussions
are currently enabled, and obtain a list of comments.
"""
enabled = schema.Bool(title=_(u"Is commenting enabled?"))
total_comments = schema.Int(title=_(u"Total number of comments on this item"), min=0, readonly=True)
last_comment_date = schema.Date(title=_(u"Date of the most recent comment"), readonly=True)
commentators = schema.Set(title=_(u"The set of unique commentators (usernames)"), readonly=True)
class IDiscussionSettings(Interface):
"""Global discussion settings. This describes records stored in the
configuration registry and obtainable via plone.registry.
"""
globally_enabled = schema.Bool(title=_(u"Globally enabled"),
description=_(u"Use this setting to enable or disable comments globally"),
default=True)
@@ -0,0 +1,3 @@
<metadata>
<version>1.0a1</version>
</metadata>
+1
View File
@@ -0,0 +1 @@
1.0a1