--- /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 <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
+
+# 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"""
--- 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)
--- /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 <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.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
--- 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)
--- /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 <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
+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)}}
--- /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 @@
+<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">
+ <time i18n:translate="">just now</time>
+ <a class="username" tal:content="request.principal.title">Owner</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/shared/common/zmi/templates/review-comments-json.pt Thu Jun 02 15:31:37 2016 +0200
@@ -0,0 +1,17 @@
+<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">
+ <time tal:content="view.get_age(comment)">age</time>
+ <a class="username" tal:content="principal.title">Owner</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/shared/common/zmi/templates/review-comments.pt Thu Jun 02 15:31:37 2016 +0200
@@ -0,0 +1,54 @@
+<div class="ams-widget comments" i18n:domain="pyams_content"
+ data-ams-plugins="pyams_content"
+ data-ams-plugin-pyams_content-src="/--static--/pyams_content/js/pyams_content{MyAMS.devext}.js"
+ data-ams-plugin-pyams_content-css="/--static--/pyams_content/css/pyams_content{MyAMS.devext}.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">
+ <time tal:content="view.get_age(comment)">age</time>
+ <a class="username" tal:content="principal.title">Owner</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/shared/common/zmi/templates/review-notification.pt Thu Jun 02 15:31:37 2016 +0200
@@ -0,0 +1,27 @@
+<!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" />
+</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>
+ to make a review of a content called « <strong i18n:name="title" tal:content="i18n:context.title">title</strong> »
+ which has been created on publication platform « <span i18n:name="service_name"
+ tal:content="options['settings'].service_name">Service name</span> ».
+ </p>
+ <p i18n:translate="">Comment associated with this request is:</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.html')"
+ tal:content="i18n:context.title"></a></p>
+ <br />
+ <pre tal:content="options['settings'].signature">signature</pre>
+</body>
+</html>
--- /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);
+}
--- /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
--- 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);
--- 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("<option></option>").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("<option></option>").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("<option></option>").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:'<i class="text-danger fa fa-2x fa-bell shake animated"></i> '+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('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>');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("<option></option>").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("<option></option>").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("<option></option>").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("<option></option>").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:'<i class="text-danger fa fa-2x fa-bell shake animated"></i> '+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('<h1 class="loading"><i class="fa fa-2x fa-gear fa-spin"></i></h1>');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("<option></option>").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
--- /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)";
+ }
+ }
+ }
+}