# HG changeset patch # User Thierry Florac # Date 1510310787 -3600 # Node ID 2dc445ad2cf5d66f23f8b352a23d56bc950e550a # Parent ccd42b19051a5e68b390423ecd817bdc1eedb5d7 Created 'features' module to handle shared features outside of pure content management diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/__init__.py diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/checker/__init__.py --- /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 +# 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 = '
' + + 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, '
') + output.append('
') + else: + output.append(translate(VALUE_OK) + '
') + return '\n'.join(output) + + def inner_check(self, request): + return [] diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/checker/interfaces.py --- /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 +# 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 = 'OK' +MISSING_VALUE = _(' - {field}: no value') +MISSING_LANG_VALUE = _(' - {field} ({lang}): no value') +ERROR_VALUE = _(' - {field}: {message}') + + +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""" diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/checker/zmi/__init__.py --- /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 +# 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 = '{0}'.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) diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/preview/__init__.py --- /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 +# 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 diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/preview/interfaces.py --- /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 +# 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. + """ diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/preview/zmi/__init__.py --- /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 +# 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) diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/preview/zmi/interfaces.py --- /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 +# 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""" diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/review/__init__.py --- /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 +# 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)} diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/review/interfaces.py --- /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 +# 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. + """ diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/review/zmi/__init__.py --- /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 +# 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)}} diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/review/zmi/templates/review-add-comment.pt --- /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 @@ +
  • + + + + + + + + + + + Review query from  + + Owner + +   + (as reviewer) + + + + + + +
  • diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/review/zmi/templates/review-comments-json.pt --- /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 @@ +
  • + + + + + + + + + + + Review query from  + + Owner + +   + (as reviewer) + + + + + + +
  • diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/review/zmi/templates/review-comments.pt --- /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 @@ +
    +
    + + +

    + + +
    +
    + + +
    +
    diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/features/review/zmi/templates/review-notification.pt --- /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 @@ + + + + + [<tal:var i18n:name="service_name" tal:content="options['settings'].service_name">Service name</tal:var>] + You are requested for a content review + + + + + + +

    Hello,

    +

    + You have been requested by sender, contributor + of « Service name » + website, to make a review of a content. +

    +

    + sender added the following message to his + request: +

    +
    comment
    +

    To review and comment this publication, please use the following link: .

    +

    After reading this content, please use the « Comments » menu entry.

    +

    If you don't want to reply to this request, please contact + sender directly by replying to this mail.

    +

    Thank you.

    +
    +
    signature
    + + diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/shared/common/zmi/review.py --- 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 -# 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)}} diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/shared/common/zmi/templates/review-add-comment.pt --- 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 @@ -
  • - - - - - - - - - - - Review query from  - - Owner - -   - (as reviewer) - - - - - - -
  • diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/shared/common/zmi/templates/review-comments-json.pt --- 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 @@ -
  • - - - - - - - - - - - Review query from  - - Owner - -   - (as reviewer) - - - - - - -
  • diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/shared/common/zmi/templates/review-comments.pt --- 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 @@ -
    -
    - - -

    - - -
    -
    - - -
    -
    diff -r ccd42b19051a -r 2dc445ad2cf5 src/pyams_content/shared/common/zmi/templates/review-notification.pt --- 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 @@ - - - - - [<tal:var i18n:name="service_name" tal:content="options['settings'].service_name">Service name</tal:var>] - You are requested for a content review - - - - - - -

    Hello,

    -

    - You have been requested by sender, contributor - of « Service name » - website, to make a review of a content. -

    -

    - sender added the following message to his - request: -

    -
    comment
    -

    To review and comment this publication, please use the following link: .

    -

    After reading this content, please use the « Comments » menu entry.

    -

    If you don't want to reply to this request, please contact - sender directly by replying to this mail.

    -

    Thank you.

    -
    -
    signature
    - -