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"