diff --git a/plone/app/discussion/browser/traversal.py b/plone/app/discussion/browser/traversal.py index f06728e..ae3a38c 100644 --- a/plone/app/discussion/browser/traversal.py +++ b/plone/app/discussion/browser/traversal.py @@ -1,5 +1,5 @@ -"""Implement an IPublishTraverse adapter for discussion items that allows -traversal into the 'replies' dictionary, as well as the ++comments++ traversal -namespace. +"""Implement the ++comments++ traversal namespace. This should return the +IDiscussion container for the context, from which traversal will continue +into an actual comment object. """ diff --git a/plone/app/discussion/comment.py b/plone/app/discussion/comment.py index 0c01de8..9299797 100644 --- a/plone/app/discussion/comment.py +++ b/plone/app/discussion/comment.py @@ -1,26 +1,15 @@ -"""Discussion items and replies +"""The default comment class and factory. """ -from zope.interface import implements, alsoProvides - -from BTrees.OOBTree import OOBTree +from zope.interface import implements +from zope.component.factory import Factory 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 +from plone.app.discussion.interfaces import IComment class Comment(Explicit, Traversable, RoleManager, Owned): """A comment. @@ -36,11 +25,10 @@ class Comment(Explicit, Traversable, RoleManager, Owned): __parent__ = None __name__ = None - ancestor = None title = u"" - mime_type = "text/plain" + mime_type = "text/plain" text = u"" creator = None @@ -52,23 +40,32 @@ class Comment(Explicit, Traversable, RoleManager, Owned): author_name = None author_email = None - replies = None - - def __init__(self, id, ancestor, parent, **kw): - self.__name__ = id - self.__parent__ = parent - self.ancestor = ancestor + def __init__(self, id=None, conversation=None, **kw): + self.__name__ = unicode(id) + self.__parent__ = conversation for k, v in kw: setattr(self, k, v) - - replies = Replies() # convenience functions @property def id(self): - return self.__name__ + return str(self.__name__) def getId(self): - return self.__name__ + """The id of the comment, as a string + """ + return self.id + + def Title(self): + """The title of the comment + """ + return self.title + + def Creator(self): + """The name of the person who wrote the comment + """ + return self.creator + +CommentFactory = Factory(Comment) \ No newline at end of file diff --git a/plone/app/discussion/configure.zcml b/plone/app/discussion/configure.zcml index 5d205dd..eaa3de7 100644 --- a/plone/app/discussion/configure.zcml +++ b/plone/app/discussion/configure.zcml @@ -1,6 +1,12 @@ - + + + + + + + + + diff --git a/plone/app/discussion/conversation.py b/plone/app/discussion/conversation.py new file mode 100644 index 0000000..a5051b8 --- /dev/null +++ b/plone/app/discussion/conversation.py @@ -0,0 +1,95 @@ +"""The conversation and replies adapters + +The conversation is responsible for storing all comments. It provides a +dict-like API for accessing comments, where keys are integers and values +are IComment objects. It also provides features for finding comments quickly. + +The two IReplies adapters - one for the IConversation and one for IComment - +manipulate the same data structures, but provide an API for finding and +manipulating the comments directly in reply to a particular comment or at the +top level of the conversation. +""" + +from zope.interface import implements +from zope.component import adapts + +from BTrees.OIBTree import OIBTree +from BTrees.IOBTree import IOBTree +from BTrees.IIBTree import IIBTree, IISet + +from Acquisition import Explicit + +from plone.app.discussion.interfaces import IConversation, IComment, IReplies + +class Conversation(Explicit): + """A conversation is a container for all comments on a content object. + """ + + implements(IConversation) + + def __init__(self, id="++comments++"): + self.id = id + + # username -> count of comments; key is removed when count reaches 0 + self._commentators = OIBTree() + self._last_comment_date = None + + # id -> comment - find comment by id + self._comments = IOBTree() + + # # id -> IISet (children) - find all children for a given comment. 0 signifies root. + self._children = IOBTree() + + # id -> id (parent) - find the parent for a given comment. 0 signifies root + self._parents = IIBTree() + + def getId(self): + """ + """ + return self.id + + @property + def enabled(self): + # TODO + return True + + @property + def total_comments(self): + # TODO + return 0 + + @property + def last_comment_date(self): + # TODO + return None + + @property + def commentators(self): + # TODO: + return set() + + def getComments(start=0, size=None): + # TODO + pass + + def getThreads(start=0, size=None, root=None, depth=None): + # TODO + pass + + # Dict API + + # TODO: Update internal data structures when items added or removed + +class ConversationReplies(object): + """ + """ + + implements(IReplies) + adapts(Conversation) + +class CommentReplies(object): + """ + """ + + implements(IReplies) + adapts(IComment) \ No newline at end of file diff --git a/plone/app/discussion/discussable.py b/plone/app/discussion/discussable.py deleted file mode 100644 index e823855..0000000 --- a/plone/app/discussion/discussable.py +++ /dev/null @@ -1,3 +0,0 @@ -"""Default implementation of the IDiscussable adapter. -""" - diff --git a/plone/app/discussion/interfaces.py b/plone/app/discussion/interfaces.py index 7050729..315adf7 100644 --- a/plone/app/discussion/interfaces.py +++ b/plone/app/discussion/interfaces.py @@ -6,57 +6,6 @@ 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. @@ -65,4 +14,100 @@ class IDiscussionSettings(Interface): globally_enabled = schema.Bool(title=_(u"Globally enabled"), description=_(u"Use this setting to enable or disable comments globally"), default=True) + + index_comments = schema.Bool(title=_(u"Index comments"), + description=_(u"Enable this option to ensure that comments are searchable. " + "Turning this off may improve performance for sites with large " + "volumes of comments that do not wish to make them searcahble using " + "the standard search tools."), + default=True) +class IConversation(IIterableMapping, IWriteMapping): + """A conversation about a content object. + + This is a persistent object in its own right and manages all comments. + + The dict interface allows access to all comments. They are stored by + integer key, in the order they were added. + + To get replies at the top level, adapt the conversation to IReplies. + + The conversation can be traversed to via the ++comments++ namespace. + For example, path/to/object/++comments++/123 retrieves comment 123. + + The __parent__ of the conversation (and the acquisition parent during + traversal) is the content object. The conversation is the __parent__ + (and acquisition parent) for all comments, regardless of threading. + """ + + 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) + + def getComments(start=0, size=None): + """Return a batch of comment objects for rendering. The 'start' + parameter is the id of the comment from which to start the batch. + The 'size' parameter is the number of comments to return in the + batch. + + The comments are returned in creation date order, in the exact batche + size specified. + """ + + def getThreads(start=0, size=None, root=None, depth=None): + """Return a batch of comment objects for rendering. The 'start' + parameter is the id of the comment from which to start the batch. + The 'size' parameter is the number of comments to return in the + batch. 'root', if given, is the id of the comment to which reply + threads will be found. If not given, all threads are returned. + If 'depth' is given, it can be used to limit the depth of threads + returned. For example, depth=1 will return only direct replies. + + Comments are returned as a recursive list of '(comment, children)', + where 'children' is a similar list of (comment, children), or an empty + list of a comment has no direct replies. + + The returned number of comments may be bigger than the batch size, + in order to give enough context to show the full lineage of the + starting comment. + """ + +class IReplies(IIterableMapping, IWriteMapping): + """A set of related comments in reply to a given content object or + another comment. + + Adapt a conversation or another comment to this interface to obtain the + direct replies. + """ + +class IComment(Interface): + """A comment. + + Comments are indexed in the catalog and subject to workflow and security. + """ + + portal_type = schema.ASCIILine(title=_(u"Portal type"), default="Discussion Item") + + __parent__ = schema.Object(title=_(u"Conversation"), schema=Interface) + __name__ = schema.TextLine(title=_(u"Name")) + + comment_id = schema.Int(title=_(u"A comment id unique to this conversation")) + in_reply_to = schema.Int(title=_(u"Id of comment this comment is in reply to"), required=False) + + 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) diff --git a/plone/app/discussion/permissions.zcml b/plone/app/discussion/permissions.zcml new file mode 100644 index 0000000..bfd6c8f --- /dev/null +++ b/plone/app/discussion/permissions.zcml @@ -0,0 +1,9 @@ + + + + + + +