diff --git a/README.rst b/README.rst
index 723b8230..cfe856a8 100644
--- a/README.rst
+++ b/README.rst
@@ -11,6 +11,35 @@ django-nyt
.. image:: http://codecov.io/github/django-wiki/django-nyt/coverage.svg?branch=master
:target: http://codecov.io/github/django-wiki/django-nyt?branch=master
+What the fork
+-------------
+It's a fork migrated to channels-2 and pure async code
+Also, added presence trigger to user model.
+For using presence you need add method to your UserModel like
+.. code:: python
+
+ def update_presence(self, presence):
+ UserPresence.objects.update_or_create(
+ defaults={'user': self, 'status': presence or False}
+ )
+
+ class UserPresence(models.Model):
+ user = models.OneToOneField('custom_app.UserCustomModel',
+ related_name='presence',
+ on_delete=models.CASCADE)
+ status = models.BooleanField(default=False)
+ last_updated = models.DateTimeField(auto_now=True, editable=False)
+
+ class Meta:
+ verbose_name = _('user presence')
+ verbose_name_plural = _("user presences")
+
+ def __str__(self):
+ status = _('Online') if self.status else _('Offline')
+ return f'{status} on {self.last_updated}'
+
+`custom_app.UserCustomModel` replace to your user model or use django's default
+
Concept
-------
@@ -32,18 +61,13 @@ an interval of their choice.
Data can be accessed easily from Django models or from the included JSON
views.
-Channels (django-channels)
+Channels (django-channels) UPD in 1.2.0
--------------------------
-Starting from django-nyt 1.0, support for the upcoming
-`channels `_ has been added together with
-Django 1.9 and 1.10 support.
-
-In order to install the prerelease, use an extra flag for pip:
-
-.. code:: bash
+Starting from django-nyt 1.2.0, working on
+`channels-2 `_
+together with Django 2.1+
- pip install django-nyt --pre
Docs
diff --git a/django_nyt/__init__.py b/django_nyt/__init__.py
index e4331ff9..723fc2eb 100644
--- a/django_nyt/__init__.py
+++ b/django_nyt/__init__.py
@@ -1,5 +1,5 @@
_disable_notifications = False
-__version__ = "1.1.2"
+__version__ = "1.4.0"
default_app_config = "django_nyt.apps.DjangoNytConfig"
diff --git a/django_nyt/consumers.py b/django_nyt/consumers.py
index 98ba8765..1fa5c7a4 100644
--- a/django_nyt/consumers.py
+++ b/django_nyt/consumers.py
@@ -1,57 +1,110 @@
-import logging
-
-from channels import Group
-from channels.auth import channel_session_user, channel_session_user_from_http
+from logging import getLogger
+from channels.consumer import AsyncConsumer
+from channels.db import database_sync_to_async
+import json
+import six
from . import models, settings
-logger = logging.getLogger(__name__)
+logger = getLogger(name=__name__)
+
+
+class NytConsumer(AsyncConsumer):
+
+ @database_sync_to_async
+ def get_subscriptions(self):
+ """
+ :return: Subscription query for a given message's user
+ """
+ user = self.scope['user']
+ if user and user.is_authenticated:
+ return models.Subscription.objects.filter(settings__user=user)
+ else:
+ return models.Subscription.objects.none()
-def get_subscriptions(message):
- """
- :return: Subscription query for a given message's user
- """
- if message.user.is_authenticated:
- return models.Subscription.objects.filter(settings__user=message.user)
- else:
- return models.Subscription.objects.none()
+ @database_sync_to_async
+ def presence_out(self):
+ user = self.scope['user']
+ if hasattr(user, 'update_presence'):
+ user.update_presence(False)
+ @database_sync_to_async
+ def presence_in(self):
+ user = self.scope['user']
+ if hasattr(user, 'update_presence'):
+ user.update_presence(True)
-@channel_session_user_from_http
-def ws_connect(message):
- """
- Connected to websocket.connect
- """
- logger.debug("Adding new connection for user {}".format(message.user))
- message.reply_channel.send({"accept": True})
+ async def websocket_connect(self, event):
+ """
+ Connected to websocket.connect
+ """
+ logger.debug("Adding new connection for user {}".format(self.scope['user']))
+ await self.presence_in()
+ await self.send({"type": "websocket.accept"})
- for subscription in get_subscriptions(message):
- Group(
- settings.NOTIFICATION_CHANNEL.format(
- notification_key=subscription.notification_type.key
+ subscriptions = await self.get_subscriptions()
+ for subscription in subscriptions:
+ await self.channel_layer.group_add(
+ settings.NOTIFICATION_CHANNEL.format(
+ notification_key=subscription.notification_type.key
+ ), self.channel_name
)
- ).add(message.reply_channel)
-
-
-@channel_session_user
-def ws_disconnect(message):
- """
- Connected to websocket.disconnect
- """
- logger.debug("Removing connection for user {} (disconnect)".format(message.user))
- for subscription in get_subscriptions(message):
- Group(
- settings.NOTIFICATION_CHANNEL.format(
- notification_key=subscription.notification_type.key
+
+ await self.channel_layer.group_add(
+ 'nyt_personal-{}'.format(self.scope['user'].api_uuid), self.channel_name
+ )
+
+ async def wsconnect(self, event):
+ """
+ Connected to wsconnect
+ """
+ await self.websocket_connect(event)
+
+ async def websocket_disconnect(self, event):
+ """
+ Connected to websocket.disconnect
+ """
+ logger.debug("Removing connection for user {} (disconnect)".format(self.scope['user']))
+ await self.presence_out()
+ subscriptions = await self.get_subscriptions()
+ for subscription in subscriptions:
+ await self.channel_layer.group_discard(
+ settings.NOTIFICATION_CHANNEL.format(
+ notification_key=subscription.notification_type.key
+ ), self.channel_name
)
- ).discard(message.reply_channel)
+ await self.channel_layer.group_discard(
+ 'nyt_personal-{}'.format(self.scope['user'].api_uuid), self.channel_name
+ )
+
+ async def wsdisconnect(self, event):
+ """
+ Connected to wsdisconnect
+ """
+ await self.websocket_disconnect(event)
+
+ async def websocket_subscribe(self, event):
+ logger.debug("Adding new subscription on channel layer for user {}".format(self.scope['user']))
+
+ await self.channel_layer.group_add(
+ event['room'], self.channel_name
+ )
+
+ async def websocket_receive(self, event):
+ await self.send(self.event_to_msg(event))
+
+ async def websocket_send(self, event):
+ await self.send(self.event_to_msg(event))
+
+ def event_to_msg(self, event):
+ if event.get('text'):
+ return {"type": "websocket.send", 'text': self.parse_event_text(event['text'])}
+ return {"type": "websocket.send", 'text': 'empty message'}
-def ws_receive(message):
- """
- Receives messages, this is currently just for debugging purposes as there
- is no communication API for the websockets.
- """
- logger.debug("Received a message, responding with a non-API message")
- message.reply_channel.send({'text': 'OK'})
+ @staticmethod
+ def parse_event_text(text):
+ if not isinstance(text, six.string_types):
+ return json.dumps(text)
+ return text
diff --git a/django_nyt/routing.py b/django_nyt/routing.py
index da74e7fa..ec89de96 100644
--- a/django_nyt/routing.py
+++ b/django_nyt/routing.py
@@ -1,9 +1,13 @@
-from channels.routing import route
-
+from django.urls import path
+from channels.auth import AuthMiddlewareStack
+from channels.routing import ProtocolTypeRouter, URLRouter
from . import consumers
-channel_routing = [
- route("websocket.connect", consumers.ws_connect, path=r"^/nyt/?$"),
- route("websocket.disconnect", consumers.ws_disconnect),
- route("websocket.receive", consumers.ws_receive),
-]
+
+main_router = ProtocolTypeRouter({
+ 'websocket': AuthMiddlewareStack(
+ URLRouter([
+ path('nyt', consumers.NytConsumer),
+ ])
+ )
+})
diff --git a/django_nyt/subscribers.py b/django_nyt/subscribers.py
index 2c2dfb17..14401d50 100644
--- a/django_nyt/subscribers.py
+++ b/django_nyt/subscribers.py
@@ -1,10 +1,10 @@
-import logging
-
-from channels import Group
+from logging import getLogger
+from channels.layers import get_channel_layer
+from asgiref.sync import async_to_sync
from . import models, settings
-logger = logging.getLogger(__name__)
+logger = getLogger(name=__name__)
def notify_subscribers(notifications, key):
@@ -13,15 +13,22 @@ def notify_subscribers(notifications, key):
"""
logger.debug("Broadcasting to subscribers")
-
+ channel_layer = get_channel_layer()
notification_type_ids = models.NotificationType.objects.values('key').filter(key=key)
for notification_type in notification_type_ids:
- g = Group(
- settings.NOTIFICATION_CHANNEL.format(
- notification_key=notification_type['key']
- )
- )
- g.send(
- {'text': 'new-notification'}
- )
+ channel_name = settings.NOTIFICATION_CHANNEL.format(notification_key=notification_type['key'])
+ async_to_sync(channel_layer.group_send)(channel_name, {'type': 'websocket.send', 'text': 'new-notification'})
+
+
+def subscribe_channels(subscribe):
+ """
+ subscribe connected user on channels level
+ """
+
+ logger.debug("Broadcasting to subscribers")
+ channel_layer = get_channel_layer()
+ subscriber_room = 'nyt_personal-{}'.format(subscribe.settings.user.api_uuid)
+ channel_name = settings.NOTIFICATION_CHANNEL.format(notification_key=subscribe.notification_type.key)
+
+ async_to_sync(channel_layer.group_send)(subscriber_room, {'type': 'websocket.subscribe', "room": channel_name})
diff --git a/django_nyt/utils.py b/django_nyt/utils.py
index 76dd1961..7f1ec868 100644
--- a/django_nyt/utils.py
+++ b/django_nyt/utils.py
@@ -1,7 +1,6 @@
from django.db.models import Model
from django.utils.translation import gettext as _
-
-from . import _disable_notifications, models, settings
+from . import _disable_notifications, models, settings as nyt_settings
def notify(message, key, target_object=None, url=None, filter_exclude={}, recipient_users=None):
@@ -51,7 +50,7 @@ def notify(message, key, target_object=None, url=None, filter_exclude={}, recipi
)
# Notify channel subscribers if we have channels enabled
- if settings.ENABLE_CHANNELS:
+ if nyt_settings.ENABLE_CHANNELS:
from django_nyt import subscribers
subscribers.notify_subscribers(objects, key)
@@ -73,10 +72,16 @@ def subscribe(settings, key, content_type=None, object_id=None, **kwargs):
:param: **kwargs: Additional models.Subscription field values
"""
notification_type = models.NotificationType.get_by_key(key, content_type=content_type)
-
- return models.Subscription.objects.get_or_create(
+ subscription = models.Subscription.objects.get_or_create(
settings=settings,
notification_type=notification_type,
object_id=object_id,
**kwargs
)[0]
+
+ # Notify channel subscribers if we have channels enabled
+ if nyt_settings.ENABLE_CHANNELS:
+ from django_nyt import subscribers
+ subscribers.subscribe_channels(subscription)
+
+ return subscription
diff --git a/django_nyt/views.py b/django_nyt/views.py
index baa96b11..403c0593 100644
--- a/django_nyt/views.py
+++ b/django_nyt/views.py
@@ -1,5 +1,5 @@
from django.contrib.auth.decorators import login_required
-from django.db.models import Q
+from django.db.models import Q, Count
from django.shortcuts import get_object_or_404, redirect
from django.utils.translation import gettext as _
from django_nyt import models
@@ -37,7 +37,7 @@ def get_notifications(
notifications = models.Notification.objects.filter(
Q(subscription__settings__user=request.user) |
- Q(user=request.user),
+ Q(user=request.user)
)
if is_viewed is not None:
diff --git a/setup.py b/setup.py
index 568650eb..e01f89f6 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,6 @@
from django_nyt import __version__
-
packages = find_packages()
setup(
diff --git a/test-project/testapp/routing.py b/test-project/testapp/routing.py
new file mode 100644
index 00000000..db9dfdc6
--- /dev/null
+++ b/test-project/testapp/routing.py
@@ -0,0 +1,5 @@
+from channels.routing import ProtocolTypeRouter
+
+application = ProtocolTypeRouter({
+ # Empty for now (http->django views is added by default)
+})
diff --git a/test-project/testproject/settings/__init__.py b/test-project/testproject/settings/__init__.py
index 340e5bef..4c3e9633 100644
--- a/test-project/testproject/settings/__init__.py
+++ b/test-project/testproject/settings/__init__.py
@@ -76,7 +76,6 @@
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
@@ -154,3 +153,4 @@
NYT_ENABLE_ADMIN = True
+ASGI_APPLICATION = "testapp.routing.application"