First commit
authorThierry Florac <thierry.florac@onf.fr>
Thu, 02 Jun 2016 15:52:52 +0200
changeset 0 f53281280c23
child 1 b394108019cd
First commit
src/pyams_notify/__init__.py
src/pyams_notify/doctests/README.txt
src/pyams_notify/event.py
src/pyams_notify/handlers/__init__.py
src/pyams_notify/handlers/login.py
src/pyams_notify/handlers/workflow.py
src/pyams_notify/include.py
src/pyams_notify/interfaces/__init__.py
src/pyams_notify/locales/fr/LC_MESSAGES/pyams_notify.mo
src/pyams_notify/locales/fr/LC_MESSAGES/pyams_notify.po
src/pyams_notify/locales/pyams_notify.pot
src/pyams_notify/resources.py
src/pyams_notify/skin/__init__.py
src/pyams_notify/skin/resources/js/jquery-WebSocket.js
src/pyams_notify/skin/resources/js/jquery-WebSocket.min.js
src/pyams_notify/skin/resources/js/pyams_notify.js
src/pyams_notify/skin/resources/js/pyams_notify.min.js
src/pyams_notify/tests/__init__.py
src/pyams_notify/tests/test_utilsdocs.py
src/pyams_notify/tests/test_utilsdocstrings.py
src/pyams_notify/viewlet/__init__.py
src/pyams_notify/viewlet/templates/notifications.pt
src/pyams_notify/views/__init__.py
src/pyams_notify/views/context.py
src/pyams_notify/views/notification.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/__init__.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,24 @@
+#
+# 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'
+
+
+from pyramid.i18n import TranslationStringFactory
+_ = TranslationStringFactory('pyams_notify')
+
+
+def includeme(config):
+    """Pyramid include"""
+
+    from .include import include_package
+    include_package(config)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/doctests/README.txt	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,3 @@
+====================
+pyams_notify package
+====================
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/event.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,120 @@
+#
+# 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 json
+from datetime import datetime
+
+# import interfaces
+from pyams_notify.interfaces import INotification, INotificationHandler
+from pyams_security.interfaces import IPrincipalInfo
+from pyams_security.interfaces.profile import IPublicProfile
+from pyams_skin.interfaces.configuration import IBackOfficeConfiguration
+from transaction.interfaces import ITransactionManager
+
+# import packages
+from pyams_security.utility import get_principal
+from pyams_utils.date import format_datetime
+from pyams_utils.request import query_request
+from pyams_utils.url import absolute_url
+from ws4py.client.threadedclient import WebSocketClient
+from zope.interface import implementer
+from zope.schema.fieldproperty import FieldProperty
+
+
+@implementer(INotification)
+class Notification(object):
+    """Notification object"""
+
+    action = FieldProperty(INotification['action'])
+    category = FieldProperty(INotification['category'])
+    title = FieldProperty(INotification['title'])
+    message = FieldProperty(INotification['message'])
+    source = FieldProperty(INotification['source'])
+    target = FieldProperty(INotification['target'])
+    url = FieldProperty(INotification['url'])
+    timestamp = FieldProperty(INotification['timestamp'])
+    user_data = FieldProperty(INotification['user_data'])
+
+    def __init__(self, request, **settings):
+        self.request = request
+        self.context = settings.pop('context', request.context)
+        self.action = settings.pop('action', None)
+        self.category = settings.pop('category', None)
+        self.message = settings.pop('message', None)
+        source = settings.pop('source', request.principal.id)
+        if IPrincipalInfo.providedBy(source):
+            source = source.id
+        self.source_id = source
+        self.url = settings.pop('url', None)
+        self.timestamp = format_datetime(datetime.utcnow())
+        self.user_data = settings
+
+    def get_source(self, principal_id=None):
+        if principal_id is None:
+            principal_id = self.request.principal.id
+        principal = get_principal(self.request, principal_id)
+        self.source = {'id': principal.id,
+                       'title': principal.title,
+                       'avatar': absolute_url(IPublicProfile(principal).avatar,
+                                              self.request,
+                                              '++thumb++square:32x32.png')}
+        configuration = IBackOfficeConfiguration(self.request.root)
+        self.title = '{0} - {1}'.format(configuration.short_title, principal.title)
+
+    def get_target(self):
+        handler = self.request.registry.queryAdapter(self, INotificationHandler, name=self.category)
+        if handler is not None:
+            self.target = handler.get_target()
+
+    def send(self):
+        self.get_source(self.source_id)
+        self.get_target()
+        ITransactionManager(self.request.context).get().addAfterCommitHook(notify, kws={'event': self,
+                                                                                        'request': self.request})
+
+
+class NotificationEncoder(json.JSONEncoder):
+    """Notification encoder"""
+
+    def default(self, obj):
+        if isinstance(obj, Notification):
+            return {'action': obj.action,
+                    'category': obj.category,
+                    'title': obj.title,
+                    'message': obj.message,
+                    'source': obj.source,
+                    'target': obj.target,
+                    'url': obj.url,
+                    'timestamp': obj.timestamp}
+        else:
+            return super(NotificationEncoder, self).default(obj)
+
+
+def notify(status, event, request=None):
+    """Send event notification"""
+    if not status:  # aborted transaction
+        return
+    if request is None:
+        request = query_request()
+    if request is None:
+        return
+    ws = WebSocketClient('ws://{0}/notify'.format(request.registry.settings.get('pyams_notify.tcp_handler')))
+    ws.connect()
+    try:
+        json_data = json.dumps(event, cls=NotificationEncoder)
+        ws.send(json_data)
+    finally:
+        ws.close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/handlers/__init__.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/handlers/login.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,62 @@
+#
+# 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_notify.interfaces import INotification, INotificationHandler
+from pyams_security.interfaces import IAuthenticatedPrincipalEvent, IProtectedObject
+
+# import packages
+from pyams_notify.event import Notification
+from pyams_security.utility import get_principal
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.request import query_request
+from pyramid.events import subscriber
+
+from pyams_notify import _
+
+
+#
+# User authentication notification
+#
+
+@subscriber(IAuthenticatedPrincipalEvent)
+def handle_authenticated_principal(event):
+    """Handle authenticated principal"""
+    request = query_request()
+    if request is None:
+        return
+    translate = request.localizer.translate
+    principal = get_principal(request, event.principal_id)
+    notification = Notification(request=request,
+                                action='notify',
+                                category='user.login',
+                                source=event.principal_id,
+                                message=translate(_("User {0} logged in...")).format(principal.title))
+    notification.send()
+
+
+@adapter_config(name='user.login', context=INotification, provides=INotificationHandler)
+class UserLoginNotificationHandler(ContextAdapter):
+    """User login notification handler"""
+
+    def get_target(self):
+        root = self.context.request.root
+        protection = IProtectedObject(root)
+        principals = {'system:admin'} | \
+                     protection.get_principals('system.Manager') | \
+                     protection.get_principals('pyams.Webmaster')
+        return {'principals': tuple(principals)}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/handlers/workflow.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,69 @@
+#
+# 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_i18n.interfaces import II18n
+from pyams_notify.interfaces import INotification, INotificationHandler
+from pyams_security.interfaces import IProtectedObject
+from pyams_workflow.interfaces import IWorkflowTransitionEvent
+
+# import packages
+from pyams_notify.event import Notification
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.request import query_request
+from pyams_utils.url import absolute_url
+from pyramid.events import subscriber
+from pyramid.location import lineage
+
+
+@subscriber(IWorkflowTransitionEvent)
+def handle_workflow_event(event):
+    """Handle workflow transition event"""
+    request = query_request()
+    if request is None:
+        return
+    transition = event.transition
+    if transition.user_data.get('notify') is None:
+        return
+    translate = request.localizer.translate
+    notification = Notification(request=request,
+                                action='notify',
+                                category='content.workflow',
+                                message=translate(transition.user_data['notify_message']).format(
+                                    II18n(event.object).query_attribute('title', request=request)),
+                                url=absolute_url(event.object, request, 'admin.html'),
+                                transition=transition)
+    notification.send()
+
+
+@adapter_config(name='content.workflow', context=INotification, provides=INotificationHandler)
+class ContentWorkflowTransitionNotificationHandler(ContextAdapter):
+    """Content workflow transition notification handler"""
+
+    def get_target(self):
+        notified_roles = self.context.user_data['transition'].user_data['notify']
+        if '*' in notified_roles:
+            return {}
+        principals = set()
+        for context in lineage(self.context.context):
+            for role_id in notified_roles:
+                protection = IProtectedObject(context, None)
+                if protection is not None:
+                    principals |= protection.get_principals(role_id)
+        if self.context.source['id'] in principals:
+            principals.remove(self.context.source['id'])
+        return {'principals': tuple(principals)}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/include.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,30 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+
+
+def include_package(config):
+    """Pyramid include"""
+
+    # add translations
+    config.add_translation_dirs('pyams_notify:locales')
+
+    # load registry components
+    config.scan()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/interfaces/__init__.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,70 @@
+#
+# 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.interface import Interface
+
+# import packages
+from zope.schema import TextLine, Text, Dict, Datetime
+
+from pyams_notify import _
+
+
+MEMCACHED_QUEUE_KEY = b'_PyAMS_notify_messages_queue_'
+
+
+class INotification(Interface):
+    """Notification interface"""
+
+    action = TextLine(title=_("Notification action"))
+
+    category = TextLine(title=_("Notification category"))
+
+    title = TextLine(title=_("Notification title"))
+
+    message = Text(title=_("Notification message"))
+
+    source = Dict(title=_("Notification source"),
+                  description=_("Attributes of the principal which emitted the notification"),
+                  key_type=TextLine(),
+                  value_type=TextLine())
+
+    target = Dict(title=_("Notification target"),
+                  description=_("Notification targets (principals, roles...)"),
+                  key_type=TextLine())
+
+    url = TextLine(title=_("Notification URL"),
+                   description=_("URL targetted by this notification"),
+                   required=False)
+
+    timestamp = TextLine(title=_("Notification timestamp"))
+
+    user_data = Dict(title=_("User data"))
+
+    def send(self):
+        """Send notification to recipients"""
+
+
+class INotificationHandler(Interface):
+    """Notification handler interface"""
+
+    def get_target(self):
+        """Get notification target"""
+
+
+class IUserProfileAnnotations(Interface):
+    """User profile annotations subscriptions"""
Binary file src/pyams_notify/locales/fr/LC_MESSAGES/pyams_notify.mo has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/locales/fr/LC_MESSAGES/pyams_notify.po	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,78 @@
+#
+# French translations for PACKAGE package
+# This file is distributed under the same license as the PACKAGE package.
+# Thierry Florac <tflorac@ulthar.net>, 2016.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2016-06-01 15:50+0200\n"
+"PO-Revision-Date: 2016-06-01 15:51+0200\n"
+"Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
+"Language-Team: French\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: src/pyams_notify/handlers/login.py:49
+#, python-format
+msgid "User {0} logged in..."
+msgstr "L'utilisateur {0} s'est connecté"
+
+#: src/pyams_notify/viewlet/__init__.py:36
+msgid "Notifications"
+msgstr "Notifications"
+
+#: src/pyams_notify/viewlet/templates/notifications.pt:3
+msgid "No notification to display."
+msgstr "Aucune notification à afficher."
+
+#: src/pyams_notify/interfaces/__init__.py:33
+msgid "Notification action"
+msgstr "Action de la notification"
+
+#: src/pyams_notify/interfaces/__init__.py:35
+msgid "Notification category"
+msgstr "Catgéorie"
+
+#: src/pyams_notify/interfaces/__init__.py:37
+msgid "Notification title"
+msgstr "Titre"
+
+#: src/pyams_notify/interfaces/__init__.py:39
+msgid "Notification message"
+msgstr "Message"
+
+#: src/pyams_notify/interfaces/__init__.py:41
+msgid "Notification source"
+msgstr "Source"
+
+#: src/pyams_notify/interfaces/__init__.py:42
+msgid "Attributes of the principal which emitted the notification"
+msgstr "Propriétés de l'utilisateur à la source de la notification"
+
+#: src/pyams_notify/interfaces/__init__.py:46
+msgid "Notification target"
+msgstr "Cible"
+
+#: src/pyams_notify/interfaces/__init__.py:47
+msgid "Notification targets (principals, roles...)"
+msgstr "Cibles de la notification (utilisateurs, roles...)"
+
+#: src/pyams_notify/interfaces/__init__.py:50
+msgid "Notification URL"
+msgstr "URL"
+
+#: src/pyams_notify/interfaces/__init__.py:51
+msgid "URL targetted by this notification"
+msgstr "URL visée par cette notification"
+
+#: src/pyams_notify/interfaces/__init__.py:54
+msgid "Notification timestamp"
+msgstr "Horodatage"
+
+#: src/pyams_notify/interfaces/__init__.py:56
+msgid "User data"
+msgstr "Données utilisateur"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/locales/pyams_notify.pot	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,78 @@
+# 
+# SOME DESCRIPTIVE TITLE
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2016-06-01 15:50+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+
+#: ./src/pyams_notify/handlers/login.py:49
+#, python-format
+msgid "User {0} logged in..."
+msgstr ""
+
+#: ./src/pyams_notify/viewlet/__init__.py:36
+msgid "Notifications"
+msgstr ""
+
+#: ./src/pyams_notify/viewlet/templates/notifications.pt:3
+msgid "No notification to display."
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:33
+msgid "Notification action"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:35
+msgid "Notification category"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:37
+msgid "Notification title"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:39
+msgid "Notification message"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:41
+msgid "Notification source"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:42
+msgid "Attributes of the principal which emitted the notification"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:46
+msgid "Notification target"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:47
+msgid "Notification targets (principals, roles...)"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:50
+msgid "Notification URL"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:51
+msgid "URL targetted by this notification"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:54
+msgid "Notification timestamp"
+msgstr ""
+
+#: ./src/pyams_notify/interfaces/__init__.py:56
+msgid "User data"
+msgstr ""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/resources.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,33 @@
+#
+# 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_skin.interfaces.resources import IResources
+from pyramid.interfaces import IRequest
+
+# import packages
+from pyams_notify.skin import pyams_notify
+from pyams_utils.adapter import adapter_config, ContextRequestViewAdapter
+from zope.interface import Interface
+
+
+@adapter_config(context=(Interface, IRequest, Interface), provides=IResources)
+class ResourcesAdapter(ContextRequestViewAdapter):
+    """Get context resources"""
+
+    def get_resources(self):
+        pyams_notify.need()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/skin/__init__.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,35 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from fanstatic import Library, Resource
+from pyams_skin import myams, jquery
+
+
+library = Library('pyams_notify', 'resources')
+
+websocket = Resource(library, 'js/jquery-WebSocket.js',
+                     minified='js/jquery-WebSocket.min.js',
+                     depends=[jquery],
+                     bottom=True)
+
+pyams_notify = Resource(library, 'js/pyams_notify.js',
+                        minified='js/pyams_notify.min.js',
+                        depends=[myams, websocket],
+                        bottom=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/skin/resources/js/jquery-WebSocket.js	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,754 @@
+/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
+
+/**
+ * jquery.WebSocket
+ *
+ * jquery-WebSocket.js - enables WebSocket support with emulated fallback
+ *
+ * One simple interface $.WebSocket(url, protocol, options); thats it.
+ * The same interface as current native WebSocket implementation. The same
+ * native events (onopen, onmessage, onerror, onclose) + custom event onsend.
+ *
+ * But jquery.WebSockets adds some nice features:
+ *
+ *  [x] Multiplexing - Use a single socket connection and as many logical pipes
+ *      within as your browser supports. All these pipes are emulated WebSockets
+ *      also with the same API + same events! Use each pipe as WebSocket! But
+ *      this requires you to implement the protocol on this level of communication
+ *      The data is en- + decoded in a special way to make multiplexing possible
+ *
+ *  [x] Interface for adding protocol to manipulate data before they are send
+ *      and right after they arrive before event onmessage is fired!
+ *
+ * LICENSE:
+ * jquery.WebSocket
+ *
+ * Copyright (c) 2012, Benjamin Carl - All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ * - All advertising materials mentioning features or use of this software
+ *   must display the following acknowledgement: This product includes software
+ *   developed by Benjamin Carl and other contributors.
+ * - Neither the name Benjamin Carl nor the names of other contributors
+ *   may be used to endorse or promote products derived from this
+ *   software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Please feel free to contact us via e-mail: opensource@clickalicious.de
+ *
+ * @category   jquery
+ * @package    jquery_plugin
+ * @subpackage jquery_plugin_WebSocket
+ * @author     Benjamin Carl <opensource@clickalicious.de>
+ * @copyright  2012 - 2013 Benjamin Carl
+ * @license    http://www.opensource.org/licenses/bsd-license.php The BSD License
+ * @version    0.0.3
+ * @link       http://www.clickalicious.de
+ * @see        -
+ * @since      File available since Release 1.0.0
+ */
+(function($){
+
+    // attach to jQuery
+    $.extend({
+
+        // export WebSocket = $.WebSocket
+        WebSocket: function(url, protocol, options)
+        {
+            /**
+             * The default values, use comma to separate the settings,
+             * example:
+             *
+             * @type {jquery.WebSocket}
+             * @private
+             */
+            var defaults = {
+                url: url,
+                http: null,
+                enableProtocols: false,
+                enablePipes: false,
+                encoding: 'utf-8',                                                // fallback: encoding for AJAX LP
+                method: 'post',                                                   // fallback: method for AJAX LP
+                delay: 0,                                                         // number of ms to delay open event
+                interval: 3000                                                    // number of ms between poll request
+            };
+
+            // overwrite (append) to option defaults
+            options = $.extend(
+                {},
+                defaults,
+                options
+            );
+
+            // WebSocket Id and readyStates
+            const WS_ID      = 'WebSocketPipe';
+            const CONNECTING = 0;
+            const OPEN       = 1;
+            const CLOSING    = 2;
+            const CLOSED     = 3;
+
+            // the function table to store references to callbacks
+            var _functionTable = {
+                onopen:    function()  {},
+                onerror:   function(e) {},
+                onclose:   function()  {},
+                onmessage: function(e) {},
+                send:      function(d) { _ws._send(d); }
+            };
+
+            /***********************************************************************************************************
+             *
+             * PRIVATE MEMBERS
+             *
+             **********************************************************************************************************/
+
+            /**
+             * private: _token
+             *
+             * Returns a random token on request
+             *
+             * This method is intend to return a random token on
+             * request e.g. used as pipe-Id
+             *
+             * @returns {String} Random token
+             */
+            function _token()
+            {
+                return Math.random().toString(36).substr(2);
+            };
+
+            /**
+             * private: _urlWsToHttp
+             *
+             * Converts a ws:// or wss:// style link to a http:// or https:// link
+             *
+             * This method is intend to convert ws-links to http-links
+             *
+             * @returns {String} http-link
+             */
+            function _urlWsToHttp(url)
+            {
+                var protocol = (url.attr('protocol') === 'wss') ? 'https://' : 'http://';
+                var host     = url.attr('host');
+                var port     = (
+                                   (url.attr('protocol') == 'wss' && url.attr('port') != 443) ||
+                                   (url.attr('protocol') == 'ws' && url.attr('port') != 80) ?
+                                   ':' + url.attr('port') : ''
+                               );
+                var path     = ((url.attr('path') != '/') ? url.attr('path') : '');
+
+                // return new combined url
+                return protocol + host + port + path;
+            };
+
+            /**
+             * private: _dispatchProtocol
+             *
+             * Dispatch event to registered protocol handler
+             * (events: onmessage, onsend[only on emulated WebSocket!])
+             *
+             * @param e The event object
+             *
+             * @returns The processed object
+             */
+            function _dispatchProtocol(e, direction, ws)
+            {
+                // give object event from protocol to protocol
+                for (var protocol in ws.protocols) {
+                    e = ws.protocols[protocol].callback(e, direction);
+                }
+
+                // return dirty object
+                return e;
+            };
+
+            /**
+             * private: _dispatchPipe
+             *
+             *  Dispatch event to pipe
+             * (events: onmessage, onsend)
+             */
+            function _dispatchPipe(id, e)
+            {
+                for (var pipe in ws.pipes) {
+                    if (pipe == id) {
+                        var p = ws.pipes[pipe];
+                        if (p.onmessage !== undefined &&
+                                typeof(p.onmessage) === 'function'
+                            ) {
+                               p.onmessage(e);
+                        }
+                    }
+                }
+            };
+
+            /**
+             * private: _proxy
+             *
+             * This is the proxy between an intercepted call and calls
+             * the user defined callback
+             *
+             * @param {String} trigger The trigger (event-name) to dispatch
+             * @param {Object} e       The event object to dispatch
+             *
+             * @returns The result of dispatch (depends on operation!)
+             * @private
+             */
+            function _proxy(trigger, e)
+            {
+                return _functionTable[trigger](e);
+            };
+
+            /**
+             * private: _injectHook
+             *
+             * Injects hooks into a given WebSocket object
+             *
+             * This method injects the hooks for the given name of an event (event)
+             * into a given WebSocket object (ws).
+             *
+             * @param {String} event The name of the event to hook
+             * @param {Object} ws    An instance of a WebSocket to patch (inject to)
+             *
+             * @returns void
+             * @private
+             */
+            function _injectHook(event, ws)
+            {
+                //
+                if (event == 'send') {
+                    ws._send = ws.send;
+                }
+
+                // install our proxy method for intercepting events
+                ws[event] = function(e) {
+                    var uid;
+
+                    if (event == 'onmessage') {
+                        // apply registered protocols first
+                        e = _dispatchProtocol(e, 'i', ws);
+
+                        // where to get info -> multiplexed?
+                        // if multiplexed look into package for uid
+                        if (ws.multiplexed) {
+                            // extract uid
+                            //uid = /"uid"\:\s"([a-z0-9A-z]*)"/.exec(e.data)[1];
+                            uid = JSON.parse(e.data).uid;
+                        }
+
+                        if (uid !== undefined) {
+                            $(ws.pipes[uid]).trigger(e);
+                        } else {
+                            // dispatch to all pipes
+                            for (pipe in ws.pipes) {
+                                $(ws.pipes[pipe]).trigger(
+                                    e
+                                );
+                            }
+                        }
+                    }
+
+                    (event == 'send') ? e = _dispatchProtocol(e, 'o', ws) : null;
+
+                    _proxy(event, e);
+                };
+
+                // override setter with custom hook to fetch user defined callbacks
+                window.WebSocket.prototype.__defineSetter__(
+                   event,
+                    function(v) {
+                        _functionTable[event] = v;
+                    }
+                );
+
+                // override getter with custom hook to fetch user defined callbacks
+                window.WebSocket.prototype.__defineGetter__(
+                    event,
+                    function()  {
+                        return _functionTable[event];
+                    }
+                );
+            };
+
+            /**
+             * Returns a fresh pipe-object, which is in fact an emulated WebSocket
+             * which is extended here within on the fly with our pipe logic.
+             *
+             * @param {String} url The url connect to (resource or path ...)
+             * @param {String} id The optional Id of the pipe (unique-identifier)
+             *
+             * @returns {Object} A fresh pipe (emulated WebSocket)
+             * @private
+             */
+            function _protocol(name, callback)
+            {
+                return {
+                    name: name,
+                    callback: callback
+                };
+            };
+
+            /**
+             * Returns a fresh pipe-object, which is in fact an emulated WebSocket
+             * which is extended here within on the fly with our pipe logic.
+             *
+             * @param {String} url The url connect to (resource or path ...)
+             * @param {String} id The optional Id of the pipe (unique-identifier)
+             *
+             * @returns {Object} A fresh pipe (emulated WebSocket)
+             * @private
+             */
+            function _pipe(url)
+            {
+                // create, merge and return new pipe instance
+                return $.extend(
+                    _getWebSocketSkeleton(url, false),     // merge an websocket structure  _getWebSocketSkeleton(url)
+                    {                                      // with our additions            {}
+                        packet: function(uid) {
+                            return {
+                                type: WS_ID,
+                                action: 'message',
+                                data: null,
+                                uid: uid
+                            };
+                        },
+
+                        startup: function() {
+                            var p = this.packet(this.uid, this.url);
+                            p.action = 'startup';
+                            p.data   = this.url;
+
+                            _ws.send(
+                                JSON.stringify(p)
+                            );
+
+                        },
+
+                        shutdown: function() {
+                            // send shutdown pipe packet
+                            var p = this.packet(this.uid, this.url);
+                            p.action = 'shutdown';
+
+                            // send as text string
+                            _ws.send(
+                                JSON.stringify(p)
+                            );
+                        },
+
+                        send: function(data) {
+                            // get packet
+                            var p = this.packet(this.uid, this.url);
+                            p.data = data;
+
+                            // send as text string
+                            _ws.send(
+                                JSON.stringify(p)
+                            );
+
+                            // trigger event send
+                            $(this).triggerHandler('send');
+                        },
+
+                        start: function() {
+                            // send initial packet to server for handshaking
+                            this.startup();
+
+                            // trigger event open
+                            $(this).triggerHandler('open');
+
+                            return this;
+                        },
+
+                        close: function() {
+                            // send packet close
+                            this.shutdown();
+
+                            // trigger native event WebSocket:onclose (native)
+                            $(this).triggerHandler('close');
+                        }
+                    }
+                );
+            }
+
+            /**
+             * Returns skeleton of a WebSocket object
+             *
+             * @param {String} url The url connect to (resource or path ...)
+             * @param {String} id  The optional Id of the pipe (unique-identifier)
+             *
+             * @returns {Object} A fresh pipe (emulated WebSocket)
+             * @private
+             */
+            function _getWebSocketSkeleton(url, isNative)
+            {
+                return {
+                    type: 'WebSocket',                  // CUSTOM type     String type of object
+                    uid: _token(),                      // CUSTOM uid      an unique Id used to identify the ws
+                    //native: isNative,                   // CUSTOM native   status as boolean
+                    readyState: CONNECTING,             // NATIVE default  status is 0 = CONNECTING
+                    bufferedAmount: 0,                  // NATIVE integer  currently buffered data in bytes
+                    url: url,                           // NATIVE url      The url to connect to
+
+                    send: function(data) {},            // NATIVE send()   sending "data" to server
+                    start: function() {},               // CUSTOM start()  used as custom trigger for opening
+                    close: function() {},               // NATIVE close()  closes the connection
+
+                    onopen: function() {},              // NATIVE EVENT
+                    onerror: function(e) {},            // NATIVE EVENT
+                    onclose: function() {},             // NATIVE EVENT
+                    onmessage: function(e) {},          // NATIVE EVENT
+
+                    protocols: {},                      // CUSTOM          container for protocols in user defined order
+                    pipes: {},                          // CUSTOM          container for all registered pipes
+                    multiplexed: false,                 // CUSTOM          multiplexed status of this object
+
+                    /**
+                     * Export: registerPipe()
+                     *
+                     * Creates a new pipe and return pipe-reference.
+                     *
+                     * This method creates a "pipe" which is in fact an emulated WebSocket
+                     * (the same emulation as used in older browsers). These "pipes" are
+                     * used as a logical connection within a physical connection to the Server
+                     *
+                     * Pipe 1 -> O==\     (WebSocket)    /==O -> Pipe 1
+                     * Pipe 2 -> O==========================O -> Pipe 2
+                     * Pipe 3 -> O==/                    \==O -> Pipe 3
+                     *
+                     * So we can use those Pipes in the same way like the original WebSocket object.
+                     * You can use the same events what enables you and your app to use less
+                     * connections between server and client and use different endpoints for your
+                     * services too.
+                     *
+                     * @param {String} url      The url to connect to
+                     * @param {String} protocol The protocol to use
+                     * @param {Object} options  Custom options to pass through
+                     *
+                     * @returns {Object} the instance created
+                     */
+                    registerPipe: function(url, protocol, options) {
+                        var p = new _pipe(url);
+                        this.multiplexed = true;
+
+                        // we iterate the functionTable and use the events for injecting our hooks
+                        for (event in _functionTable) {
+                            // inject all hooks except "send"
+                            if (event != 'send') {
+                                _injectHook(event, p);
+                            }
+                        }
+
+                        // try:
+                        // return ws.pipes[p.id] = p = new _pipe(url, id);
+                        var r = this.pipes[p.uid] = $.extend(p, options).start();
+                        return r;
+                    },
+                    unregisterPipe: function(id) {
+                        this.pipes[id] = null;
+                        (this.pipes.length === 0) ? this.multiplexed = false : void(0);
+                        // timer?
+                    },
+                    registerProtocol: function(name, callback) {
+                        var p = new _protocol(name, callback);
+                        return this.protocols[name] = p;
+                    },
+                    unregisterProtocol: function(name) {
+                        this.protocols[name] = null;
+                    },
+                    extension: null,
+                    protocol: null,
+                    reason: null,
+                    binaryType: null
+                };
+            }
+
+            /**
+             * Creates and return a new jQuery error event with passed var added as .data
+             *
+             * @param {Mixed} data The data to add to event
+             *
+             * @returns {jQuery.Event} An jQuery error event object
+             * @private
+             */
+            function _ErrorEvent(data)
+            {
+                // create MessageEvent event and add received data
+                var e = jQuery.Event('error');
+                e.data = data;
+                return e;
+            }
+
+            /**
+             * Creates and return a new jQuery message event with passed var added as .data
+             *
+             * @param {Mixed} data The data to add to event
+             *
+             * @returns {jQuery.Event} An jQuery message event object
+             * @private
+             */
+            function _MessageEvent(data)
+            {
+                // create MessageEvent event and add received data
+                var e = jQuery.Event('message');
+                e.data = data;
+                return e;
+            }
+
+            /**
+             * Returns an emulated WebSocket which is build upon jQueries AJAX functionality.
+             *
+             * @param {Mixed} data The data to add to event
+             *
+             * we create and return an emulated WebSocket Object. We you can use this object in a very
+             * similar way to the native WebSocket Implementation. The connection was graceful degraded to
+             * AJAX long polling ...
+             *
+             * @returns ???
+             * @private
+             */
+            function _WebSocket(url)
+            {
+                var _interval, _handler, _emulation = {
+
+                    /**
+                     * export: send
+                     *
+                     * Sends data via options.method to server (http/xhr request)
+                     *
+                     * This method is intend to ...
+                     *
+                     * @param mixed data The data to send
+                     *
+                     * @returns boolean TRUE on success, otherwise FALSE
+                     */
+                    send: function(data) {
+                        // default result is true = success
+                        var success = true;
+
+                        // send data via jQuery ajax()
+                        $.ajax({
+                            async: false,
+                            type: options.method,
+                            url: url + (
+                                (options.method == 'GET' && options.arguments) ?
+                                 '?' + $.param(options.arguments) :
+                                 ''
+                            ),
+                            data: (
+                                (options.method == 'POST' && options.arguments) ?
+                                 $.param($.extend(options.arguments, {data: data})) :
+                                 null
+                            ),
+                            dataType: 'text',
+                            contentType: "application/x-www-form-urlencoded; charset=" + options.encoding,
+                            success: function(data) {
+                                // trigger native event MessageEvent (emulated)
+                                $(_emulation).trigger(
+                                    new _MessageEvent(_dispatchProtocol(data, 'i', _emulation))
+                                );
+                            },
+                            error: function(xhr, data, errorThrown) {
+                                // in case of error no success
+                                success = false;
+
+                                // trigger native event ErrorEvent (emulated)
+                                $(_emulation).trigger(
+                                    _ErrorEvent(data)
+                                );
+                            }
+                        });
+
+                        // return result of operation
+                        return success;
+                    },
+
+                    /**
+                     * export: close
+                     *
+                     * Closes an existing and open connection
+                     *
+                     * This method is intend to ...
+                     *
+                     * @returns void
+                     */
+                    close: function() {
+                        // kill timer!
+                        clearTimeout(_handler);
+                        clearInterval(_interval);
+
+                        // update readyState
+                        this.readyState = CLOSED;
+
+                        // trigger native event WebSocket:onclose (native)
+                        $(_emulation).triggerHandler('close');
+                    }
+                };
+
+                /**
+                 * private: _poll
+                 *
+                 * Polls server for new data and returns the result
+                 *
+                 * This method is intend to ...
+                 *
+                 * @returns void
+                 * @private
+                 */
+                function _poll() {
+                    $.ajax({
+                        type: options.method,
+                        url: url + (
+                            (options.method == 'GET' && options.arguments) ?
+                             '?' + $.param(options.arguments) :
+                             ''
+                        ),
+                        dataType: 'text',
+                        data: (
+                            (options.method == 'POST' && options.arguments) ?
+                             $.param(options.arguments) :
+                             null
+                        ),
+                        success: function(data) {
+                            // trigger our emulated MessageEvent
+                            $(_emulation).trigger(
+                                new _MessageEvent(data)
+                            );
+                        },
+                        error: function(xhr, data, errorThrown) {
+                            // in case of error no success
+                            success = false;
+
+                            // trigger native event ErrorEvent (emulated)
+                            $(_emulation).trigger(
+                                _ErrorEvent(data)
+                            );
+                        }
+                    });
+
+                    // trigger custom event WebSocket:onsend (custom)
+                    $(_emulation).triggerHandler('send');
+                };
+
+                // run our emulation
+                _handler = setTimeout(
+                    function() {
+                        _emulation.readyState = OPEN;
+                        _poll();
+                        _interval = setInterval(_poll, options.interval);
+
+                        // trigger event WebSocket:onopen (emulated)
+                        $(_emulation).triggerHandler('open');
+                    },
+                    options.delay
+                );
+
+                // return emulated socket implementation
+                return _emulation;
+            };
+
+            /**
+             * private: _extend
+             *
+             * Copies all properties from source to destination object as
+             * long as they not exist in destination.
+             *
+             * This method is intend to ...
+             *
+             * @param {Object} source      The object to copy from
+             * @param {Object} destination The object to copy to
+             *
+             * @returns {Object} The object destination with new properties set
+             * @private
+             */
+            function _extend(source, destination) {
+              for (property in source) {
+                if (!destination[property]) {
+                  destination[property] = source[property];
+                }
+              }
+
+              return destination;
+            }
+
+            /**
+             * private: _getWebSocket
+             *
+             * Returns a WebSocket-Object (native or emulated)
+             *
+             * This method is intend to return a WebSocket-Object no matter
+             * if the browser supports native WebSockets or not. For older
+             * Browsers the WebSocket-Object is emulated with Ajax (Fake)
+             * Push: Long Polling.
+             *
+             * @param {String} url The resource-locator (e.g. ws://127.0.0.1:80/ ||
+             *                        ws://127.0.0.1:80/this/is/fallback/)
+             *
+             * @returns {Object} WebSocket
+             * @private
+             */
+            function _getWebSocket(url)
+            {
+              var ws, isNative = true;
+
+                // websocket support built in?
+                if (window.WebSocket) {
+                    // in firefox we use MozWebSocket (but it seems that from now (FF 17) MozWebsocket is removed)
+                    if (typeof(MozWebSocket) == 'function') {
+                      ws = new MozWebSocket(url);
+                    } else {
+                        ws = new WebSocket(url);
+                    }
+
+                } else {
+                    // inject emulated WebSocket implementation into DOM
+                    window.WebSocket = _WebSocket;
+
+                    url      = options.http;
+                    isNative = false;
+                    ws       = new WebSocket(url);
+                }
+
+                // extend it with our additions and return
+                return _extend(
+                    _getWebSocketSkeleton(url, isNative),
+                    ws
+                );
+            };
+
+            /***********************************************************************************************************
+             *
+             *  MAIN()
+             *
+             **********************************************************************************************************/
+
+            // get WebSocket object (either native or emulated)
+            var _ws = _getWebSocket(options.url);
+
+            // we iterate the functionTable and use the events for injecting our hooks
+            for (event in _functionTable) {
+                _injectHook(event, _ws);
+            }
+
+            // return WebSocket
+            return _ws;
+        }
+    });
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/skin/resources/js/jquery-WebSocket.min.js	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,1 @@
+(function(a){a.extend({WebSocket:function(h,q,d){var o={url:h,http:null,enableProtocols:false,enablePipes:false,encoding:"utf-8",method:"post",delay:0,interval:3000};d=a.extend({},o,d);const z="WebSocketPipe";const m=0;const w=1;const x=2;const c=3;var s={onopen:function(){},onerror:function(A){},onclose:function(){},onmessage:function(A){},send:function(A){t._send(A)}};function y(){return Math.random().toString(36).substr(2)}function r(B){var E=(B.attr("protocol")==="wss")?"https://":"http://";var C=B.attr("host");var A=((B.attr("protocol")=="wss"&&B.attr("port")!=443)||(B.attr("protocol")=="ws"&&B.attr("port")!=80)?":"+B.attr("port"):"");var D=((B.attr("path")!="/")?B.attr("path"):"");return E+C+A+D}function g(C,B,A){for(var D in A.protocols){C=A.protocols[D].callback(C,B)}return C}function l(D,C){for(var A in ws.pipes){if(A==D){var B=ws.pipes[A];if(B.onmessage!==undefined&&typeof(B.onmessage)==="function"){B.onmessage(C)}}}}function j(A,B){return s[A](B)}function n(B,A){if(B=="send"){A._send=A.send}A[B]=function(D){var C;if(B=="onmessage"){D=g(D,"i",A);if(A.multiplexed){C=JSON.parse(D.data).uid}if(C!==undefined){a(A.pipes[C]).trigger(D)}else{for(pipe in A.pipes){a(A.pipes[pipe]).trigger(D)}}}(B=="send")?D=g(D,"o",A):null;j(B,D)};window.WebSocket.prototype.__defineSetter__(B,function(C){s[B]=C});window.WebSocket.prototype.__defineGetter__(B,function(){return s[B]})}function v(A,B){return{name:A,callback:B}}function k(A){return a.extend(p(A,false),{packet:function(B){return{type:z,action:"message",data:null,uid:B}},startup:function(){var B=this.packet(this.uid,this.url);B.action="startup";B.data=this.url;t.send(JSON.stringify(B))},shutdown:function(){var B=this.packet(this.uid,this.url);B.action="shutdown";t.send(JSON.stringify(B))},send:function(B){var C=this.packet(this.uid,this.url);C.data=B;t.send(JSON.stringify(C));a(this).triggerHandler("send")},start:function(){this.startup();a(this).triggerHandler("open");return this},close:function(){this.shutdown();a(this).triggerHandler("close")}})}function p(B,A){return{type:"WebSocket",uid:y(),readyState:m,bufferedAmount:0,url:B,send:function(C){},start:function(){},close:function(){},onopen:function(){},onerror:function(C){},onclose:function(){},onmessage:function(C){},protocols:{},pipes:{},multiplexed:false,registerPipe:function(D,G,C){var F=new k(D);this.multiplexed=true;for(event in s){if(event!="send"){n(event,F)}}var E=this.pipes[F.uid]=a.extend(F,C).start();return E},unregisterPipe:function(C){this.pipes[C]=null;(this.pipes.length===0)?this.multiplexed=false:void (0)},registerProtocol:function(C,E){var D=new v(C,E);return this.protocols[C]=D},unregisterProtocol:function(C){this.protocols[C]=null},extension:null,protocol:null,reason:null,binaryType:null}}function b(A){var B=jQuery.Event("error");B.data=A;return B}function u(A){var B=jQuery.Event("message");B.data=A;return B}function f(C){var B,A,E={send:function(F){var G=true;a.ajax({async:false,type:d.method,url:C+((d.method=="GET"&&d.arguments)?"?"+a.param(d.arguments):""),data:((d.method=="POST"&&d.arguments)?a.param(a.extend(d.arguments,{data:F})):null),dataType:"text",contentType:"application/x-www-form-urlencoded; charset="+d.encoding,success:function(H){a(E).trigger(new u(g(H,"i",E)))},error:function(J,H,I){G=false;a(E).trigger(b(H))}});return G},close:function(){clearTimeout(A);clearInterval(B);this.readyState=c;a(E).triggerHandler("close")}};function D(){a.ajax({type:d.method,url:C+((d.method=="GET"&&d.arguments)?"?"+a.param(d.arguments):""),dataType:"text",data:((d.method=="POST"&&d.arguments)?a.param(d.arguments):null),success:function(F){a(E).trigger(new u(F))},error:function(H,F,G){success=false;a(E).trigger(b(F))}});a(E).triggerHandler("send")}A=setTimeout(function(){E.readyState=w;D();B=setInterval(D,d.interval);a(E).triggerHandler("open")},d.delay);return E}function e(B,A){for(property in B){if(!A[property]){A[property]=B[property]}}return A}function i(C){var A,B=true;if(window.WebSocket){if(typeof(MozWebSocket)=="function"){A=new MozWebSocket(C)}else{A=new WebSocket(C)}}else{window.WebSocket=f;C=d.http;B=false;A=new WebSocket(C)}return e(p(C,B),A)}var t=i(d.url);for(event in s){n(event,t)}return t}})})(jQuery);
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/skin/resources/js/pyams_notify.js	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,155 @@
+(function($, globals) {
+
+	"use strict";
+
+	var MyAMS = globals.MyAMS;
+
+	var PyAMS_notify = {
+
+		connection: null,
+
+		initConnection: function() {
+			var tcp_handler = $('[data-ams-notify-server]', '#user-activity').data('ams-notify-server');
+			if (tcp_handler) {
+				var ws = $.WebSocket('ws://' + tcp_handler + '/subscribe');
+				ws.onopen = PyAMS_notify.onSocketOpened;
+				ws.onmessage = PyAMS_notify.onSocketMessage;
+				ws.onerror = PyAMS_notify.onSocketError;
+				ws.onclose = PyAMS_notify.onSocketClosed;
+				PyAMS_notify.connection = ws;
+				setInterval(PyAMS_notify.checkConnection, 30000);
+			}
+		},
+
+		checkConnection: function() {
+			if ((PyAMS_notify.connection === null) ||
+				(PyAMS_notify.connection.readyState === WebSocket.CLOSED)) {
+				PyAMS_notify.initConnection();
+			}
+		},
+
+		onSocketOpened: function(event) {
+			console.debug("WS subscription connection opened");
+			MyAMS.ajax.post('get-notifications-context.json', {}, function(result) {
+				if (result.principal.id !== '') {
+					PyAMS_notify.connection.send(JSON.stringify({
+						action: 'subscribe',
+						principal: result.principal,
+						context: result.context
+					}));
+					MyAMS.ajax.post('get-user-notifications.json', {}, PyAMS_notify.showNotifications);
+				}
+			});
+		},
+
+		onSocketMessage: function(event) {
+			var data = JSON.parse(event.data);
+			PyAMS_notify.notifyOnDesktop(data);
+			PyAMS_notify.notifyInWebpage(data);
+		},
+
+		onSocketError: function(event) {
+			console.log(event);
+		},
+
+		onSocketClosed: function(event) {
+			PyAMS_notify.connection = null;
+			console.debug("WS connection closed");
+		},
+
+		notifyOnDesktop: function(data) {
+
+			function do_notify() {
+				var options = {
+					title: data.title,
+					body: data.message,
+					icon: data.source.avatar
+				};
+				var notification = new Notification(options.title, options);
+				notification.onclick = function() {
+					if (data.url) {
+						window.open(data.url);
+					}
+				};
+			}
+
+			if (window.Notification && (Notification.permission !== 'denied')) {
+				if (Notification.permission === 'default') {
+					Notification.requestPermission(function (status) {
+						if (status === 'granted') {
+							do_notify();
+						}
+					});
+				} else {
+					do_notify();
+				}
+			}
+		},
+
+		createNotification: function(notification) {
+			var li = $('<li></li>');
+			var span = $('<span></span>');
+			var link = $('<a></a>').addClass('msg')
+								   .attr('href', notification.url);
+			if (notification.source.avatar) {
+				$('<img>').addClass('air air-top-left margin-top-2')
+						  .attr('src', notification.source.avatar)
+						  .appendTo(link);
+			} else {
+				$('<i></i>').addClass('fa fa-2x fa-user air air-top-left img margin-left-5 margin-top-2')
+							.appendTo(link);
+			}
+			$('<time></time>').text(notification.timestamp)
+							  .appendTo(link);
+			$('<span></span>').addClass('from')
+							  .text(notification.source.title)
+							  .appendTo(link);
+			$('<span></span>').addClass('msg-body')
+							  .text(notification.message)
+							  .appendTo(link);
+			link.appendTo(span);
+			span.appendTo(li);
+			return li;
+		},
+
+		notifyInWebpage: function(data) {
+			var badge = $('.badge', '#user-activity >span');
+			var count = parseInt(badge.text());
+			badge.text(count + 1);
+			var notifications = $('.notification-body', '#user-activity');
+			PyAMS_notify.createNotification(data).prependTo(notifications);
+			MyAMS.skin.checkNotification();
+		},
+
+		showNotifications: function(data) {
+			var badge = $('.badge', '#user-activity >span');
+			badge.text(data.length);
+			var notifications = $('.notification-body', '#user-activity');
+			notifications.empty();
+			if (data.length > 0) {
+				notifications.prev('p').hide();
+				for (var index = 0; index < data.length; index++) {
+					var notification = data[index];
+					PyAMS_notify.createNotification(notification).appendTo(notifications);
+				}
+			} else {
+				notifications.prev('p').show();
+			}
+			MyAMS.skin.checkNotification();
+		},
+
+		refreshNotifications: function() {
+			return function() {
+				MyAMS.ajax.post('get-user-notifications.json', {}, PyAMS_notify.showNotifications);
+			};
+		}
+	};
+	globals.PyAMS_notify = PyAMS_notify;
+
+	MyAMS.ajax.check($.WebSocket,
+					 '/--static--/pyams_notify/js/jquery-WebSocket' + MyAMS.devext + '.js',
+					 function() {
+						PyAMS_notify.initConnection();
+					 });
+
+})(jQuery, this);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/skin/resources/js/pyams_notify.min.js	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,1 @@
+(function(c,b){var d=b.MyAMS;var a={connection:null,initConnection:function(){var f=c("[data-ams-notify-server]","#user-activity").data("ams-notify-server");if(f){var e=c.WebSocket("ws://"+f+"/subscribe");e.onopen=a.onSocketOpened;e.onmessage=a.onSocketMessage;e.onerror=a.onSocketError;e.onclose=a.onSocketClosed;a.connection=e;setInterval(a.checkConnection,30000)}},checkConnection:function(){if((a.connection===null)||(a.connection.readyState===WebSocket.CLOSED)){a.initConnection()}},onSocketOpened:function(e){console.debug("WS subscription connection opened");d.ajax.post("get-notifications-context.json",{},function(f){if(f.principal.id!==""){a.connection.send(JSON.stringify({action:"subscribe",principal:f.principal,context:f.context}));d.ajax.post("get-user-notifications.json",{},a.showNotifications)}})},onSocketMessage:function(e){var f=JSON.parse(e.data);a.notifyOnDesktop(f);a.notifyInWebpage(f)},onSocketError:function(e){console.log(e)},onSocketClosed:function(e){a.connection=null;console.debug("WS connection closed")},notifyOnDesktop:function(e){function f(){var g={title:e.title,body:e.message,icon:e.source.avatar};var h=new Notification(g.title,g);h.onclick=function(){if(e.url){window.open(e.url)}}}if(window.Notification&&(Notification.permission!=="denied")){if(Notification.permission==="default"){Notification.requestPermission(function(g){if(g==="granted"){f()}})}else{f()}}},createNotification:function(h){var e=c("<li></li>");var f=c("<span></span>");var g=c("<a></a>").addClass("msg").attr("href",h.url);if(h.source.avatar){c("<img>").addClass("air air-top-left margin-top-2").attr("src",h.source.avatar).appendTo(g)}else{c("<i></i>").addClass("fa fa-2x fa-user air air-top-left img margin-left-5 margin-top-2").appendTo(g)}c("<time></time>").text(h.timestamp).appendTo(g);c("<span></span>").addClass("from").text(h.source.title).appendTo(g);c("<span></span>").addClass("msg-body").text(h.message).appendTo(g);g.appendTo(f);f.appendTo(e);return e},notifyInWebpage:function(h){var e=c(".badge","#user-activity >span");var g=parseInt(e.text());e.text(g+1);var f=c(".notification-body","#user-activity");a.createNotification(h).prependTo(f);d.skin.checkNotification()},showNotifications:function(h){var e=c(".badge","#user-activity >span");e.text(h.length);var g=c(".notification-body","#user-activity");g.empty();if(h.length>0){g.prev("p").hide();for(var f=0;f<h.length;f++){var i=h[f];a.createNotification(i).appendTo(g)}}else{g.prev("p").show()}d.skin.checkNotification()},refreshNotifications:function(){return function(){d.ajax.post("get-user-notifications.json",{},a.showNotifications)}}};b.PyAMS_notify=a;d.ajax.check(c.WebSocket,"/--static--/pyams_notify/js/jquery-WebSocket"+d.devext+".js",function(){a.initConnection()})})(jQuery,this);
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/tests/__init__.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,1 @@
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/tests/test_utilsdocs.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,59 @@
+#
+# 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.
+#
+
+"""
+Generic Test case for pyams_notify doctest
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.dirname(__file__)
+
+def doc_suite(test_dir, setUp=None, tearDown=None, globs=None):
+    """Returns a test suite, based on doctests found in /doctest."""
+    suite = []
+    if globs is None:
+        globs = globals()
+
+    flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+             doctest.REPORT_ONLY_FIRST_FAILURE)
+
+    package_dir = os.path.split(test_dir)[0]
+    if package_dir not in sys.path:
+        sys.path.append(package_dir)
+
+    doctest_dir = os.path.join(package_dir, 'doctests')
+
+    # filtering files on extension
+    docs = [os.path.join(doctest_dir, doc) for doc in
+            os.listdir(doctest_dir) if doc.endswith('.txt')]
+
+    for test in docs:
+        suite.append(doctest.DocFileSuite(test, optionflags=flags,
+                                          globs=globs, setUp=setUp,
+                                          tearDown=tearDown,
+                                          module_relative=False))
+
+    return unittest.TestSuite(suite)
+
+def test_suite():
+    """returns the test suite"""
+    return doc_suite(current_dir)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/tests/test_utilsdocstrings.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,62 @@
+#
+# 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.
+#
+
+"""
+Generic Test case for pyams_notify doc strings
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.abspath(os.path.dirname(__file__))
+
+def doc_suite(test_dir, globs=None):
+    """Returns a test suite, based on doc tests strings found in /*.py"""
+    suite = []
+    if globs is None:
+        globs = globals()
+
+    flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+             doctest.REPORT_ONLY_FIRST_FAILURE)
+
+    package_dir = os.path.split(test_dir)[0]
+    if package_dir not in sys.path:
+        sys.path.append(package_dir)
+
+    # filtering files on extension
+    docs = [doc for doc in
+            os.listdir(package_dir) if doc.endswith('.py')]
+    docs = [doc for doc in docs if not doc.startswith('__')]
+
+    for test in docs:
+        fd = open(os.path.join(package_dir, test))
+        content = fd.read()
+        fd.close()
+        if '>>> ' not in content:
+            continue
+        test = test.replace('.py', '')
+        location = 'pyams_notify.%s' % test
+        suite.append(doctest.DocTestSuite(location, optionflags=flags,
+                                          globs=globs))
+
+    return unittest.TestSuite(suite)
+
+def test_suite():
+    """returns the test suite"""
+    return doc_suite(current_dir)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/viewlet/__init__.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,41 @@
+#
+# 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_skin.interfaces.viewlet import IActivityViewletManager
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyams_skin.viewlet.activity import ActivityViewlet
+from pyams_template.template import template_config
+from pyams_viewlet.viewlet import viewlet_config
+
+from pyams_notify import _
+
+
+@viewlet_config(name='pyams.notifications', layer=IPyAMSLayer, manager=IActivityViewletManager)
+@template_config(template='templates/notifications.pt', layer=IPyAMSLayer)
+class NotificationsViewlet(ActivityViewlet):
+    """Notifications viewlet"""
+
+    name = 'notifications'
+    label = _("Notifications")
+    url = 'PyAMS_notify.refreshNotifications'
+
+    @property
+    def notify_server(self):
+        return self.request.registry.settings.get('pyams_notify.tcp_handler')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/viewlet/templates/notifications.pt	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,6 @@
+<div class="notifications-parent no-margin" i18n:domain="pyams_notify"
+	 tal:attributes="data-ams-notify-server view.notify_server">
+	<p class="alert alert-info" i18n:translate="">No notification to display.</p>
+	<ul class="notification-body">
+	</ul>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/views/__init__.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,20 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/views/context.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,32 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+
+# import packages
+from pyramid.view import view_config
+
+
+@view_config(name='get-notifications-context.json', renderer='json', xhr=True)
+def NotificationsContextView(request):
+    """Get context for notifications"""
+    principal = request.principal
+    return {'principal': {'id': principal.id,
+                          'title': principal.title,
+                          'principals': tuple(request.effective_principals)},
+            'context': {'*': ['user.login', 'content.workflow']}}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_notify/views/notification.py	Thu Jun 02 15:52:52 2016 +0200
@@ -0,0 +1,61 @@
+#
+# 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 pickle
+try:
+    import pylibmc
+except ImportError:
+    pylibmc = None
+
+# import interfaces
+from pyams_notify.interfaces import MEMCACHED_QUEUE_KEY
+from pyams_skin.layer import IPyAMSLayer
+
+# import packages
+from pyramid.view import view_config
+
+
+def filtered(notification, request):
+    """Filter notification against current request"""
+    return request.principal.id in notification['target'].get('principals', ())
+
+
+@view_config(name='get-user-notifications.json', request_type=IPyAMSLayer, renderer='json', xhr=True)
+class UserNotificationsView(object):
+    """User notifications view"""
+
+    def __init__(self, request):
+        self.request = request
+        self.context = request.context
+
+    @property
+    def memcached_server(self):
+        return self.request.registry.settings.get('pyams_notify_ws.memcached_server')
+
+    def __call__(self):
+        if pylibmc is None:
+            return ()
+        server = self.memcached_server
+        if server is not None:
+            client = pylibmc.Client([server])
+            notifications = client.get(MEMCACHED_QUEUE_KEY)
+            if notifications is None:
+                return ()
+            else:
+                return [n for n in reversed(list(filter(lambda x: filtered(x, self.request),
+                                                        pickle.loads(notifications))))]
+        else:
+            return ()