diff --git a/src/wirecloud/live/__init__.py b/src/wirecloud/live/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/wirecloud/live/consumers.py b/src/wirecloud/live/consumers.py
new file mode 100644
index 0000000000..24fe13ece7
--- /dev/null
+++ b/src/wirecloud/live/consumers.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 CoNWeT Lab., Universidad Politécnica de Madrid
+
+# This file is part of Wirecloud.
+
+# Wirecloud is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# Wirecloud is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with Wirecloud. If not, see .
+
+from channels import Group
+from channels.auth import channel_session_user, channel_session_user_from_http
+
+
+@channel_session_user_from_http
+def ws_connect(message):
+ Group("wc-live-%s" % message.user.username).add(message.reply_channel)
+ Group("wc-live-*").add(message.reply_channel)
+
+
+@channel_session_user
+def ws_message(message):
+ Group("chat-%s" % message.channel_session['room']).send({
+ "text": message['text'],
+ })
+
+
+@channel_session_user
+def ws_disconnect(message):
+ Group("wc-live-%s" % message.user.username).discard(message.reply_channel)
+ Group("wc-live-*").discard(message.reply_channel)
diff --git a/src/wirecloud/live/models.py b/src/wirecloud/live/models.py
new file mode 100644
index 0000000000..a3f14826b3
--- /dev/null
+++ b/src/wirecloud/live/models.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 CoNWeT Lab., Universidad Politécnica de Madrid
+
+# This file is part of Wirecloud.
+
+# Wirecloud is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# Wirecloud is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with Wirecloud. If not, see .
+
+from __future__ import unicode_literals
+
+import json
+
+from channels import Group
+from django.dispatch import receiver
+from django.db.models.signals import m2m_changed, post_save
+import requests
+
+from wirecloud.platform.models import CatalogueResource, Workspace
+
+
+def notify(data, affected_users):
+ for user in affected_users:
+ Group('wc-live-%s' % user).send({"text": json.dumps(data)})
+
+
+def get_affected_users(instance):
+
+ if instance.public:
+ return '*'
+ else:
+ affected_users = set(instance.users.values_list("username", flat=True))
+ for group in instance.groups.all():
+ affected_users.update(group.user_set.values_list("username", flat=True))
+ return ",".join(affected_users)
+
+
+@receiver(post_save, sender=Workspace)
+def workspace_update(sender, instance, created, raw, **kwargs):
+
+ affected_users = get_affected_users(instance)
+
+ if affected_users != '':
+ notify(
+ {
+ "workspace": instance.id,
+ "action": "update",
+ },
+ affected_users
+ )
+
+
+@receiver(m2m_changed, sender=CatalogueResource.groups.through)
+@receiver(m2m_changed, sender=CatalogueResource.users.through)
+def update_users_or_groups(sender, instance, action, reverse, model, pk_set, using, **kwargs):
+ if reverse or action.startswith('pre_') or (pk_set is not None and len(pk_set) == 0):
+ return
+
+ affected_users = ",".join(model.objects.filter(pk__in=pk_set).values_list("username", flat=True))
+ notify(
+ {
+ "component": instance.local_uri_part,
+ "action": "installed" if action == "post_add" else "uninstalled"
+ },
+ affected_users
+ )
+
+
+@receiver(post_save, sender=CatalogueResource)
+def mac_update(sender, instance, created, raw, **kwargs):
+
+ affected_users = get_affected_users(instance)
+
+ if affected_users != '':
+ notify(
+ {
+ "component": instance.local_uri_part,
+ "action": "update",
+ },
+ affected_users
+ )
diff --git a/src/wirecloud/live/routing.py b/src/wirecloud/live/routing.py
new file mode 100644
index 0000000000..a3a383a96c
--- /dev/null
+++ b/src/wirecloud/live/routing.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 CoNWeT Lab., Universidad Politécnica de Madrid
+
+# This file is part of Wirecloud.
+
+# Wirecloud is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# Wirecloud is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with Wirecloud. If not, see .
+
+from channels.routing import route
+from wirecloud.live.consumers import ws_connect, ws_message, ws_disconnect
+
+channel_routing = [
+ route("websocket.connect", ws_connect, path="^/api/live"),
+ route("websocket.receive", ws_message),
+ route("websocket.disconnect", ws_disconnect),
+]
diff --git a/src/wirecloud/live/tests.py b/src/wirecloud/live/tests.py
new file mode 100644
index 0000000000..aa6cc160b1
--- /dev/null
+++ b/src/wirecloud/live/tests.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 CoNWeT Lab., Universidad Politécnica de Madrid
+
+# This file is part of Wirecloud.
+
+# Wirecloud is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# Wirecloud is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with Wirecloud. If not, see .
+
+from __future__ import unicode_literals
+
+import datetime
+
+from django.contrib.auth.models import User
+from mock import patch
+
+from wirecloud.commons.utils.testcases import WirecloudTestCase
+from wirecloud.platform.models import CatalogueResource, Workspace
+
+@patch('wirecloud.live.models.notify')
+class LiveNotificationsTestCase(WirecloudTestCase):
+
+ fixtures = ('selenium_test_data',)
+ tags = ('wirecloud-noselenium', 'wirecloud-live')
+
+ def setUp(self):
+ self.normuser = User.objects.get(username="normuser")
+
+ def test_new_macs_are_notified(self, notify_mock):
+ instance = CatalogueResource.objects.create(type=1, creation_date=datetime.datetime.now(), short_name="MyWidget", vendor="Wirecloud", version="1.0")
+ instance.users.add(self.normuser)
+ notify_mock.assert_called_once_with(
+ {
+ "component": "Wirecloud/MyWidget/1.0",
+ "action": "installed"
+ },
+ "normuser"
+ )
+
+ def test_workspace_updates_are_notified(self, notify_mock):
+ instance = Workspace.objects.get(pk="2")
+ instance.description = "New description"
+ instance.save()
+ notify_mock.assert_called_once_with(
+ {
+ "workspace": 2,
+ "action": "update"
+ },
+ "user_with_workspaces"
+ )