diff --git a/buildout.cfg b/buildout.cfg index 0e386ac..1992b65 100644 --- a/buildout.cfg +++ b/buildout.cfg @@ -1,7 +1,7 @@ [buildout] extends = 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 parts += instance diff --git a/dev.cfg b/dev.cfg index 19336af..c90b587 100644 --- a/dev.cfg +++ b/dev.cfg @@ -2,7 +2,7 @@ extends = buildout.cfg extensions = buildout.eggtractor mr.developer -tractor-src-directory = +tractor-src-directory = . src auto-checkout = @@ -15,7 +15,7 @@ auto-checkout = parts += omelette releaser - pocompile + pocompile zopepy sphinxbuilder sphinxupload diff --git a/plone/app/discussion/architecture.txt b/plone/app/discussion/architecture.txt index c99a265..dff5036 100644 --- a/plone/app/discussion/architecture.txt +++ b/plone/app/discussion/architecture.txt @@ -19,19 +19,19 @@ plone.app.discussion. permissions. **Discussion items are light weight objects** - Discussion item objects are 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 + Discussion item objects are 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** + **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. 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. 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.container events are fired when + The usual zope.lifecycleevent and zope.container events are fired when discussion items are added, removed, or modified. diff --git a/plone/app/discussion/comment.py b/plone/app/discussion/comment.py index 616d79d..a4f7653 100644 --- a/plone/app/discussion/comment.py +++ b/plone/app/discussion/comment.py @@ -71,67 +71,67 @@ logger = logging.getLogger("plone.app.discussion") class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable, RoleManager, Owned, Implicit, Persistent): """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' # This needs to be kept in sync with types/Discussion_Item.xml title fti_title = 'Comment' - + __parent__ = None - - comment_id = None # long - in_reply_to = None # long - + + comment_id = None # long + in_reply_to = None # long + title = u"" - + mime_type = None text = u"" - + creator = None creation_date = None modification_date = None - + author_username = None - + author_name = None author_email = None - + user_notification = None - + # Note: we want to use zope.component.createObject() to instantiate # comments as far as possible. comment_id and __parent__ are set via # IConversation.addComment(). - + def __init__(self): self.creation_date = self.modification_date = datetime.utcnow() - + @property def __name__(self): return self.comment_id and unicode(self.comment_id) or None - + @property def id(self): return self.comment_id and str(self.comment_id) or None - + def getId(self): """The id of the comment, as a string. """ return self.id - + def getText(self, targetMimetype=None): """The body text of a comment. """ transforms = getToolByName(self, 'portal_transforms') - + if targetMimetype is None: targetMimetype = 'text/x-html-safe' - + sourceMimetype = getattr(self, 'mime_type', None) if sourceMimetype is None: registry = queryUtility(IRegistry) @@ -146,21 +146,21 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable, text, context=self, mimetype=sourceMimetype).getData() - + def Title(self): """The title of the comment. """ - + if self.title: return self.title - + if not self.creator: creator = translate(Message(_(u"label_anonymous", default=u"Anonymous"))) else: creator = self.creator creator = creator - + # Fetch the content object (the parent of the comment is the # conversation, the parent of the conversation is the content object). content = aq_base(self.__parent__.__parent__) @@ -169,26 +169,26 @@ class Comment(CatalogAware, WorkflowAware, DynamicType, Traversable, mapping={'creator': creator, 'content': safe_unicode(content.Title())})) return title - + def Creator(self): """The name of the person who wrote the comment. """ return self.creator - + def Type(self): """The Discussion Item content type. """ return self.fti_title - + # CMF's event handlers assume any IDynamicType has these :( - - def opaqueItems(self): # pragma: no cover + + def opaqueItems(self): # pragma: no cover return [] - - def opaqueIds(self): # pragma: no cover + + def opaqueIds(self): # pragma: no cover return [] - - def opaqueValues(self): # pragma: no cover + + def opaqueValues(self): # pragma: no cover return [] CommentFactory = Factory(Comment) @@ -242,24 +242,24 @@ def notify_content_object_moved(obj, event): for comment in conversation.getComments(): aq_base(comment).__parent__.__parent__.__parent__ = event.newParent catalog.reindexObject(aq_base(comment)) - + def notify_user(obj, event): """Tell users when a comment has been added. - + This method composes and sends emails to all users that have added a comment to this conversation and enabled user notification. - + This requires the user_notification setting to be enabled in the discussion control panel. """ - + # Check if user notification is enabled registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) if not settings.user_notification_enabled: return - + # Get informations that are necessary to send an email mail_host = getToolByName(obj, 'MailHost') portal_url = getToolByName(obj, 'portal_url') @@ -282,16 +282,16 @@ def notify_user(obj, event): if (obj != comment and comment.user_notification and comment.author_email): emails.add(comment.author_email) - + if not emails: return - + subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) message = translate(Message( MAIL_NOTIFICATION_MESSAGE, mapping={'title': safe_unicode(content_object.title), - 'link': content_object.absolute_url() + + 'link': content_object.absolute_url() + '/view#' + obj.id, 'text': obj.text}), context=obj.REQUEST) @@ -312,13 +312,13 @@ def notify_user(obj, event): def notify_moderator(obj, event): """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. - + The moderator_notification setting has to be enabled 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, 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) if not settings.moderator_notification_enabled: return - + # Get informations that are necessary to send an email mail_host = getToolByName(obj, 'MailHost') portal_url = getToolByName(obj, 'portal_url') portal = portal_url.getPortalObject() sender = portal.getProperty('email_from_address') - + if settings.moderator_email: mto = settings.moderator_email else: mto = sender - + # Check if a sender address is available if not sender: return - + conversation = aq_parent(obj) content_object = aq_parent(conversation) - + # Compose email subject = translate(_(u"A comment has been posted."), context=obj.REQUEST) message = translate(Message(MAIL_NOTIFICATION_MESSAGE_MODERATOR, @@ -358,7 +358,7 @@ def notify_moderator(obj, event): 'link_delete': obj.absolute_url() + '/@@moderate-delete-comment', }), context=obj.REQUEST) - + # Send email try: mail_host.send(message, mto, sender, subject, charset='utf-8') diff --git a/plone/app/discussion/design.txt b/plone/app/discussion/design.txt index 2300dec..4e76c1d 100644 --- a/plone/app/discussion/design.txt +++ b/plone/app/discussion/design.txt @@ -17,7 +17,7 @@ id and allow traversal). Hence, traversing to obj/++conversation++/123 retrieves the comment with id 123. 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 allowed. @@ -42,13 +42,13 @@ Factories Comments should always be created via the 'Discussion Item' IFactory utility. 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. 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 with [x]. @@ -60,17 +60,17 @@ with [x]. [ ] DynamicType = [ ] IDynamicType [ ] CMFCatalogAware = [ ] [ ] SimpleItem = [ ] ISimpleItem - [ ] Item [ ] + [ ] Item [ ] [?] Base = [ ] [ ] Resource = [ ] - [ ] CopySource = [ ] ICopySource + [ ] CopySource = [ ] ICopySource [ ] Tabs = [ ] [x] Traversable = [ ] ITraversable [ ] Element = [ ] [x] Owned = [ ] IOwned [ ] UndoSupport = [ ] IUndoSupport - [ ] Persistent [ ] - [ ] Implicit [ ] + [ ] Persistent [ ] + [ ] Implicit [ ] [x] RoleManager = [ ] IRoleManager [ ] RoleManager = [ ] IPermissionMappingSupport [ ] DefaultDublinCoreImpl = [ ] IDublinCore @@ -86,7 +86,7 @@ Thus, we want: - 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. @@ -121,7 +121,7 @@ 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. @@ -136,7 +136,7 @@ These could work in a workflow like this: +----- {auto-publish} -----+ | | +----- {auto-moderate} ----+ - + The 'posted' state is the initial state. 'published' is the state where the comment is visible to non-reviewers. @@ -150,22 +150,22 @@ 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 is placed in a viewlet. 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 -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 +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 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 removed for non JavaScript enabled browsers. -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 +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 SPAM protection. diff --git a/plone/app/discussion/interfaces.py b/plone/app/discussion/interfaces.py index 1ed57f2..adcb38d 100644 --- a/plone/app/discussion/interfaces.py +++ b/plone/app/discussion/interfaces.py @@ -112,7 +112,7 @@ class IDiscussionSettings(Interface): required=False, default=False, ) - + moderator_email = schema.ASCIILine( title = _(u'label_moderator_email', default=u'Moderator Email Address'), description = _(u'help_moderator_email', diff --git a/plone/app/discussion/upgrades.zcml b/plone/app/discussion/upgrades.zcml index 5eec4b2..b1854d0 100644 --- a/plone/app/discussion/upgrades.zcml +++ b/plone/app/discussion/upgrades.zcml @@ -1,7 +1,7 @@ - + - +