Created 'features' module to handle shared features outside of pure content management
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/checker/__init__.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,47 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.checker.interfaces import VALUE_OK
+
+# import packages
+from pyams_utils.adapter import ContextAdapter
+from pyams_utils.request import check_request
+
+
+class BaseContentChecker(ContextAdapter):
+ """Base content checker"""
+
+ label = None
+ weight = 1
+ sep = '<br />'
+
+ def get_check_output(self, request=None):
+ if request is None:
+ request = check_request()
+ translate = request.localizer.translate
+ output = self.inner_check(request)
+ if output:
+ output = [self.sep.join(output)]
+ output.insert(0, '<div class="padding-left-20">')
+ output.append('</div>')
+ else:
+ output.append(translate(VALUE_OK) + '<br />')
+ return '\n'.join(output)
+
+ def inner_check(self, request):
+ return []
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/checker/interfaces.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,39 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface, Attribute
+
+from pyams_content import _
+
+
+VALUE_OK = '<span class="text-success">OK</span>'
+MISSING_VALUE = _(' - {field}: <span class="text-danger">no value</span>')
+MISSING_LANG_VALUE = _(' - {field} ({lang}): <span class="text-danger">no value</span>')
+ERROR_VALUE = _(' - {field}: <span class="text-danger">{message}</span>')
+
+
+class IContentChecker(Interface):
+ """Content checker interface"""
+
+ label = Attribute("Adapter label")
+ weight = Attribute("Adapter weight")
+
+ def get_check_output(self, request=None):
+ """Gte context check as HTML output"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/checker/zmi/__init__.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,80 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.checker.interfaces import IContentChecker
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION
+from pyams_form.interfaces.form import IWidgetsSuffixViewletsManager
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.interfaces.viewlet import IContextActions
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem
+from pyams_viewlet.viewlet import viewlet_config, Viewlet
+from pyams_zmi.form import AdminDialogDisplayForm
+from z3c.form import field
+from zope.interface import Interface
+
+from pyams_content import _
+
+
+@viewlet_config(name='check-content.menu', context=Interface, layer=IAdminLayer,
+ view=IInnerPage, manager=IContextActions, permission=MANAGE_CONTENT_PERMISSION, weight=20)
+class ContentCheckerMenu(ToolbarMenuItem):
+ """Content checker menu item"""
+
+ label = _("Check content...")
+ label_css_class = 'fa fa-fw fa-check-square-o'
+
+ url = 'check-content.html'
+ modal_target = True
+
+
+@pagelet_config(name='check-content.html', context=Interface, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class ContentCheckerForm(AdminDialogDisplayForm):
+ """Content checker display form"""
+
+ legend = _("Content check")
+ icon_css_class = 'fa fa-fw fa-check-square-o'
+
+ fields = field.Fields(Interface)
+
+
+@viewlet_config(name='check-content', context=Interface, layer=IAdminLayer, view=ContentCheckerForm,
+ manager=IWidgetsSuffixViewletsManager, weight=1)
+class ContentCheckerWidgetsSuffix(Viewlet):
+ """Content checker widgets suffix"""
+
+ def render(self):
+ output = []
+ registry = self.request.registry
+ translate = self.request.localizer.translate
+ for name, checker in sorted(registry.getAdapters((self.context, ), IContentChecker),
+ key=lambda x: x[1].weight):
+ header = '<strong>{0}</strong>'.format(translate(checker.label))
+ checker_output = checker.get_check_output()
+ if checker_output:
+ output.append(translate(_('{0}:')).format(header))
+ output.append(checker_output)
+ if not output:
+ translate = self.request.localizer.translate
+ output.append(translate(_("No checker available. This content is clean!")))
+ return '\n'.join(output)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/preview/__init__.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/preview/interfaces.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,28 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface
+
+
+class IPreviewTarget(Interface):
+ """Preview target marker interface
+
+ This interface is used to mark contents which can handle preview.
+ """
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/preview/zmi/__init__.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,60 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.features.preview.interfaces import IPreviewTarget
+from pyams_content.features.preview.zmi.interfaces import IPreviewForm
+from pyams_skin.interfaces.viewlet import IToolbarViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from pyams_utils.interfaces import VIEW_SYSTEM_PERMISSION
+from pyams_zmi.layer import IAdminLayer
+
+# import packages
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.toolbar import ToolbarAction
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogDisplayForm
+from z3c.form import field
+from zope.interface import implementer, Interface
+
+from pyams_content import _
+
+
+@viewlet_config(name='preview.action', context=IPreviewTarget, layer=IAdminLayer, view=Interface,
+ manager=IToolbarViewletManager, permisison=VIEW_SYSTEM_PERMISSION, weight=50)
+class PreviewAction(ToolbarAction):
+ """Content preview action"""
+
+ label = _("Preview")
+
+ group_css_class = 'btn-group margin-right-10'
+ label_css_class = 'fa fa-newspaper-o'
+ css_class = 'btn btn-xs btn-default'
+
+ url = 'preview.html'
+ modal_target = True
+
+
+@pagelet_config(name='preview.html', context=IPreviewTarget, layer=IPyAMSLayer, permission=VIEW_SYSTEM_PERMISSION)
+@implementer(IPreviewForm)
+class PreviewForm(AdminDialogDisplayForm):
+ """Content preview form"""
+
+ legend = _("Content preview")
+ dialog_class = 'modal-max'
+
+ fields = field.Fields(Interface)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/preview/zmi/interfaces.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,25 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from zope.interface import Interface
+
+
+class IPreviewForm(Interface):
+ """Preview form marker interface"""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/review/__init__.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,233 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import os
+from datetime import datetime
+from uuid import uuid4
+
+# import interfaces
+from pyams_content.interfaces import READER_ROLE
+from pyams_content.features.review.interfaces import IReviewManager, IReviewComment, IReviewComments, \
+ REVIEW_COMMENTS_ANNOTATION_KEY, CommentAddedEvent, ICommentAddedEvent, IReviewTarget
+from pyams_content.shared.common.interfaces import IWfSharedContentRoles
+from pyams_i18n.interfaces import II18n
+from pyams_mail.interfaces import IPrincipalMailInfo
+from pyams_security.interfaces import ISecurityManager, IProtectedObject
+from pyams_security.interfaces.notification import INotificationSettings
+from pyramid_chameleon.interfaces import IChameleonTranslate
+from pyramid_mailer.interfaces import IMailer
+from zope.annotation.interfaces import IAnnotations
+from zope.location.interfaces import ISublocations
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from persistent import Persistent
+from pyams_mail.message import HTMLMessage
+from pyams_security.principal import MissingPrincipal
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.container import BTreeOrderedContainer
+from pyams_utils.registry import query_utility
+from pyams_utils.request import check_request, query_request
+from pyams_utils.url import absolute_url
+from pyramid.events import subscriber
+from pyramid.threadlocal import get_current_registry
+from pyramid_chameleon.zpt import PageTemplateFile
+from zope.container.contained import Contained
+from zope.interface import implementer
+from zope.location import locate
+from zope.schema.fieldproperty import FieldProperty
+
+from pyams_content import _
+
+
+@implementer(IReviewComment)
+class ReviewComment(Persistent, Contained):
+ """Review comment persistent class"""
+
+ owner = FieldProperty(IReviewComment['owner'])
+ comment = FieldProperty(IReviewComment['comment'])
+ comment_type = FieldProperty(IReviewComment['comment_type'])
+ creation_date = FieldProperty(IReviewComment['creation_date'])
+
+ def __init__(self, owner, comment, comment_type='comment'):
+ self.owner = owner
+ self.comment = comment
+ self.comment_type = comment_type
+ self.creation_date = datetime.utcnow()
+
+
+@implementer(IReviewComments)
+class ReviewCommentsContainer(BTreeOrderedContainer):
+ """Review comments container"""
+
+ reviewers = FieldProperty(IReviewComments['reviewers'])
+
+ def clear(self):
+ for k in self.keys()[:]:
+ del self[k]
+
+ def add_comment(self, comment):
+ uuid = str(uuid4())
+ self[uuid] = comment
+ reviewers = self.reviewers or set()
+ reviewers.add(comment.owner)
+ self.reviewers = reviewers
+ get_current_registry().notify(CommentAddedEvent(self.__parent__, comment))
+
+
+@adapter_config(context=IReviewTarget, provides=IReviewComments)
+def SharedContentReviewCommentsFactory(context):
+ """Shared content review comments factory"""
+ annotations = IAnnotations(context)
+ comments = annotations.get(REVIEW_COMMENTS_ANNOTATION_KEY)
+ if comments is None:
+ comments = annotations[REVIEW_COMMENTS_ANNOTATION_KEY] = ReviewCommentsContainer()
+ locate(comments, context, '++review-comments++')
+ return comments
+
+
+@adapter_config(name='review-comments', context=IReviewTarget, provides=ITraversable)
+class SharedContentReviewCommentsNamespace(ContextAdapter):
+ """++review-comments++ namespace traverser"""
+
+ def traverse(self, name, furtherpath=None):
+ return IReviewComments(self.context)
+
+
+@adapter_config(name='review-comments', context=IReviewTarget, provides=ISublocations)
+class SharedContentReviewCommentsSublocations(ContextAdapter):
+ """Shared content review comments sub-location adapter"""
+
+ def sublocations(self):
+ return IReviewComments(self.context).values()
+
+
+@adapter_config(context=IReviewTarget, provides=IReviewManager)
+class SharedContentReviewAdapter(ContextAdapter):
+ """Shared content review adapter"""
+
+ review_template = PageTemplateFile(os.path.join(os.path.dirname(__file__),
+ 'zmi/templates/review-notification.pt'))
+
+ def ask_review(self, reviewers, comment, notify_all=True):
+ """Ask for content review"""
+ roles = IWfSharedContentRoles(self.context, None)
+ if roles is None:
+ return
+ # check request
+ request = check_request()
+ translate = request.localizer.translate
+ # initialize mailer
+ security = query_utility(ISecurityManager)
+ settings = INotificationSettings(security)
+ sender_name = request.principal.title if request.principal is not None else settings.sender_name
+ sender_address = settings.sender_email
+ sender = security.get_principal(request.principal.id, info=False)
+ sender_mail_info = IPrincipalMailInfo(sender, None)
+ if sender_mail_info is not None:
+ for sender_name, sender_address in sender_mail_info.get_addresses():
+ break
+ if settings.enable_notifications:
+ mailer = query_utility(IMailer, name=settings.mailer)
+ else:
+ mailer = None
+ # create message
+ message_body = self.review_template(request=request,
+ context=self.context,
+ translate=query_utility(IChameleonTranslate),
+ options={'settings': settings,
+ 'comment': comment,
+ 'sender': sender_name})
+ # notify reviewers
+ notifications = 0
+ readers = roles.readers
+ for reviewer in reviewers:
+ if settings.enable_notifications and \
+ (mailer is not None) and \
+ (notify_all or (reviewer not in readers)):
+ principal = security.get_principal(reviewer, info=False)
+ if not isinstance(principal, MissingPrincipal):
+ mail_info = IPrincipalMailInfo(principal, None)
+ if mail_info is not None:
+ for name, address in mail_info.get_addresses():
+ message = HTMLMessage(
+ subject=translate(_("[{service_name}] A content review is requested")).format(
+ service_name=settings.subject_prefix),
+ fromaddr='{name} <{address}>'.format(name=sender_name,
+ address=sender_address),
+ toaddr='{name} <{address}>'.format(name=name, address=address),
+ html=message_body)
+ mailer.send(message)
+ notifications += 1
+ readers.add(reviewer)
+ roles.readers = readers
+ # add comment
+ review_comment = ReviewComment(owner=request.principal.id,
+ comment=comment,
+ comment_type='request')
+ IReviewComments(self.context).add_comment(review_comment)
+ # return notifications count
+ return notifications
+
+
+#
+# Review comment notification
+#
+
+try:
+ from pyams_notify.interfaces import INotification, INotificationHandler
+ from pyams_notify.event import Notification
+except ImportError:
+ pass
+else:
+
+ @subscriber(ICommentAddedEvent)
+ def handle_new_comment(event):
+ """Handle new review comment"""
+ request = query_request()
+ if request is None:
+ return
+ content = event.object
+ translate = request.localizer.translate
+ notification = Notification(request=request,
+ context=content,
+ source=event.comment.owner,
+ action='notify',
+ category='content.review',
+ message=translate(_("A new comment was added on content « {0} »")).format(
+ II18n(content).query_attribute('title', request=request)),
+ url=absolute_url(content, request, 'admin#review-comments.html'),
+ comments=IReviewComments(content))
+ notification.send()
+
+
+ @adapter_config(name='content.review', context=INotification, provides=INotificationHandler)
+ class ContentReviewNotificationHandler(ContextAdapter):
+ """Content review notification handler"""
+
+ def get_target(self):
+ context = self.context.context
+ principals = set()
+ protection = IProtectedObject(context, None)
+ if protection is not None:
+ principals |= protection.get_principals(READER_ROLE)
+ comments = self.context.user_data.get('comments')
+ if comments is not None:
+ principals |= comments.reviewers
+ source_id = self.context.source['id']
+ if source_id in principals:
+ principals.remove(source_id)
+ return {'principals': tuple(principals)}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/review/interfaces.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,101 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.interfaces import IContainer, IContained
+from zope.interface.interfaces import IObjectEvent, ObjectEvent
+
+# import packages
+from pyams_security.schema import Principal, PrincipalsSet
+from zope.container.constraints import contains, containers
+from zope.interface import implementer, Interface, Attribute
+from zope.schema import Text, Choice, Datetime
+
+from pyams_content import _
+
+
+COMMENT_TYPES = {'request': _("Review request"),
+ 'comment': _("Reviewer comment")}
+
+
+class ICommentAddedEvent(IObjectEvent):
+ """Comment added event interface"""
+
+ comment = Attribute("New comment")
+
+
+@implementer(ICommentAddedEvent)
+class CommentAddedEvent(ObjectEvent):
+ """Comment added event"""
+
+ def __init__(self, object, comment):
+ super(CommentAddedEvent, self).__init__(object)
+ self.comment = comment
+
+
+class IReviewComment(IContained, IAttributeAnnotatable):
+ """Review comment interface"""
+
+ containers('.IReviewComments')
+
+ owner = Principal(title=_("Comment writer"),
+ required=True)
+
+ comment = Text(title=_("Comment body"),
+ required=True)
+
+ comment_type = Choice(title=_("Comment type"),
+ values=COMMENT_TYPES.keys(),
+ required=True,
+ default='comment')
+
+ creation_date = Datetime(title=_("Creation date"),
+ required=False)
+
+
+REVIEW_COMMENTS_ANNOTATION_KEY = 'pyams_content.review_comments'
+
+
+class IReviewComments(IContainer):
+ """Review comments container interface"""
+
+ contains(IReviewComment)
+
+ reviewers = PrincipalsSet(title=_("Reviewers list"),
+ description=_("List of principals which reviewed the comment"),
+ required=False)
+
+ def clear(self):
+ """Remove all comments"""
+
+ def add_comment(self, comment):
+ """Add given comment to list"""
+
+
+class IReviewManager(Interface):
+ """Content review interface"""
+
+ def ask_review(self, reviewers, comment, notify=True):
+ """Ask for content review"""
+
+
+class IReviewTarget(Interface):
+ """Review target marker interface
+
+ This interface is used to mark contents which can handle review.
+ """
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/review/zmi/__init__.py Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,281 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION, COMMENT_CONTENT_PERMISSION
+from pyams_content.features.review.interfaces import IReviewManager, IReviewComments, IReviewTarget
+from pyams_security.interfaces import ISecurityManager
+from pyams_security.interfaces.profile import IPublicProfile
+from pyams_skin.interfaces.viewlet import IContextActions, IWidgetTitleViewletManager
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces.menu import IContentManagementMenu
+from pyams_zmi.layer import IAdminLayer
+from pyramid_chameleon.interfaces import IChameleonTranslate
+
+# import packages
+from pyams_content.features.review import ReviewComment
+from pyams_form.form import AJAXAddForm
+from pyams_form.schema import CloseButton
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_security.schema import PrincipalsSet
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_skin.viewlet.toolbar import ToolbarMenuItem, JsToolbarAction
+from pyams_template.template import template_config, get_view_template
+from pyams_utils.date import get_age, format_datetime
+from pyams_utils.registry import get_utility, query_utility
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zmi.form import AdminDialogAddForm
+from pyams_zmi.view import InnerAdminView
+from pyramid.view import view_config
+from z3c.form import field, button
+from zope.interface import Interface
+from zope.schema import Text, Bool
+
+from pyams_content import _
+
+
+#
+# Review request form
+#
+
+@viewlet_config(name='ask-review.menu', context=IReviewTarget, layer=IPyAMSLayer,
+ view=Interface, manager=IContextActions, permission=MANAGE_CONTENT_PERMISSION, weight=10)
+class WfSharedContentReviewMenu(ToolbarMenuItem):
+ """Shared content review menu"""
+
+ label = _("Ask for review...")
+ label_css_class = 'fa fa-fw fa-eye'
+
+ url = 'ask-review.html'
+ modal_target = True
+
+
+class ISharedContentReviewInfo(Interface):
+ """Shared content review infos"""
+
+ reviewers = PrincipalsSet(title=_("Sought principals"),
+ description=_("List of principals from which a review is requested"),
+ required=True)
+
+ comment = Text(title=_("Comment"),
+ description=_("Comment associated with this request"),
+ required=True)
+
+ notify_all = Bool(title=_("Notify all reviewers"),
+ description=_("If 'yes', selected reviewers will be notified by mail of your request, "
+ "even if they were already members of the reviewers group. Otherwise, only new "
+ "reviewers will be notified"),
+ default=False,
+ required=True)
+
+
+class ISharedContentReviewButtons(Interface):
+ """Shared content review form buttons"""
+
+ close = CloseButton(name='close', title=_("Cancel"))
+ review = button.Button(name='review', title=_("Ask for content review"))
+
+
+@pagelet_config(name='ask-review.html', context=IReviewTarget, layer=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION)
+class WfSharedContentReviewForm(AdminDialogAddForm):
+ """Shared content review form"""
+
+ legend = _("Content review request")
+ icon_css_class = 'fa fa-fw fa-eye'
+
+ fields = field.Fields(ISharedContentReviewInfo)
+ buttons = button.Buttons(ISharedContentReviewButtons)
+
+ ajax_handler = 'ask-review.json'
+ edit_permission = MANAGE_CONTENT_PERMISSION
+
+ label_css_class = 'control-label col-md-4'
+ input_css_class = 'col-md-8'
+
+ def updateWidgets(self, prefix=None):
+ super(WfSharedContentReviewForm, self).updateWidgets(prefix)
+ if 'comment' in self.widgets:
+ self.widgets['comment'].widget_css_class = 'textarea'
+ self.widgets['comment'].addClass('height-100')
+
+ def updateActions(self):
+ super(WfSharedContentReviewForm, self).updateActions()
+ if 'review' in self.actions:
+ self.actions['review'].addClass('btn-primary')
+
+ def createAndAdd(self, data):
+ manager = IReviewManager(self.context, None)
+ if manager is not None:
+ return manager.ask_review(data.get('reviewers'),
+ data.get('comment'),
+ data.get('notify_all'))
+
+
+@view_config(name='ask-review.json', context=IReviewTarget, request_type=IPyAMSLayer,
+ permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
+class WfSharedContentReviewAJAXForm(AJAXAddForm, WfSharedContentReviewForm):
+ """Shared content review form, JSON renderer"""
+
+ def get_ajax_output(self, changes):
+ translate = self.request.localizer.translate
+ if changes:
+ return {'status': 'success',
+ 'message': translate(_("Request successful. "
+ "{count} new notification(s) have been sent")).format(count=changes),
+ 'events': [{
+ 'event': 'PyAMS_content.changed_item',
+ 'options': {'handler': 'PyAMS_content.review.updateComments'}
+ }]}
+ else:
+ return {'status': 'info',
+ 'message': translate(_("Request successful. No new notification have been sent")),
+ 'events': [{
+ 'event': 'PyAMS_content.changed_item',
+ 'options': {'handler': 'PyAMS_content.review.updateComments'}
+ }]}
+
+
+#
+# Share contents comments
+#
+
+@viewlet_config(name='review-comments.menu', context=IReviewTarget, layer=IAdminLayer,
+ manager=IContentManagementMenu, permission=COMMENT_CONTENT_PERMISSION, weight=30)
+class SharedContentReviewCommentsMenu(MenuItem):
+ """Shared content review comments menu"""
+
+ label = _("Comments")
+ icon_class = 'fa-comments-o'
+ url = '#review-comments.html'
+
+ badge_class = 'bg-color-info'
+
+ def update(self):
+ super(SharedContentReviewCommentsMenu, self).update()
+ nb_comments = len(IReviewComments(self.context))
+ self.badge = str(nb_comments)
+ if nb_comments == 0:
+ self.badge_class += ' hidden'
+
+
+@pagelet_config(name='review-comments.html', context=IReviewTarget, layer=IPyAMSLayer,
+ permission=COMMENT_CONTENT_PERMISSION)
+@template_config(template='templates/review-comments.pt', layer=IPyAMSLayer)
+class SharedContentReviewCommentsView(InnerAdminView):
+ """Shared content review comments view"""
+
+ legend = _("Review comments")
+
+ comments = None
+ security = None
+
+ def update(self):
+ super(SharedContentReviewCommentsView, self).update()
+ self.comments = IReviewComments(self.context).values()
+ self.security = get_utility(ISecurityManager)
+
+ def get_principal(self, principal_id):
+ return self.security.get_principal(principal_id)
+
+ def get_avatar(self, principal):
+ return IPublicProfile(principal).avatar
+
+ def get_date(self, comment):
+ return format_datetime(comment.creation_date)
+
+ def get_age(self, comment):
+ return get_age(comment.creation_date)
+
+
+@viewlet_config(name='add-review-comment.action', context=IReviewTarget, layer=IAdminLayer,
+ view=SharedContentReviewCommentsView, manager=IWidgetTitleViewletManager,
+ permission=COMMENT_CONTENT_PERMISSION)
+class SharedContentReviewAddCommentAction(JsToolbarAction):
+ """Shared content review add comment action"""
+
+ label = _("Add comment...")
+ url = 'PyAMS_content.review.addCommentAction'
+
+
+@view_config(name='get-last-review-comments.json', context=IReviewTarget, request_type=IPyAMSLayer,
+ permission=COMMENT_CONTENT_PERMISSION, renderer='json', xhr=True)
+@template_config(template='templates/review-comments-json.pt')
+class ReviewCommentsView(SharedContentReviewCommentsView):
+ """"Get review comments"""
+
+ def __init__(self, request):
+ self.request = request
+ self.context = request.context
+
+ template = get_view_template()
+
+ def __call__(self):
+ result = {'status': 'success',
+ 'count': 0}
+ comments = IReviewComments(self.context)
+ previous_count = int(self.request.params.get('count', 0))
+ current_count = len(comments)
+ if previous_count == current_count:
+ result['count'] = current_count
+ else:
+ self.comments = comments.values()[previous_count:]
+ self.security = get_utility(ISecurityManager)
+ comments_body = self.template(request=self.request,
+ context=self.context,
+ view=self,
+ translate=query_utility(IChameleonTranslate))
+ result.update({'content': comments_body,
+ 'count': len(comments)})
+ return result
+
+
+@view_config(name='add-review-comment.json', context=IReviewTarget, request_type=IPyAMSLayer,
+ permission=COMMENT_CONTENT_PERMISSION, renderer='json', xhr=True)
+@template_config(template='templates/review-add-comment.pt')
+class ReviewCommentAddForm(object):
+ """Review comment add form"""
+
+ def __init__(self, request):
+ self.request = request
+ self.context = request.context
+
+ template = get_view_template()
+
+ def __call__(self):
+ request = self.request
+ translate = request.localizer.translate
+ comment_body = request.params.get('comment')
+ if not comment_body:
+ return {'status': 'error',
+ 'message': translate(_("Message is mandatory!"))}
+ # add new comment
+ comment = ReviewComment(owner=request.principal.id,
+ comment=request.params.get('comment'))
+ comments = IReviewComments(request.context)
+ comments.add_comment(comment)
+ # return comment infos
+ profile = IPublicProfile(request.principal)
+ comment_body = self.template(request=request,
+ context=self.context,
+ translate=query_utility(IChameleonTranslate),
+ options={'comment': comment,
+ 'profile': profile})
+ return {'status': 'success',
+ 'callback': 'PyAMS_content.review.addCommentCallback',
+ 'options': {'content': comment_body,
+ 'count': len(comments)}}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/review/zmi/templates/review-add-comment.pt Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,27 @@
+<li class="message" tal:define="comment options['comment']" i18n:domain="pyams_content"
+ tal:attributes="class 'message {0}'.format(comment.comment_type)">
+ <tal:var define="avatar options['profile'].avatar">
+ <tal:if condition="avatar">
+ <img tal:attributes="src extension:absolute_url(avatar, '++thumb++square:32x32.png')" />
+ </tal:if>
+ <tal:if condition="not:avatar">
+ <i class="fa fa-lg fa-user img"></i>
+ </tal:if>
+ <span class="message-text">
+ <a class="username">
+ <tal:if condition="comment.comment_type=='request'">
+ <span class="txt-color-text nobold"
+ i18n:translate="">Review query from</span>
+ </tal:if>
+ <tal:var content="request.principal.title">Owner</tal:var>
+ <tal:if condition="comment.owner in context.readers">
+
+ <span class="txt-color-text nobold"
+ i18n:translate="">(as reviewer)</span>
+ </tal:if>
+ <time class="margin-left-10" i18n:translate="">just now</time>
+ </a>
+ <tal:var content="structure extension:html(comment.comment)" />
+ </span>
+ </tal:var>
+</li>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/review/zmi/templates/review-comments-json.pt Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,31 @@
+<li class="message" tal:repeat="comment view.comments" i18n:domain="pyams_content"
+ tal:attributes="class 'message {0} {1}'.format(comment.comment_type, 'odd' if repeat['comment'].odd else 'even')">
+ <tal:var define="principal view.get_principal(comment.owner);
+ avatar view.get_avatar(principal);">
+ <tal:if condition="avatar">
+ <img tal:attributes="src extension:absolute_url(avatar, '++thumb++square:32x32.png')" />
+ </tal:if>
+ <tal:if condition="not:avatar">
+ <i class="fa fa-lg fa-user img"></i>
+ </tal:if>
+ <span class="message-text">
+ <a class="username">
+ <tal:if condition="comment.comment_type=='request'">
+ <span class="txt-color-text nobold"
+ i18n:translate="">Review query from</span>
+ </tal:if>
+ <tal:var content="principal.title">Owner</tal:var>
+ <tal:if condition="comment.owner in context.readers">
+
+ <span class="txt-color-text nobold"
+ i18n:translate="">(as reviewer)</span>
+ </tal:if>
+ <time class="margin-left-10 hint opaque align-base"
+ data-ams-hint-gravity="w" data-ams-hint-offset="5"
+ tal:attributes="title view.get_date(comment)"
+ tal:content="view.get_age(comment).lower()">age</time>
+ </a>
+ <tal:var content="structure extension:html(comment.comment)" />
+ </span>
+ </tal:var>
+</li>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/review/zmi/templates/review-comments.pt Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,68 @@
+<div class="ams-widget comments" i18n:domain="pyams_content"
+ data-ams-plugins="pyams_content"
+ tal:attributes="data-ams-plugin-pyams_content-src extension:resource_path('pyams_content.skin:pyams_content');
+ data-ams-plugin-pyams_content-css extension:resource_path('pyams_content.skin:pyams_content_css');"
+ data-ams-plugin-pyams_content-async="false"
+ data-ams-callback="PyAMS_content.review.initComments">
+ <header>
+ <span tal:condition="view.widget_icon_class | nothing"
+ class="widget-icon"><i tal:attributes="class view.widget_icon_class"></i>
+ </span>
+ <h2 tal:content="view.legend"></h2>
+ <tal:var content="structure provider:pyams.widget_title" />
+ <tal:var content="structure provider:pyams.toolbar" />
+ </header>
+ <div class="widget-body no-padding">
+ <div class="chat-body no-padding">
+ <ul class="messages">
+ <li class="message" tal:repeat="comment view.comments"
+ tal:attributes="class 'message {0} {1}'.format(comment.comment_type, 'odd' if repeat['comment'].odd else 'even')">
+ <tal:var define="principal view.get_principal(comment.owner);
+ avatar view.get_avatar(principal);">
+ <tal:if condition="avatar">
+ <img tal:attributes="src extension:absolute_url(avatar, '++thumb++square:32x32.png')" />
+ </tal:if>
+ <tal:if condition="not:avatar">
+ <i class="fa fa-lg fa-user img"></i>
+ </tal:if>
+ <span class="message-text">
+ <a class="username">
+ <tal:if condition="comment.comment_type=='request'">
+ <span class="txt-color-text nobold"
+ i18n:translate="">Review query from</span>
+ </tal:if>
+ <tal:var content="principal.title">Owner</tal:var>
+ <tal:if condition="comment.owner in context.readers">
+
+ <span class="txt-color-text nobold"
+ i18n:translate="">(as reviewer)</span>
+ </tal:if>
+ <time class="margin-left-10 hint opaque align-base"
+ data-ams-hint-gravity="w" data-ams-hint-offset="5"
+ tal:attributes="title view.get_date(comment)"
+ tal:content="view.get_age(comment).lower()">age</time>
+ </a>
+ <tal:var content="structure extension:html(comment.comment)" />
+ </span>
+ </tal:var>
+ </li>
+ </ul>
+ </div>
+ <div class="chat-footer no-padding">
+ <form method="post" data-async
+ data-ams-form-data-init-callback="PyAMS_content.review.initCommentData"
+ tal:attributes="data-ams-form-handler extension:absolute_url(context, 'add-review-comment.json')">
+ <fieldset class="textarea-div no-margin">
+ <div class="typearea">
+ <textarea placeholder="Add a comment..." name="comment"
+ i18n:attributes="placeholder"></textarea>
+ </div>
+ </fieldset>
+ <span class="textarea-controls">
+ <button type="submit" class="btn btn-sm btn-primary pull-right margin-right-20"
+ i18n:translate="">Add comment</button>
+ </span>
+ </form>
+ </div>
+ </div>
+</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_content/features/review/zmi/templates/review-notification.pt Fri Nov 10 11:46:27 2017 +0100
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" i18n:domain="pyams_content">
+<head>
+ <title i18n:translate="">
+ [<tal:var i18n:name="service_name" tal:content="options['settings'].service_name">Service name</tal:var>]
+ You are requested for a content review
+ </title>
+ <meta http-equiv="CONTENT-TYPE" content="text/html; charset=utf-8" />
+ <meta charset="utf-8" />
+ <style>
+ html,
+ body,
+ p,
+ pre {
+ font-family: Ubuntu, Verdana, Arial, Helvetica, sans-serif;
+ font-size: 13px;
+ color: #333;
+ }
+ pre {
+ border-left: 2px solid #346597;
+ padding-left: 10px;
+ }
+ pre.noborder {
+ border-left: none;
+ padding-left: 0;
+ }
+ </style>
+</head>
+<body>
+ <p i18n:translate="">Hello,</p>
+ <p i18n:translate="">
+ You have been requested by <span i18n:name="sender" tal:content="options['sender']">sender</span>, contributor
+ of « <span i18n:name="service_name" tal:content="options['settings'].service_name">Service name</span> »
+ website, to make a review of a content.
+ </p>
+ <p i18n:translate="">
+ <span i18n:name="sender" tal:content="options['sender']">sender</span> added the following message to his
+ request:
+ </p>
+ <pre style="padding: 5px 20px;" tal:content="options['comment']">comment</pre>
+ <p i18n:translate="">To review and comment this publication, please use the following link: <a i18n:name="target"
+ tal:attributes="href extension:absolute_url(context, 'admin')"
+ tal:content="i18n:context.title"></a>.</p>
+ <p i18n:translate="">After reading this content, please use the « Comments » menu entry.</p>
+ <p i18n:translate="">If you don't want to reply to this request, please contact
+ <span i18n:name="sender" tal:content="options['sender']">sender</span> directly by replying to this mail.</p>
+ <p i18n:translate="">Thank you.</p>
+ <br />
+ <pre class="noborder" tal:content="options['settings'].signature">signature</pre>
+</body>
+</html>
--- a/src/pyams_content/shared/common/zmi/review.py Fri Oct 13 10:03:20 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,282 +0,0 @@
-#
-# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-
-__docformat__ = 'restructuredtext'
-
-
-# import standard library
-
-# import interfaces
-from pyams_content.interfaces import MANAGE_CONTENT_PERMISSION, COMMENT_CONTENT_PERMISSION
-from pyams_content.interfaces.review import IReviewManager, IReviewComments
-from pyams_content.shared.common.interfaces import IWfSharedContent
-from pyams_security.interfaces import ISecurityManager
-from pyams_security.interfaces.profile import IPublicProfile
-from pyams_skin.interfaces.viewlet import IContextActions, IWidgetTitleViewletManager
-from pyams_skin.layer import IPyAMSLayer
-from pyams_zmi.interfaces.menu import IContentManagementMenu
-from pyams_zmi.layer import IAdminLayer
-from pyramid_chameleon.interfaces import IChameleonTranslate
-
-# import packages
-from pyams_content.shared.common.review import ReviewComment
-from pyams_form.form import AJAXAddForm
-from pyams_form.schema import CloseButton
-from pyams_pagelet.pagelet import pagelet_config
-from pyams_security.schema import PrincipalsSet
-from pyams_skin.viewlet.menu import MenuItem
-from pyams_skin.viewlet.toolbar import ToolbarMenuItem, JsToolbarAction
-from pyams_template.template import template_config, get_view_template
-from pyams_utils.date import get_age, format_datetime
-from pyams_utils.registry import get_utility, query_utility
-from pyams_viewlet.viewlet import viewlet_config
-from pyams_zmi.form import AdminDialogAddForm
-from pyams_zmi.view import InnerAdminView
-from pyramid.view import view_config
-from z3c.form import field, button
-from zope.interface import Interface
-from zope.schema import Text, Bool
-
-from pyams_content import _
-
-
-#
-# Review request form
-#
-
-@viewlet_config(name='ask-review.menu', context=IWfSharedContent, layer=IPyAMSLayer,
- view=Interface, manager=IContextActions, permission=MANAGE_CONTENT_PERMISSION, weight=10)
-class WfSharedContentReviewMenu(ToolbarMenuItem):
- """Shared content review menu"""
-
- label = _("Ask for review...")
- label_css_class = 'fa fa-fw fa-eye'
-
- url = 'ask-review.html'
- modal_target = True
-
-
-class ISharedContentReviewInfo(Interface):
- """Shared content review infos"""
-
- reviewers = PrincipalsSet(title=_("Sought principals"),
- description=_("List of principals from which a review is requested"),
- required=True)
-
- comment = Text(title=_("Comment"),
- description=_("Comment associated with this request"),
- required=True)
-
- notify_all = Bool(title=_("Notify all reviewers"),
- description=_("If 'yes', selected reviewers will be notified by mail of your request, "
- "even if they were already members of the reviewers group. Otherwise, only new "
- "reviewers will be notified"),
- default=False,
- required=True)
-
-
-class ISharedContentReviewButtons(Interface):
- """Shared content review form buttons"""
-
- close = CloseButton(name='close', title=_("Cancel"))
- review = button.Button(name='review', title=_("Ask for content review"))
-
-
-@pagelet_config(name='ask-review.html', context=IWfSharedContent, layer=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION)
-class WfSharedContentReviewForm(AdminDialogAddForm):
- """Shared content review form"""
-
- legend = _("Content review request")
- icon_css_class = 'fa fa-fw fa-eye'
-
- fields = field.Fields(ISharedContentReviewInfo)
- buttons = button.Buttons(ISharedContentReviewButtons)
-
- ajax_handler = 'ask-review.json'
- edit_permission = MANAGE_CONTENT_PERMISSION
-
- label_css_class = 'control-label col-md-4'
- input_css_class = 'col-md-8'
-
- def updateWidgets(self, prefix=None):
- super(WfSharedContentReviewForm, self).updateWidgets(prefix)
- if 'comment' in self.widgets:
- self.widgets['comment'].widget_css_class = 'textarea'
- self.widgets['comment'].addClass('height-100')
-
- def updateActions(self):
- super(WfSharedContentReviewForm, self).updateActions()
- if 'review' in self.actions:
- self.actions['review'].addClass('btn-primary')
-
- def createAndAdd(self, data):
- manager = IReviewManager(self.context, None)
- if manager is not None:
- return manager.ask_review(data.get('reviewers'),
- data.get('comment'),
- data.get('notify_all'))
-
-
-@view_config(name='ask-review.json', context=IWfSharedContent, request_type=IPyAMSLayer,
- permission=MANAGE_CONTENT_PERMISSION, renderer='json', xhr=True)
-class WfSharedContentReviewAJAXForm(AJAXAddForm, WfSharedContentReviewForm):
- """Shared content review form, JSON renderer"""
-
- def get_ajax_output(self, changes):
- translate = self.request.localizer.translate
- if changes:
- return {'status': 'success',
- 'message': translate(_("Request successful. "
- "{count} new notification(s) have been sent")).format(count=changes),
- 'events': [{
- 'event': 'PyAMS_content.changed_item',
- 'options': {'handler': 'PyAMS_content.review.updateComments'}
- }]}
- else:
- return {'status': 'info',
- 'message': translate(_("Request successful. No new notification have been sent")),
- 'events': [{
- 'event': 'PyAMS_content.changed_item',
- 'options': {'handler': 'PyAMS_content.review.updateComments'}
- }]}
-
-
-#
-# Share contents comments
-#
-
-@viewlet_config(name='review-comments.menu', context=IWfSharedContent, layer=IAdminLayer,
- manager=IContentManagementMenu, permission=COMMENT_CONTENT_PERMISSION, weight=30)
-class SharedContentReviewCommentsMenu(MenuItem):
- """Shared content review comments menu"""
-
- label = _("Comments")
- icon_class = 'fa-comments-o'
- url = '#review-comments.html'
-
- badge_class = 'bg-color-info'
-
- def update(self):
- super(SharedContentReviewCommentsMenu, self).update()
- nb_comments = len(IReviewComments(self.context))
- self.badge = str(nb_comments)
- if nb_comments == 0:
- self.badge_class += ' hidden'
-
-
-@pagelet_config(name='review-comments.html', context=IWfSharedContent, layer=IPyAMSLayer,
- permission=COMMENT_CONTENT_PERMISSION)
-@template_config(template='templates/review-comments.pt', layer=IPyAMSLayer)
-class SharedContentReviewCommentsView(InnerAdminView):
- """Shared content review comments view"""
-
- legend = _("Review comments")
-
- comments = None
- security = None
-
- def update(self):
- super(SharedContentReviewCommentsView, self).update()
- self.comments = IReviewComments(self.context).values()
- self.security = get_utility(ISecurityManager)
-
- def get_principal(self, principal_id):
- return self.security.get_principal(principal_id)
-
- def get_avatar(self, principal):
- return IPublicProfile(principal).avatar
-
- def get_date(self, comment):
- return format_datetime(comment.creation_date)
-
- def get_age(self, comment):
- return get_age(comment.creation_date)
-
-
-@viewlet_config(name='add-review-comment.action', context=IWfSharedContent, layer=IAdminLayer,
- view=SharedContentReviewCommentsView, manager=IWidgetTitleViewletManager,
- permission=COMMENT_CONTENT_PERMISSION)
-class SharedContentReviewAddCommentAction(JsToolbarAction):
- """Shared content review add comment action"""
-
- label = _("Add comment...")
- url = 'PyAMS_content.review.addCommentAction'
-
-
-@view_config(name='get-last-review-comments.json', context=IWfSharedContent, request_type=IPyAMSLayer,
- permission=COMMENT_CONTENT_PERMISSION, renderer='json', xhr=True)
-@template_config(template='templates/review-comments-json.pt')
-class ReviewCommentsView(SharedContentReviewCommentsView):
- """"Get review comments"""
-
- def __init__(self, request):
- self.request = request
- self.context = request.context
-
- template = get_view_template()
-
- def __call__(self):
- result = {'status': 'success',
- 'count': 0}
- comments = IReviewComments(self.context)
- previous_count = int(self.request.params.get('count', 0))
- current_count = len(comments)
- if previous_count == current_count:
- result['count'] = current_count
- else:
- self.comments = comments.values()[previous_count:]
- self.security = get_utility(ISecurityManager)
- comments_body = self.template(request=self.request,
- context=self.context,
- view=self,
- translate=query_utility(IChameleonTranslate))
- result.update({'content': comments_body,
- 'count': len(comments)})
- return result
-
-
-@view_config(name='add-review-comment.json', context=IWfSharedContent, request_type=IPyAMSLayer,
- permission=COMMENT_CONTENT_PERMISSION, renderer='json', xhr=True)
-@template_config(template='templates/review-add-comment.pt')
-class ReviewCommentAddForm(object):
- """Review comment add form"""
-
- def __init__(self, request):
- self.request = request
- self.context = request.context
-
- template = get_view_template()
-
- def __call__(self):
- request = self.request
- translate = request.localizer.translate
- comment_body = request.params.get('comment')
- if not comment_body:
- return {'status': 'error',
- 'message': translate(_("Message is mandatory!"))}
- # add new comment
- comment = ReviewComment(owner=request.principal.id,
- comment=request.params.get('comment'))
- comments = IReviewComments(request.context)
- comments.add_comment(comment)
- # return comment infos
- profile = IPublicProfile(request.principal)
- comment_body = self.template(request=request,
- context=self.context,
- translate=query_utility(IChameleonTranslate),
- options={'comment': comment,
- 'profile': profile})
- return {'status': 'success',
- 'callback': 'PyAMS_content.review.addCommentCallback',
- 'options': {'content': comment_body,
- 'count': len(comments)}}
--- a/src/pyams_content/shared/common/zmi/templates/review-add-comment.pt Fri Oct 13 10:03:20 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,27 +0,0 @@
-<li class="message" tal:define="comment options['comment']" i18n:domain="pyams_content"
- tal:attributes="class 'message {0}'.format(comment.comment_type)">
- <tal:var define="avatar options['profile'].avatar">
- <tal:if condition="avatar">
- <img tal:attributes="src extension:absolute_url(avatar, '++thumb++square:32x32.png')" />
- </tal:if>
- <tal:if condition="not:avatar">
- <i class="fa fa-lg fa-user img"></i>
- </tal:if>
- <span class="message-text">
- <a class="username">
- <tal:if condition="comment.comment_type=='request'">
- <span class="txt-color-text nobold"
- i18n:translate="">Review query from</span>
- </tal:if>
- <tal:var content="request.principal.title">Owner</tal:var>
- <tal:if condition="comment.owner in context.readers">
-
- <span class="txt-color-text nobold"
- i18n:translate="">(as reviewer)</span>
- </tal:if>
- <time class="margin-left-10" i18n:translate="">just now</time>
- </a>
- <tal:var content="structure extension:html(comment.comment)" />
- </span>
- </tal:var>
-</li>
--- a/src/pyams_content/shared/common/zmi/templates/review-comments-json.pt Fri Oct 13 10:03:20 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,31 +0,0 @@
-<li class="message" tal:repeat="comment view.comments" i18n:domain="pyams_content"
- tal:attributes="class 'message {0} {1}'.format(comment.comment_type, 'odd' if repeat['comment'].odd else 'even')">
- <tal:var define="principal view.get_principal(comment.owner);
- avatar view.get_avatar(principal);">
- <tal:if condition="avatar">
- <img tal:attributes="src extension:absolute_url(avatar, '++thumb++square:32x32.png')" />
- </tal:if>
- <tal:if condition="not:avatar">
- <i class="fa fa-lg fa-user img"></i>
- </tal:if>
- <span class="message-text">
- <a class="username">
- <tal:if condition="comment.comment_type=='request'">
- <span class="txt-color-text nobold"
- i18n:translate="">Review query from</span>
- </tal:if>
- <tal:var content="principal.title">Owner</tal:var>
- <tal:if condition="comment.owner in context.readers">
-
- <span class="txt-color-text nobold"
- i18n:translate="">(as reviewer)</span>
- </tal:if>
- <time class="margin-left-10 hint opaque align-base"
- data-ams-hint-gravity="w" data-ams-hint-offset="5"
- tal:attributes="title view.get_date(comment)"
- tal:content="view.get_age(comment).lower()">age</time>
- </a>
- <tal:var content="structure extension:html(comment.comment)" />
- </span>
- </tal:var>
-</li>
--- a/src/pyams_content/shared/common/zmi/templates/review-comments.pt Fri Oct 13 10:03:20 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,68 +0,0 @@
-<div class="ams-widget comments" i18n:domain="pyams_content"
- data-ams-plugins="pyams_content"
- tal:attributes="data-ams-plugin-pyams_content-src extension:resource_path('pyams_content.skin:pyams_content');
- data-ams-plugin-pyams_content-css extension:resource_path('pyams_content.skin:pyams_content_css');"
- data-ams-plugin-pyams_content-async="false"
- data-ams-callback="PyAMS_content.review.initComments">
- <header>
- <span tal:condition="view.widget_icon_class | nothing"
- class="widget-icon"><i tal:attributes="class view.widget_icon_class"></i>
- </span>
- <h2 tal:content="view.legend"></h2>
- <tal:var content="structure provider:pyams.widget_title" />
- <tal:var content="structure provider:pyams.toolbar" />
- </header>
- <div class="widget-body no-padding">
- <div class="chat-body no-padding">
- <ul class="messages">
- <li class="message" tal:repeat="comment view.comments"
- tal:attributes="class 'message {0} {1}'.format(comment.comment_type, 'odd' if repeat['comment'].odd else 'even')">
- <tal:var define="principal view.get_principal(comment.owner);
- avatar view.get_avatar(principal);">
- <tal:if condition="avatar">
- <img tal:attributes="src extension:absolute_url(avatar, '++thumb++square:32x32.png')" />
- </tal:if>
- <tal:if condition="not:avatar">
- <i class="fa fa-lg fa-user img"></i>
- </tal:if>
- <span class="message-text">
- <a class="username">
- <tal:if condition="comment.comment_type=='request'">
- <span class="txt-color-text nobold"
- i18n:translate="">Review query from</span>
- </tal:if>
- <tal:var content="principal.title">Owner</tal:var>
- <tal:if condition="comment.owner in context.readers">
-
- <span class="txt-color-text nobold"
- i18n:translate="">(as reviewer)</span>
- </tal:if>
- <time class="margin-left-10 hint opaque align-base"
- data-ams-hint-gravity="w" data-ams-hint-offset="5"
- tal:attributes="title view.get_date(comment)"
- tal:content="view.get_age(comment).lower()">age</time>
- </a>
- <tal:var content="structure extension:html(comment.comment)" />
- </span>
- </tal:var>
- </li>
- </ul>
- </div>
- <div class="chat-footer no-padding">
- <form method="post" data-async
- data-ams-form-data-init-callback="PyAMS_content.review.initCommentData"
- tal:attributes="data-ams-form-handler extension:absolute_url(context, 'add-review-comment.json')">
- <fieldset class="textarea-div no-margin">
- <div class="typearea">
- <textarea placeholder="Add a comment..." name="comment"
- i18n:attributes="placeholder"></textarea>
- </div>
- </fieldset>
- <span class="textarea-controls">
- <button type="submit" class="btn btn-sm btn-primary pull-right margin-right-20"
- i18n:translate="">Add comment</button>
- </span>
- </form>
- </div>
- </div>
-</div>
--- a/src/pyams_content/shared/common/zmi/templates/review-notification.pt Fri Oct 13 10:03:20 2017 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml" i18n:domain="pyams_content">
-<head>
- <title i18n:translate="">
- [<tal:var i18n:name="service_name" tal:content="options['settings'].service_name">Service name</tal:var>]
- You are requested for a content review
- </title>
- <meta http-equiv="CONTENT-TYPE" content="text/html; charset=utf-8" />
- <meta charset="utf-8" />
- <style>
- html,
- body,
- p,
- pre {
- font-family: Ubuntu, Verdana, Arial, Helvetica, sans-serif;
- font-size: 13px;
- color: #333;
- }
- pre {
- border-left: 2px solid #346597;
- padding-left: 10px;
- }
- pre.noborder {
- border-left: none;
- padding-left: 0;
- }
- </style>
-</head>
-<body>
- <p i18n:translate="">Hello,</p>
- <p i18n:translate="">
- You have been requested by <span i18n:name="sender" tal:content="options['sender']">sender</span>, contributor
- of « <span i18n:name="service_name" tal:content="options['settings'].service_name">Service name</span> »
- website, to make a review of a content.
- </p>
- <p i18n:translate="">
- <span i18n:name="sender" tal:content="options['sender']">sender</span> added the following message to his
- request:
- </p>
- <pre style="padding: 5px 20px;" tal:content="options['comment']">comment</pre>
- <p i18n:translate="">To review and comment this publication, please use the following link: <a i18n:name="target"
- tal:attributes="href extension:absolute_url(context, 'admin')"
- tal:content="i18n:context.title"></a>.</p>
- <p i18n:translate="">After reading this content, please use the « Comments » menu entry.</p>
- <p i18n:translate="">If you don't want to reply to this request, please contact
- <span i18n:name="sender" tal:content="options['sender']">sender</span> directly by replying to this mail.</p>
- <p i18n:translate="">Thank you.</p>
- <br />
- <pre class="noborder" tal:content="options['settings'].signature">signature</pre>
-</body>
-</html>