# HG changeset patch # User Thierry Florac # Date 1464874297 -7200 # Node ID 7cdbe0f6e5c2775a13ae11a801683a8076d9534e # Parent f907592596bf9c3d2c7d489d8199b6353c94fd26 Added content reviews management diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/interfaces/review.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/interfaces/review.py Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,74 @@ +# +# 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 + +# import packages +from pyams_security.schema import Principal +from zope.container.constraints import contains, containers +from zope.interface import Interface +from zope.schema import Text, Choice, Datetime + +from pyams_content import _ + + +COMMENT_TYPES = {'request': _("Review request"), + 'comment': _("Reviewer 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) + + 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""" diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/shared/common/__init__.py --- a/src/pyams_content/shared/common/__init__.py Thu Jun 02 15:30:56 2016 +0200 +++ b/src/pyams_content/shared/common/__init__.py Thu Jun 02 15:31:37 2016 +0200 @@ -17,8 +17,9 @@ # import interfaces from hypatia.interfaces import ICatalog +from pyams_content.interfaces import IBaseContentInfo +from pyams_content.interfaces.review import IReviewComments from pyams_content.shared.common.interfaces import IWfSharedContent, IWfSharedContentRoles, ISharedContent, ISharedTool -from pyams_content.interfaces import IBaseContentInfo from pyams_security.interfaces import IDefaultProtectionPolicy from pyams_sequence.interfaces import ISequentialIdTarget, ISequentialIdInfo from pyams_utils.interfaces import VIEW_PERMISSION @@ -124,6 +125,8 @@ content.contributors = contributors # reset modifiers content.modifiers = set() + # clear review comments + IReviewComments(content).clear() @adapter_config(context=IWfSharedContent, provides=ISequentialIdInfo) diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/shared/common/review.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/common/review.py Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,169 @@ +# +# 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.review import IReviewManager, IReviewComment, IReviewComments, \ + REVIEW_COMMENTS_ANNOTATION_KEY +from pyams_content.shared.common.interfaces import IWfSharedContent, IWfSharedContentRoles +from pyams_mail.interfaces import IPrincipalMailInfo +from pyams_security.interfaces import ISecurityManager +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.lifecycleevent.interfaces import IObjectCreatedEvent +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 +from pyramid.events import subscriber +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""" + + def clear(self): + for k in self.keys()[:]: + del self[k] + + def add_comment(self, comment): + uuid = str(uuid4()) + self[uuid] = comment + + +@adapter_config(context=IWfSharedContent, 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=IWfSharedContent, 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=IWfSharedContent, provides=ISublocations) +class SharedContentReviewCommentsSublocations(ContextAdapter): + """Shared content review comments sub-location adapter""" + + def sublocations(self): + return IReviewComments(self.context).values() + + +@adapter_config(context=IWfSharedContent, 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 + mailer = query_utility(IMailer, name=settings.mailer) + # 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 (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 diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/shared/common/zmi/__init__.py --- a/src/pyams_content/shared/common/zmi/__init__.py Thu Jun 02 15:30:56 2016 +0200 +++ b/src/pyams_content/shared/common/zmi/__init__.py Thu Jun 02 15:31:37 2016 +0200 @@ -20,6 +20,7 @@ # import interfaces from pyams_content.interfaces import MANAGE_SITE_ROOT_PERMISSION, MANAGE_CONTENT_PERMISSION, CREATE_CONTENT_PERMISSION, \ PUBLISH_CONTENT_PERMISSION +from pyams_content.interfaces.review import IReviewComments from pyams_content.shared.common.interfaces import IWfSharedContent, ISharedContent, ISharedTool, IManagerRestrictions from pyams_form.interfaces.form import IFormContextPermissionChecker, IWidgetsPrefixViewletsManager from pyams_i18n.interfaces import II18n, II18nManager @@ -40,7 +41,7 @@ from pyams_skin.page import DefaultPageHeaderAdapter from pyams_skin.table import DefaultElementEditorAdapter from pyams_skin.viewlet.breadcrumb import BreadcrumbItem -from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_skin.viewlet.toolbar import ToolbarMenuItem, ToolbarMenuDivider from pyams_template.template import template_config from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter, ContextAdapter, ContextRequestAdapter from pyams_utils.registry import get_utility @@ -222,8 +223,14 @@ # Duplication menus and views # +@viewlet_config(name='duplication.divider', context=IWfSharedContent, layer=IPyAMSLayer, + view=Interface, manager=IContextActions, permission=CREATE_CONTENT_PERMISSION, weight=49) +class WfSharedContentDuplicationMenuDivider(ToolbarMenuDivider): + """Shared content duplication menu divider""" + + @viewlet_config(name='duplication.menu', context=IWfSharedContent, layer=IPyAMSLayer, - view=Interface, manager=IContextActions, permission=CREATE_CONTENT_PERMISSION, weight=1) + view=Interface, manager=IContextActions, permission=CREATE_CONTENT_PERMISSION, weight=50) class WfSharedContentDuplicateMenu(ToolbarMenuItem): """Shared content duplication menu item""" @@ -279,6 +286,7 @@ new_version.creator = self.request.principal.id new_version.owner = self.request.principal.id new_version.modifiers = set() + IReviewComments(new_version).clear() # store new version translate = self.request.localizer.translate workflow = get_utility(IWorkflow, name=new_content.workflow_name) diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/shared/common/zmi/review.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/common/zmi/review.py Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,264 @@ +# +# 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 +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.page import InnerPage +from pyams_skin.viewlet.menu import MenuItem +from pyams_skin.viewlet.toolbar import ToolbarMenuItem +from pyams_template.template import template_config, get_view_template +from pyams_utils.date import get_age +from pyams_utils.registry import get_utility, query_utility +from pyams_viewlet.viewlet import viewlet_config +from pyams_zmi.form import AdminDialogAddForm +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) + self.widgets['comment'].label_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), + 'event': 'PyAMS_content.changed_item', + 'event_options': {'object_type': 'review_comments'}} + else: + return {'status': 'info', + 'message': translate(_("Request successful. No new notification have been sent")), + 'event': 'PyAMS_content.changed_item', + 'event_options': {'object_type': 'review_comments'}} + + +# +# 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(InnerPage): + """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_age(self, comment): + return get_age(comment.creation_date) + + +@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 f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/shared/common/zmi/templates/review-add-comment.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/common/zmi/templates/review-add-comment.pt Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,16 @@ +
  • + + + + + + + + + + Owner + + + +
  • diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/shared/common/zmi/templates/review-comments-json.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/common/zmi/templates/review-comments-json.pt Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,17 @@ +
  • + + + + + + + + + + Owner + + + +
  • diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/shared/common/zmi/templates/review-comments.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/common/zmi/templates/review-comments.pt Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,54 @@ +
    +
    + + +

    + + +
    +
    +
    +
      +
    • + + + + + + + + + + Owner + + + +
    • +
    +
    + +
    +
    diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/shared/common/zmi/templates/review-notification.pt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/shared/common/zmi/templates/review-notification.pt Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,27 @@ + + + + + [<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 + to make a review of a content called « title » + which has been created on publication platform « Service name ». +

    +

    Comment associated with this request is:

    +
    comment
    +

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

    +
    +
    signature
    + + diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/skin/resources/css/pyams_content.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/skin/resources/css/pyams_content.css Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,42 @@ +.ams-widget.comments .widget-body { + position: fixed; + height: calc(100% - 310px); +} +.ams-widget.comments .widget-body .chat-body { + position: relative; + height: 100%; +} +.ams-widget.comments .widget-body .chat-footer { + position: fixed; + bottom: 10px; +} +.ams-widget.comments .widget-body, +.ams-widget.comments .widget-body .chat-footer { + width: calc(100% - 240px); +} +@media (max-width: 767px) { + .ams-widget.comments .widget-body, + .ams-widget.comments .widget-body .chat-footer { + width: calc(100% - 10px); + } +} +@media (min-width: 768px) and (max-width: 979px) { + .ams-widget.comments .widget-body, + .ams-widget.comments .widget-body .chat-footer { + width: calc(100% - 20px); + } +} +.minified .ams-widget.comments .widget-body, +.minified .ams-widget.comments .widget-body .chat-footer { + width: calc(100% - 65px); +} +@media (max-width: 767px) { + .minified .ams-widget.comments .widget-body, + .minified .ams-widget.comments .widget-body .chat-footer { + width: calc(100% - 55px); + } +} +.hidden-menu .ams-widget.comments .widget-body, +.hidden-menu .ams-widget.comments .widget-body .chat-footer { + width: calc(100% - 30px); +} diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/skin/resources/css/pyams_content.min.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/skin/resources/css/pyams_content.min.css Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,1 @@ +.ams-widget.comments .widget-body{position:fixed;height:calc(100% - 310px)}.ams-widget.comments .widget-body .chat-body{position:relative;height:100%}.ams-widget.comments .widget-body .chat-footer{position:fixed;bottom:10px}.ams-widget.comments .widget-body,.ams-widget.comments .widget-body .chat-footer{width:calc(100% - 240px)}@media(max-width:767px){.ams-widget.comments .widget-body,.ams-widget.comments .widget-body .chat-footer{width:calc(100% - 10px)}}@media(min-width:768px) and (max-width:979px){.ams-widget.comments .widget-body,.ams-widget.comments .widget-body .chat-footer{width:calc(100% - 20px)}}.minified .ams-widget.comments .widget-body,.minified .ams-widget.comments .widget-body .chat-footer{width:calc(100% - 65px)}@media(max-width:767px){.minified .ams-widget.comments .widget-body,.minified .ams-widget.comments .widget-body .chat-footer{width:calc(100% - 55px)}}.hidden-menu .ams-widget.comments .widget-body,.hidden-menu .ams-widget.comments .widget-body .chat-footer{width:calc(100% - 30px)} \ No newline at end of file diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/skin/resources/js/pyams_content.js --- a/src/pyams_content/skin/resources/js/pyams_content.js Thu Jun 02 15:30:56 2016 +0200 +++ b/src/pyams_content/skin/resources/js/pyams_content.js Thu Jun 02 15:31:37 2016 +0200 @@ -333,6 +333,82 @@ }); } } + }, + + + /** + * Review comments management + */ + review: { + + timer: null, + timer_duration: { + general: 30000, + chat: 5000 + }, + + initComments: function(element) { + var chat = $('.chat-body', element); + chat.animate({scrollTop: chat[0].scrollHeight}, 1000); + clearInterval(PyAMS_content.review.timer); + PyAMS_content.review.timer = setInterval(PyAMS_content.review.updateComments, + PyAMS_content.review.timer_duration.chat); + MyAMS.skin.registerCleanCallback(PyAMS_content.review.cleanCommentsCallback); + }, + + cleanCommentsCallback: function() { + clearInterval(PyAMS_content.review.timer); + PyAMS_content.review.timer = setInterval(PyAMS_content.review.updateComments, + PyAMS_content.review.timer_duration.general); + }, + + updateComments: function() { + var badge = $('.badge', 'nav a[href="#review-comments.html"]'), + count; + var chat = $('.chat-body', '.widget-body'); + if (chat.exists()) { + count = $('.message', chat).length; + } else { + count = parseInt(badge.text()); + } + MyAMS.ajax.post('get-last-review-comments.json', {count: count}, function(result) { + if (chat.exists()) { + badge.removeClass('bg-color-danger') + .addClass('bg-color-info'); + } + if (count !== result.count) { + badge.text(result.count).removeClass('hidden'); + if (chat.exists()) { + $('.messages', chat).append(result.content); + chat.animate({scrollTop: chat[0].scrollHeight}, 1000); + } + if (!chat.exists()) { + badge.removeClass('bg-color-info') + .addClass('bg-color-danger') + .animate({'padding': '3px 12px 2px', + 'margin-right': '9px'}, 'slow', function() { + $(this).animate({'padding': '3px 6px 2px', + 'margin-right': '15px'}, 'slow'); + }); + } + } + }); + }, + + initCommentData: function(veto) { + var chat = $('.chat-body', '.widget-body'); + return {count: $('.message', chat).length}; + }, + + addCommentCallback: function(options) { + var form = $(this); + var widget = form.parents('.widget-body'); + $('.messages', widget).append(options.content); + $('textarea[name="comment"]', form).val(''); + var chat = $('.chat-body', widget); + chat.animate({scrollTop: chat[0].scrollHeight}, 1000); + $('.badge', 'nav a[href="#review-comments.html"]').text(options.count).removeClass('hidden'); + } } }; @@ -352,7 +428,16 @@ case 'galleries_container': PyAMS_content.galleries.refreshContainer(settings); break; + case 'review_comments': + PyAMS_content.review.updateComments(); + break; } }); + var badge = $('.badge', 'nav a[href="#review-comments.html"]'); + if (badge.exists()) { + PyAMS_content.review.timer = setInterval(PyAMS_content.review.updateComments, + PyAMS_content.review.timer_duration.general); + } + })(jQuery, this); diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/skin/resources/js/pyams_content.min.js --- a/src/pyams_content/skin/resources/js/pyams_content.min.js Thu Jun 02 15:30:56 2016 +0200 +++ b/src/pyams_content/skin/resources/js/pyams_content.min.js Thu Jun 02 15:31:37 2016 +0200 @@ -1,1 +1,1 @@ -(function(b,a){var d=a.MyAMS;var c={TinyMCE:{initEditor:function(e){e.image_list=c.TinyMCE.getImagesList;e.link_list=c.TinyMCE.getLinksList;return e},getImagesList:function(e){return d.ajax.post("get-images-list.json",{},e)},getLinksList:function(e){return d.ajax.post("get-links-list.json",{},e)}},profile:{switchFavorite:function(){var f=b(this);var e=f.data("sequence-oid");d.ajax.post("switch-user-favorite.json",{oid:e},function(g,h){if(g.favorite){f.removeClass("fa-star-o").addClass("fa-star")}else{f.removeClass("fa-star").addClass("fa-star-o")}})}},extfiles:{refresh:function(f){if(typeof(f)==="string"){f=JSON.parse(f)}var e=b('select[name="form.widgets.files:list"]');var g=e.data("select2");b("").attr("value",f.new_file.id).attr("selected","selected").text(f.new_file.text).appendTo(e);var h=e.select2("data");h.push(f.new_file);e.select2("data",h);g.results.empty();g.opts.populateResults.call(g,g.results,f.files,{term:""})},refreshContainer:function(g){var e=b('tr[data-ams-element-name="'+g.object_name+'"]');var f=b("span.count",b("div.action.extfiles",e));if(g.nb_files>0){f.text("("+g.nb_files+")")}else{f.text("")}}},links:{refresh:function(f){if(typeof(f)==="string"){f=JSON.parse(f)}var e=b('select[name="form.widgets.links:list"]');var g=e.data("select2");b("").attr("value",f.new_link.id).attr("selected","selected").text(f.new_link.text).appendTo(e);var h=e.select2("data");h.push(f.new_link);e.select2("data",h);g.results.empty();g.opts.populateResults.call(g,g.results,f.links,{term:""})},refreshContainer:function(g){var e=b('tr[data-ams-element-name="'+g.object_name+'"]');var f=b("span.count",b("div.action.links",e));if(g.nb_links>0){f.text("("+g.nb_links+")")}else{f.text("")}}},galleries:{refresh:function(f){if(typeof(f)==="string"){f=JSON.parse(f)}var e=b('select[name="form.widgets.galleries:list"]');var g=e.data("select2");b("").attr("value",f.new_gallery.id).attr("selected","selected").text(f.new_gallery.text).appendTo(e);var h=e.select2("data");h.push(f.new_gallery);e.select2("data",h);g.results.empty();g.opts.populateResults.call(g,g.results,f.galleries,{term:""})},setOrder:function(g,h){if(h&&h.item.hasClass("already-dropped")){return}var e=h.item.parents(".gallery");var f=b(".image",e).listattr("data-ams-element-name");d.ajax.post(e.data("ams-location")+"/set-images-order.json",{images:JSON.stringify(f)})},removeFile:function(e){return function(){var f=b(this);d.skin.bigBox({title:d.i18n.WARNING,content:'  '+d.i18n.DELETE_WARNING,buttons:d.i18n.BTN_OK_CANCEL},function(j){if(j===d.i18n.BTN_OK){var i=f.parents(".gallery");var h=i.data("ams-location");var k=f.parents(".image");var g=k.data("ams-element-name");d.ajax.post(h+"/delete-element.json",{object_name:g},function(l,m){k.remove()})}})}},refreshContainer:function(g){var e=b('tr[data-ams-element-name="'+g.object_name+'"]');var f=b("span.count",b("div.action.galleries",e));if(g.nb_galleries>0){f.text("("+g.nb_galleries+")")}else{f.text("")}}},paragraphs:{switchVisibility:function(e){return function(){var h=b(this);var f=h.parents("tr");var g=f.parents("table");d.ajax.post(g.data("ams-location")+"/set-paragraph-visibility.json",{object_name:f.data("ams-element-name")},function(i,j){if(i.visible){b("i",h).attr("class","fa fa-fw fa-eye")}else{b("i",h).attr("class","fa fa-fw fa-eye-slash text-danger")}})}},refreshParagraph:function(g){var f=b('table[id="paragraphs_list"]');var e=b('tr[data-ams-element-name="'+g.object_name+'"]',f);if(g.visible){b("i",b("td.switcher",e)).removeClass("fa-eye-slash text-danger").addClass("fa-eye")}else{b("i",b("td.switcher",e)).removeClass("fa-eye").addClass("fa-eye-slash text-danger")}b("span.title",e).text(g.title||"--")},switchEditor:function(g){var j=b(this);var i=b("i",j);var k=j.parents("td");var h=b(".editor",k);var e=j.parents("tr");if(i.hasClass("fa-plus-square-o")){var f=e.parents("table");h.html('

    ');d.ajax.post(f.data("ams-location")+"/get-paragraph-editor.json",{object_name:e.data("ams-element-name")},function(l){h.html(l);if(l){d.initContent(h);i.removeClass("fa-plus-square-o").addClass("fa-minus-square-o");e.data("ams-disabled-handlers",true)}})}else{d.skin.cleanContainer(h);h.empty();i.removeClass("fa-minus-square-o").addClass("fa-plus-square-o");e.removeData("ams-disabled-handlers")}},switchAllEditors:function(f){var h=b(this);var g=b("i",h);var e=h.parents("table");if(g.hasClass("fa-plus-square-o")){g.removeClass("fa-plus-square-o").addClass("fa-cog fa-spin");d.ajax.post(e.data("ams-location")+"/get-paragraphs-editors.json",{},function(j){for(var k in j){if(!j.hasOwnProperty(k)){continue}var i=b('tr[data-ams-element-name="'+k+'"]',e);var l=b(".editor",i);if(l.is(":empty")){l.html(j[k]);d.initContent(l)}b(".fa-plus-square-o",i).removeClass("fa-plus-square-o").addClass("fa-minus-square-o");i.data("ams-disabled-handlers",true)}if(!b("i.fa-plus-square-o",b("tbody",e)).exists()){g.removeClass("fa-cog fa-spin").addClass("fa-minus-square-o")}})}else{b(".editor",e).each(function(){d.skin.cleanContainer(b(this));b(this).empty()});b(".fa-minus-square-o",e).removeClass("fa-minus-square-o").addClass("fa-plus-square-o");b("tr",e).removeData("ams-disabled-handlers")}}},themes:{initExtracts:function(g){var f=b('select[name="form.widgets.thesaurus_name:list"]',g);var e=f.val();var i=b('select[name="form.widgets.extract_name:list"]',g);var h=i.val();if(e){d.jsonrpc.post("getExtracts",{thesaurus_name:e},{url:"/api/thesaurus/json"},function(j){i.empty();b(j.result).each(function(){b("").attr("value",this.id).attr("selected",this.id===h).text(this.text).appendTo(i)})})}i.attr("data-ams-events-handlers",'{"select2-open": "PyAMS_content.themes.getExtracts"}')},getExtracts:function(h){var e=b(h.currentTarget);var g=e.parents("form");var f=b('select[name="form.widgets.thesaurus_name:list"]',g).val();if(f){d.jsonrpc.post("getExtracts",{thesaurus_name:f},{url:"/api/thesaurus/json"},function(k){var j=b('select[name="form.widgets.extract_name:list"]',g);var i=j.data("select2");i.results.empty();i.opts.populateResults.call(i,i.results,k.result,{term:""})})}}}};a.PyAMS_content=c;b(a.document).on("PyAMS_content.changed_item",function(f,e){switch(e.object_type){case"paragraph":c.paragraphs.refreshParagraph(e);break;case"extfiles_container":c.extfiles.refreshContainer(e);break;case"links_container":c.links.refreshContainer(e);break;case"galleries_container":c.galleries.refreshContainer(e);break}})})(jQuery,this); \ No newline at end of file +(function(c,b){var e=b.MyAMS;var d={TinyMCE:{initEditor:function(f){f.image_list=d.TinyMCE.getImagesList;f.link_list=d.TinyMCE.getLinksList;return f},getImagesList:function(f){return e.ajax.post("get-images-list.json",{},f)},getLinksList:function(f){return e.ajax.post("get-links-list.json",{},f)}},profile:{switchFavorite:function(){var g=c(this);var f=g.data("sequence-oid");e.ajax.post("switch-user-favorite.json",{oid:f},function(h,i){if(h.favorite){g.removeClass("fa-star-o").addClass("fa-star")}else{g.removeClass("fa-star").addClass("fa-star-o")}})}},extfiles:{refresh:function(g){if(typeof(g)==="string"){g=JSON.parse(g)}var f=c('select[name="form.widgets.files:list"]');var h=f.data("select2");c("").attr("value",g.new_file.id).attr("selected","selected").text(g.new_file.text).appendTo(f);var i=f.select2("data");i.push(g.new_file);f.select2("data",i);h.results.empty();h.opts.populateResults.call(h,h.results,g.files,{term:""})},refreshContainer:function(h){var f=c('tr[data-ams-element-name="'+h.object_name+'"]');var g=c("span.count",c("div.action.extfiles",f));if(h.nb_files>0){g.text("("+h.nb_files+")")}else{g.text("")}}},links:{refresh:function(g){if(typeof(g)==="string"){g=JSON.parse(g)}var f=c('select[name="form.widgets.links:list"]');var h=f.data("select2");c("").attr("value",g.new_link.id).attr("selected","selected").text(g.new_link.text).appendTo(f);var i=f.select2("data");i.push(g.new_link);f.select2("data",i);h.results.empty();h.opts.populateResults.call(h,h.results,g.links,{term:""})},refreshContainer:function(h){var f=c('tr[data-ams-element-name="'+h.object_name+'"]');var g=c("span.count",c("div.action.links",f));if(h.nb_links>0){g.text("("+h.nb_links+")")}else{g.text("")}}},galleries:{refresh:function(g){if(typeof(g)==="string"){g=JSON.parse(g)}var f=c('select[name="form.widgets.galleries:list"]');var h=f.data("select2");c("").attr("value",g.new_gallery.id).attr("selected","selected").text(g.new_gallery.text).appendTo(f);var i=f.select2("data");i.push(g.new_gallery);f.select2("data",i);h.results.empty();h.opts.populateResults.call(h,h.results,g.galleries,{term:""})},setOrder:function(h,i){if(i&&i.item.hasClass("already-dropped")){return}var f=i.item.parents(".gallery");var g=c(".image",f).listattr("data-ams-element-name");e.ajax.post(f.data("ams-location")+"/set-images-order.json",{images:JSON.stringify(g)})},removeFile:function(f){return function(){var g=c(this);e.skin.bigBox({title:e.i18n.WARNING,content:'  '+e.i18n.DELETE_WARNING,buttons:e.i18n.BTN_OK_CANCEL},function(k){if(k===e.i18n.BTN_OK){var j=g.parents(".gallery");var i=j.data("ams-location");var l=g.parents(".image");var h=l.data("ams-element-name");e.ajax.post(i+"/delete-element.json",{object_name:h},function(m,n){l.remove()})}})}},refreshContainer:function(h){var f=c('tr[data-ams-element-name="'+h.object_name+'"]');var g=c("span.count",c("div.action.galleries",f));if(h.nb_galleries>0){g.text("("+h.nb_galleries+")")}else{g.text("")}}},paragraphs:{switchVisibility:function(f){return function(){var i=c(this);var g=i.parents("tr");var h=g.parents("table");e.ajax.post(h.data("ams-location")+"/set-paragraph-visibility.json",{object_name:g.data("ams-element-name")},function(j,k){if(j.visible){c("i",i).attr("class","fa fa-fw fa-eye")}else{c("i",i).attr("class","fa fa-fw fa-eye-slash text-danger")}})}},refreshParagraph:function(h){var g=c('table[id="paragraphs_list"]');var f=c('tr[data-ams-element-name="'+h.object_name+'"]',g);if(h.visible){c("i",c("td.switcher",f)).removeClass("fa-eye-slash text-danger").addClass("fa-eye")}else{c("i",c("td.switcher",f)).removeClass("fa-eye").addClass("fa-eye-slash text-danger")}c("span.title",f).text(h.title||"--")},switchEditor:function(h){var k=c(this);var j=c("i",k);var l=k.parents("td");var i=c(".editor",l);var f=k.parents("tr");if(j.hasClass("fa-plus-square-o")){var g=f.parents("table");i.html('

    ');e.ajax.post(g.data("ams-location")+"/get-paragraph-editor.json",{object_name:f.data("ams-element-name")},function(m){i.html(m);if(m){e.initContent(i);j.removeClass("fa-plus-square-o").addClass("fa-minus-square-o");f.data("ams-disabled-handlers",true)}})}else{e.skin.cleanContainer(i);i.empty();j.removeClass("fa-minus-square-o").addClass("fa-plus-square-o");f.removeData("ams-disabled-handlers")}},switchAllEditors:function(g){var i=c(this);var h=c("i",i);var f=i.parents("table");if(h.hasClass("fa-plus-square-o")){h.removeClass("fa-plus-square-o").addClass("fa-cog fa-spin");e.ajax.post(f.data("ams-location")+"/get-paragraphs-editors.json",{},function(k){for(var l in k){if(!k.hasOwnProperty(l)){continue}var j=c('tr[data-ams-element-name="'+l+'"]',f);var m=c(".editor",j);if(m.is(":empty")){m.html(k[l]);e.initContent(m)}c(".fa-plus-square-o",j).removeClass("fa-plus-square-o").addClass("fa-minus-square-o");j.data("ams-disabled-handlers",true)}if(!c("i.fa-plus-square-o",c("tbody",f)).exists()){h.removeClass("fa-cog fa-spin").addClass("fa-minus-square-o")}})}else{c(".editor",f).each(function(){e.skin.cleanContainer(c(this));c(this).empty()});c(".fa-minus-square-o",f).removeClass("fa-minus-square-o").addClass("fa-plus-square-o");c("tr",f).removeData("ams-disabled-handlers")}}},themes:{initExtracts:function(h){var g=c('select[name="form.widgets.thesaurus_name:list"]',h);var f=g.val();var j=c('select[name="form.widgets.extract_name:list"]',h);var i=j.val();if(f){e.jsonrpc.post("getExtracts",{thesaurus_name:f},{url:"/api/thesaurus/json"},function(k){j.empty();c(k.result).each(function(){c("").attr("value",this.id).attr("selected",this.id===i).text(this.text).appendTo(j)})})}j.attr("data-ams-events-handlers",'{"select2-open": "PyAMS_content.themes.getExtracts"}')},getExtracts:function(i){var f=c(i.currentTarget);var h=f.parents("form");var g=c('select[name="form.widgets.thesaurus_name:list"]',h).val();if(g){e.jsonrpc.post("getExtracts",{thesaurus_name:g},{url:"/api/thesaurus/json"},function(l){var k=c('select[name="form.widgets.extract_name:list"]',h);var j=k.data("select2");j.results.empty();j.opts.populateResults.call(j,j.results,l.result,{term:""})})}}},review:{timer:null,timer_duration:{general:30000,chat:5000},initComments:function(g){var f=c(".chat-body",g);f.animate({scrollTop:f[0].scrollHeight},1000);clearInterval(d.review.timer);d.review.timer=setInterval(d.review.updateComments,d.review.timer_duration.chat);e.skin.registerCleanCallback(d.review.cleanCommentsCallback)},cleanCommentsCallback:function(){clearInterval(d.review.timer);d.review.timer=setInterval(d.review.updateComments,d.review.timer_duration.general)},updateComments:function(){var f=c(".badge",'nav a[href="#review-comments.html"]'),h;var g=c(".chat-body",".widget-body");if(g.exists()){h=c(".message",g).length}else{h=parseInt(f.text())}e.ajax.post("get-last-review-comments.json",{count:h},function(i){if(g.exists()){f.removeClass("bg-color-danger").addClass("bg-color-info")}if(h!==i.count){f.text(i.count).removeClass("hidden");if(g.exists()){c(".messages",g).append(i.content);g.animate({scrollTop:g[0].scrollHeight},1000)}if(!g.exists()){f.removeClass("bg-color-info").addClass("bg-color-danger").animate({padding:"3px 12px 2px","margin-right":"9px"},"slow",function(){c(this).animate({padding:"3px 6px 2px","margin-right":"15px"},"slow")})}}})},initCommentData:function(f){var g=c(".chat-body",".widget-body");return{count:c(".message",g).length}},addCommentCallback:function(g){var h=c(this);var i=h.parents(".widget-body");c(".messages",i).append(g.content);c('textarea[name="comment"]',h).val("");var f=c(".chat-body",i);f.animate({scrollTop:f[0].scrollHeight},1000);c(".badge",'nav a[href="#review-comments.html"]').text(g.count).removeClass("hidden")}}};b.PyAMS_content=d;c(b.document).on("PyAMS_content.changed_item",function(g,f){switch(f.object_type){case"paragraph":d.paragraphs.refreshParagraph(f);break;case"extfiles_container":d.extfiles.refreshContainer(f);break;case"links_container":d.links.refreshContainer(f);break;case"galleries_container":d.galleries.refreshContainer(f);break;case"review_comments":d.review.updateComments();break}});var a=c(".badge",'nav a[href="#review-comments.html"]');if(a.exists()){d.review.timer=setInterval(d.review.updateComments,d.review.timer_duration.general)}})(jQuery,this); \ No newline at end of file diff -r f907592596bf -r 7cdbe0f6e5c2 src/pyams_content/skin/resources/less/pyams_content.less --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/pyams_content/skin/resources/less/pyams_content.less Thu Jun 02 15:31:37 2016 +0200 @@ -0,0 +1,42 @@ +.ams-widget.comments { + + .widget-body { + position: fixed; + height: ~"calc(100% - 310px)"; + + .chat-body { + position: relative; + height: 100%; + } + + .chat-footer { + position: fixed; + bottom: 10px; + } + + &, + .chat-footer { + width: ~"calc(100% - 240px)"; + + @media (max-width: 767px) { + width: ~"calc(100% - 10px)"; + } + @media (min-width: 768px) and (max-width: 979px) { + width: ~"calc(100% - 20px)"; + } + + .minified & { + width: ~"calc(100% - 65px)"; + + @media (max-width: 767px) { + & { + width: ~"calc(100% - 55px)"; + } + } + } + .hidden-menu & { + width: ~"calc(100% - 30px)"; + } + } + } +}