--- /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 ()